Skip to content

GH-51476: [C++][Parquet] Vectorise FindMinMax explicitly - #51478

Open
domibel wants to merge 1 commit into
apache:mainfrom
domibel:findminmax-simd
Open

domibel wants to merge 1 commit into
apache:mainfrom
domibel:findminmax-simd

Conversation

@domibel

@domibel domibel commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Rationale for this change

FindMinMaxAvx2 in parquet/level_comparison_avx2.cc was not vectorised.

What changes are included in this PR?

A single xsimd kernel FindMinMaxSimd<Arch> in level_comparison_simd_kernel_internal.h,
plus support for multiple architectures.

Are these changes tested?

Yes, with a nice speedup.

dispatch level time
NONE 513 ns
SSE4_2 54.9 ns (9.3x)
AVX2 28.7 ns (17.9x)

Are there any user-facing changes?

No

Was AI used for this PR?

PR code and description written by:

  • Human
  • AI

Reviewed before submission by:

  • Human
  • AI
  • Not reviewed

@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #51476 has been automatically assigned in GitHub to PR creator.

@domibel

domibel commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

You can run the benchmark on x86_64 with:

cd build/release
ARROW_USER_SIMD_LEVEL=NONE   ./parquet-level-conversion-benchmark --benchmark_filter=FindMinMax
ARROW_USER_SIMD_LEVEL=SSE4_2 ./parquet-level-conversion-benchmark --benchmark_filter=FindMinMax
ARROW_USER_SIMD_LEVEL=AVX2   ./parquet-level-conversion-benchmark --benchmark_filter=FindMinMax

and on aarch64 with:

cd build/release
ARROW_USER_SIMD_LEVEL=NONE   ./parquet-level-conversion-benchmark --benchmark_filter=FindMinMax
./parquet-level-conversion-benchmark --benchmark_filter=FindMinMax

@wgtmac

wgtmac commented Sep 24, 2026

Copy link
Copy Markdown
Member

Do you want to take a look? @AntoinePrv

@domibel

domibel commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

@ursabot please benchmark lang=C++

@rok

rok commented Sep 28, 2026

Copy link
Copy Markdown
Member

Benchmark runs are scheduled for commit f85237b. Watch https://buildkite.com/apache-arrow and https://conbench.arrow-dev.org for updates. A comment will be posted here when the runs are complete.

}

MinMax FindMinMaxAvx2(const int16_t* levels, int64_t num_levels) {
return avx2::FindMinMaxImpl(levels, num_levels);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is FindMinMaxImpl still in the codebase? If so, can we remove it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It cannot be removed, ARROW_USER_SIMD_LEVEL=NONE would still require it.

namespace parquet::internal {

template <typename Arch>
MinMax FindMinMaxSimd(const int16_t* levels, int64_t num_levels) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AntoinePrv Do you want to take a look at this implementation?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks reasonable to me. Cannot say if it is optimal (perhaps a manual unroll could help because of the loop-carried dependency, but perhaps the compiler can tell it is associative), but if it improves then it's good!
We might be able to get a generic version of this in https://github.com/xtensor-stack/xsimd-algorithm (yet to put proper benchmarks and all).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ursabot never finished, so please run

build/release/parquet-arrow-reader-writer-benchmark \
   --benchmark_filter='BM_ReadColumn<true,'

I would like to see some independent numbers.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@github-actions github-actions Bot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Sep 28, 2026
@pitrou

pitrou commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

FindMinMaxAvx2 in parquet/level_comparison_avx2.cc was not vectorised.

You mean gcc/clang autovectorization didn't manage to vectorize this? Interesting.
cc @emkornfield @zanmato1984 @cyb70289

@pitrou pitrou added the CI: Extra: C++ Run extra C++ CI label Sep 28, 2026
@emkornfield

emkornfield commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

You mean gcc/clang autovectorization didn't manage to vectorize this? Interesting.

On Godbolt:
Clang appears to under O2 on avx2: https://godbolt.org/z/M9oojszn1
GCC does not appear to: https://godbolt.org/z/WcnG1WoW3

Interestingly, this fomulation:

MinMax FindMinMaxImpl(const int16_t* levels, int64_t num_levels) {
  MinMax out{std::numeric_limits<int16_t>::max(), std::numeric_limits<int16_t>::min()};
  if (num_levels <= 0){
    return out;
  }
  out.min = *std::min_element(levels, levels + num_levels);
  out.max = *std::max_element(levels, levels + num_levels);
  
  return out;
}

has the reverse affect on the compilers.

@cyb70289

Copy link
Copy Markdown
Contributor

Let ai dig into latest release binary, looks the function is vectorized in internal lib. Microbenchmark refuses to vectorize for some reason.
Below are avx2 and neon assembly for FindMinMax.
The conbench compare link is spinning forever, not sure if some benchmarks are improved by this PR.

avx2
$ llvm-objdump -d -C --x86-asm-syntax=intel --no-show-raw-insn \
    --start-address=0x3c8910 --stop-address=0x3c8ac4 \
    pyarrow-x86/libparquet.so.2500

pyarrow-x86/libparquet.so.2500:	file format elf64-x86-64

Disassembly of section .text:

00000000003c8910 <parquet::internal::FindMinMaxAvx2(short const*, long)>:
  3c8910:      	mov	r8, rdi
  3c8913:      	mov	rdi, rsi
  3c8916:      	test	rsi, rsi
  3c8919:      	jle	0x3c8a80 <parquet::internal::FindMinMaxAvx2(short const*, long)+0x170>
  3c891f:      	lea	rax, [rsi - 1]
  3c8923:      	cmp	rax, 14
  3c8927:      	jbe	0x3c8a90 <parquet::internal::FindMinMaxAvx2(short const*, long)+0x180>
  3c892d:      	mov	eax, 2147516416
  3c8932:      	mov	rcx, rsi
  3c8935:      	mov	rdx, r8
  3c8938:      	vmovd	xmm2, eax
  3c893c:      	shr	rcx, 4
  3c8940:      	mov	eax, 2147450879
  3c8945:      	shl	rcx, 5
  3c8949:      	vmovd	xmm0, eax
  3c894d:      	vpbroadcastd	ymm2, xmm2
  3c8952:      	add	rcx, r8
  3c8955:      	vpbroadcastd	ymm0, xmm0
  3c895a:      	nop	word ptr [rax + rax]
  3c8960:      	vmovdqu	ymm1, ymmword ptr [rdx]
  3c8964:      	add	rdx, 32
  3c8968:      	vpminsw	ymm0, ymm0, ymm1
  3c896c:      	vpmaxsw	ymm2, ymm2, ymm1
  3c8970:      	cmp	rdx, rcx
  3c8973:      	jne	0x3c8960 <parquet::internal::FindMinMaxAvx2(short const*, long)+0x50>
  3c8975:      	vextracti128	xmm3, ymm2, 1
  3c897b:      	mov	rdx, rdi
  3c897e:      	vpmaxsw	xmm1, xmm3, xmm2
  3c8982:      	and	rdx, -16
  3c8986:      	vpsrldq	xmm4, xmm1, 8           # xmm4 = xmm1[8,9,10,11,12,13,14,15],zero,zero,zero,zero,zero,zero,zero,zero
  3c898b:      	mov	ecx, edx
  3c898d:      	vpmaxsw	xmm1, xmm1, xmm4
  3c8991:      	vpsrldq	xmm4, xmm1, 4           # xmm4 = xmm1[4,5,6,7,8,9,10,11,12,13,14,15],zero,zero,zero,zero
  3c8996:      	vpmaxsw	xmm1, xmm1, xmm4
  3c899a:      	vpsrldq	xmm4, xmm1, 2           # xmm4 = xmm1[2,3,4,5,6,7,8,9,10,11,12,13,14,15],zero,zero
  3c899f:      	vpmaxsw	xmm1, xmm1, xmm4
  3c89a3:      	vextracti128	xmm4, ymm0, 1
  3c89a9:      	vpextrw	esi, xmm1, 0
  3c89ae:      	vpminsw	xmm1, xmm4, xmm0
  3c89b2:      	vpminsw	xmm0, xmm0, xmm4
  3c89b6:      	vpsrldq	xmm5, xmm1, 8           # xmm5 = xmm1[8,9,10,11,12,13,14,15],zero,zero,zero,zero,zero,zero,zero,zero
  3c89bb:      	vpminsw	xmm1, xmm1, xmm5
  3c89bf:      	vpsrldq	xmm5, xmm1, 4           # xmm5 = xmm1[4,5,6,7,8,9,10,11,12,13,14,15],zero,zero,zero,zero
  3c89c4:      	vpminsw	xmm1, xmm1, xmm5
  3c89c8:      	vpsrldq	xmm5, xmm1, 2           # xmm5 = xmm1[2,3,4,5,6,7,8,9,10,11,12,13,14,15],zero,zero
  3c89cd:      	vpminsw	xmm1, xmm1, xmm5
  3c89d1:      	vpextrw	eax, xmm1, 0
  3c89d6:      	vpmaxsw	xmm1, xmm2, xmm3
  3c89da:      	cmp	rdi, rdx
  3c89dd:      	je	0x3c8abf <parquet::internal::FindMinMaxAvx2(short const*, long)+0x1af>
  3c89e3:      	vzeroupper
  3c89e6:      	mov	r9, rdi
  3c89e9:      	sub	r9, rdx
  3c89ec:      	lea	r10, [r9 - 1]
  3c89f0:      	cmp	r10, 6
  3c89f4:      	jbe	0x3c8a53 <parquet::internal::FindMinMaxAvx2(short const*, long)+0x143>
  3c89f6:      	vmovdqu	xmm2, xmmword ptr [r8 + 2*rdx]
  3c89fc:      	mov	rdx, r9
  3c89ff:      	and	rdx, -8
  3c8a03:      	vpmaxsw	xmm1, xmm1, xmm2
  3c8a07:      	vpminsw	xmm0, xmm0, xmm2
  3c8a0b:      	add	ecx, edx
  3c8a0d:      	and	r9d, 7
  3c8a11:      	vpsrldq	xmm2, xmm1, 8           # xmm2 = xmm1[8,9,10,11,12,13,14,15],zero,zero,zero,zero,zero,zero,zero,zero
  3c8a16:      	vpmaxsw	xmm1, xmm1, xmm2
  3c8a1a:      	vpsrldq	xmm2, xmm1, 4           # xmm2 = xmm1[4,5,6,7,8,9,10,11,12,13,14,15],zero,zero,zero,zero
  3c8a1f:      	vpmaxsw	xmm1, xmm1, xmm2
  3c8a23:      	vpsrldq	xmm2, xmm1, 2           # xmm2 = xmm1[2,3,4,5,6,7,8,9,10,11,12,13,14,15],zero,zero
  3c8a28:      	vpmaxsw	xmm1, xmm1, xmm2
  3c8a2c:      	vpextrw	esi, xmm1, 0
  3c8a31:      	vpsrldq	xmm1, xmm0, 8           # xmm1 = xmm0[8,9,10,11,12,13,14,15],zero,zero,zero,zero,zero,zero,zero,zero
  3c8a36:      	vpminsw	xmm0, xmm0, xmm1
  3c8a3a:      	vpsrldq	xmm1, xmm0, 4           # xmm1 = xmm0[4,5,6,7,8,9,10,11,12,13,14,15],zero,zero,zero,zero
  3c8a3f:      	vpminsw	xmm0, xmm0, xmm1
  3c8a43:      	vpsrldq	xmm1, xmm0, 2           # xmm1 = xmm0[2,3,4,5,6,7,8,9,10,11,12,13,14,15],zero,zero
  3c8a48:      	vpminsw	xmm0, xmm0, xmm1
  3c8a4c:      	vpextrw	eax, xmm0, 0
  3c8a51:      	je	0x3c8a79 <parquet::internal::FindMinMaxAvx2(short const*, long)+0x169>
  3c8a53:      	movsxd	rcx, ecx
  3c8a56:      	nop	word ptr cs:[rax + rax]
  3c8a60:      	movzx	edx, word ptr [r8 + 2*rcx]
  3c8a65:      	cmp	ax, dx
  3c8a68:      	cmovg	eax, edx
  3c8a6b:      	cmp	si, dx
  3c8a6e:      	cmovl	esi, edx
  3c8a71:      	inc	rcx
  3c8a74:      	cmp	rdi, rcx
  3c8a77:      	jg	0x3c8a60 <parquet::internal::FindMinMaxAvx2(short const*, long)+0x150>
  3c8a79:      	shl	esi, 16
  3c8a7c:      	or	eax, esi
  3c8a7e:      	ret
  3c8a7f:      	nop
  3c8a80:      	mov	esi, 4294934528
  3c8a85:      	mov	eax, 32767
  3c8a8a:      	shl	esi, 16
  3c8a8d:      	or	eax, esi
  3c8a8f:      	ret
  3c8a90:      	mov	eax, 2147516416
  3c8a95:      	xor	edx, edx
  3c8a97:      	mov	esi, 4294934528
  3c8a9c:      	xor	ecx, ecx
  3c8a9e:      	vmovd	xmm1, eax
  3c8aa2:      	mov	eax, 2147450879
  3c8aa7:      	vmovd	xmm0, eax
  3c8aab:      	vpbroadcastd	xmm1, xmm1
  3c8ab0:      	mov	eax, 32767
  3c8ab5:      	vpbroadcastd	xmm0, xmm0
  3c8aba:      	jmp	0x3c89e6 <parquet::internal::FindMinMaxAvx2(short const*, long)+0xd6>
  3c8abf:      	vzeroupper
  3c8ac2:      	jmp	0x3c8a79 <parquet::internal::FindMinMaxAvx2(short const*, long)+0x169>
neon
$ objdump -d -C --no-show-raw-insn \
    --start-address=0x21130c --stop-address=0x2113d0 \
    venv/lib/python3.10/site-packages/pyarrow/libparquet.so.2500

venv/lib/python3.10/site-packages/pyarrow/libparquet.so.2500:     file format elf64-littleaarch64


Disassembly of section .text:

000000000021130c <parquet::internal::FindMinMax(short const*, long)>:
  21130c:	mov	x5, x0
  211310:	cmp	x1, #0x0
  211314:	b.le	2113b0 <parquet::internal::FindMinMax(short const*, long)+0xa4>
  211318:	sub	x0, x1, #0x1
  21131c:	cmp	x0, #0x6
  211320:	b.ls	2113c0 <parquet::internal::FindMinMax(short const*, long)+0xb4>  // b.plast
  211324:	lsr	x3, x1, #3
  211328:	mov	x2, x5
  21132c:	movi	v31.8h, #0x80, lsl #8
  211330:	mvni	v30.8h, #0x80, lsl #8
  211334:	add	x3, x5, x3, lsl #4
  211338:	nop
  21133c:	nop
  211340:	ldr	q29, [x2], #16
  211344:	smin	v30.8h, v30.8h, v29.8h
  211348:	smax	v31.8h, v31.8h, v29.8h
  21134c:	cmp	x2, x3
  211350:	b.ne	211340 <parquet::internal::FindMinMax(short const*, long)+0x34>  // b.any
  211354:	smaxv	h31, v31.8h
  211358:	and	x3, x1, #0xfffffffffffffff8
  21135c:	sminv	h30, v30.8h
  211360:	sxtw	x4, w3
  211364:	smov	w2, v31.h[0]
  211368:	smov	w0, v30.h[0]
  21136c:	cmp	x1, x3
  211370:	b.eq	2113a8 <parquet::internal::FindMinMax(short const*, long)+0x9c>  // b.none
  211374:	nop
  211378:	nop
  21137c:	nop
  211380:	ldrsh	w3, [x5, x4, lsl #1]
  211384:	add	x4, x4, #0x1
  211388:	cmp	w3, w0
  21138c:	csel	w0, w3, w0, lt  // lt = tstop
  211390:	cmp	w3, w2
  211394:	csel	w2, w3, w2, gt
  211398:	sxth	w0, w0
  21139c:	sxth	w2, w2
  2113a0:	cmp	x1, x4
  2113a4:	b.gt	211380 <parquet::internal::FindMinMax(short const*, long)+0x74>
  2113a8:	bfi	w0, w2, #16, #16
  2113ac:	ret
  2113b0:	mov	w2, #0xffff8000            	// #-32768
  2113b4:	mov	w0, #0x7fff                	// #32767
  2113b8:	bfi	w0, w2, #16, #16
  2113bc:	ret
  2113c0:	mov	x4, #0x0                   	// #0
  2113c4:	mov	w2, #0xffff8000            	// #-32768
  2113c8:	mov	w0, #0x7fff                	// #32767
  2113cc:	b	211380 <parquet::internal::FindMinMax(short const*, long)+0x74>

@domibel

domibel commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Found it. It is a regression in gcc 16. https://godbolt.org/z/T517add5W

That was very confusing. Arrows deb file 25.0.1 was using gcc 14.2 and conbench 15.3. I use gcc 16.2.

@pitrou

pitrou commented Sep 30, 2026

Copy link
Copy Markdown
Member

In any case, explicit vectorization protects us against such compiler limitations, so worth doing IMHO.

@cyb70289

Copy link
Copy Markdown
Contributor

In any case, explicit vectorization protects us against such compiler limitations, so worth doing IMHO.

Agreed. This kind of issue is very annoying. Thanks @domibel for the debug !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants