This is an automated email from the ASF dual-hosted git repository.
zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-go.git
The following commit(s) were added to refs/heads/main by this push:
new 304ac00c perf(arrow/array): SIMD-pack boolean slices into bitmaps
(#1283)
304ac00c is described below
commit 304ac00c5b2e87a7f6c62942f3ca67ba1cacb7a9
Author: Minh Vu <[email protected]>
AuthorDate: Thu Sep 3 18:50:16 2026 +0200
perf(arrow/array): SIMD-pack boolean slices into bitmaps (#1283)
## Summary
- Add private AVX2 and ARM64 NEON kernels for packing `[]bool` into
Arrow bitmap bytes.
- Keep the existing scalar handling for unaligned prefixes and tails.
- Reuse the SIMD path for BooleanBuilder values and validity bitmap
packing.
- Keep noasm and unsupported architectures on the scalar implementation.
- Expand bitmap coverage across multiple SIMD blocks and offsets.
## Benchmark
Apple M1 Pro. 65,536 pre-reserved values. Representative runs:
| Benchmark | noasm | SIMD |
| --- | ---: | ---: |
| BooleanBuilder / all false | 23.4 us | 3.9 us |
| BooleanBuilder / all true | 22.6 us | 3.9 us |
| BooleanBuilder / alternating | 21.9 us | 4.6 us |
| BooleanBuilder / one in three | 22.6 us | 4.0 us |
The SIMD path is about 4 to 5x faster for this workload, with zero
allocations in both cases.
## Tests
- `go test ./arrow/array -count=1`
- `go test -race ./arrow/array -count=1`
- `go test ./arrow/bitutil ./arrow/compute/... -count=1`
- `go test -tags noasm ./arrow/array -count=1`
- `go vet ./arrow/array ./arrow/bitutil`
- `GOARCH=amd64 go test -c -o /dev/null ./arrow/array`
---
arrow/array/builder.go | 12 ++++++
arrow/array/builder_pack_amd64.go | 47 +++++++++++++++++++++++
arrow/array/builder_pack_amd64.s | 50 +++++++++++++++++++++++++
arrow/array/builder_pack_arm64.go | 47 +++++++++++++++++++++++
arrow/array/builder_pack_arm64.s | 78 +++++++++++++++++++++++++++++++++++++++
arrow/array/builder_pack_noasm.go | 24 ++++++++++++
arrow/array/builder_test.go | 8 ++--
7 files changed, 262 insertions(+), 4 deletions(-)
diff --git a/arrow/array/builder.go b/arrow/array/builder.go
index 48113f43..40e33e1e 100644
--- a/arrow/array/builder.go
+++ b/arrow/array/builder.go
@@ -237,6 +237,14 @@ func (b *builder) unsafeAppendBoolsToBitmap(valid []bool,
length int) {
byteOffset++
}
+ packed := packBoolsSIMD(nullBitmap[byteOffset:], valid)
+ for i := 0; i < packed/8; i++ {
+ bitSet := nullBitmap[byteOffset+i]
+ b.nulls += 8 - bits.OnesCount8(bitSet)
+ }
+ valid = valid[packed:]
+ byteOffset += packed / 8
+
for len(valid) >= 8 {
bitSet := packBoolsByte(valid)
nullBitmap[byteOffset] = bitSet
@@ -312,6 +320,10 @@ func packBoolsToBitmap(dst []byte, offset int, values
[]bool) {
byteOffset++
}
+ packed := packBoolsSIMD(dst[byteOffset:], values)
+ values = values[packed:]
+ byteOffset += packed / 8
+
for len(values) >= 8 {
dst[byteOffset] = packBoolsByte(values)
values = values[8:]
diff --git a/arrow/array/builder_pack_amd64.go
b/arrow/array/builder_pack_amd64.go
new file mode 100644
index 00000000..a1660e08
--- /dev/null
+++ b/arrow/array/builder_pack_amd64.go
@@ -0,0 +1,47 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build amd64 && !noasm && !appengine
+// +build amd64,!noasm,!appengine
+
+package array
+
+import (
+ "unsafe"
+
+ "golang.org/x/sys/cpu"
+)
+
+//go:noescape
+func _packBoolsAVX2(values, dst unsafe.Pointer, length int)
+
+func packBoolsSIMD(dst []byte, values []bool) int {
+ if !cpu.X86.HasAVX2 {
+ return 0
+ }
+
+ length := len(values) &^ 31
+ if length == 0 {
+ return 0
+ }
+
+ _packBoolsAVX2(
+ unsafe.Pointer(unsafe.SliceData(values)),
+ unsafe.Pointer(unsafe.SliceData(dst)),
+ length,
+ )
+ return length
+}
diff --git a/arrow/array/builder_pack_amd64.s b/arrow/array/builder_pack_amd64.s
new file mode 100644
index 00000000..ac8b31c6
--- /dev/null
+++ b/arrow/array/builder_pack_amd64.s
@@ -0,0 +1,50 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build amd64 && !noasm && !appengine
+// +build amd64,!noasm,!appengine
+
+#include "textflag.h"
+
+// func _packBoolsAVX2(values, dst unsafe.Pointer, length int)
+TEXT ·_packBoolsAVX2(SB), NOSPLIT, $0-24
+ MOVQ values+0(FP), DI
+ MOVQ dst+8(FP), SI
+ MOVQ length+16(FP), CX
+
+ // Y1 is zero. The input is a []bool, so comparing it with zero
+ // and inverting the result produces one all-ones byte per true value.
+ VPXOR Y1, Y1, Y1
+
+loop:
+ CMPQ CX, $32
+ JB done
+
+ VMOVDQU (DI), Y0
+ VPCMPEQB Y1, Y0, Y2
+ VPCMPEQB Y1, Y1, Y3
+ VPXOR Y2, Y3, Y2
+ VPMOVMSKB Y2, DX
+ MOVL DX, (SI)
+
+ ADDQ $32, DI
+ ADDQ $4, SI
+ SUBQ $32, CX
+ JMP loop
+
+done:
+ VZEROUPPER
+ RET
diff --git a/arrow/array/builder_pack_arm64.go
b/arrow/array/builder_pack_arm64.go
new file mode 100644
index 00000000..d24ce1d3
--- /dev/null
+++ b/arrow/array/builder_pack_arm64.go
@@ -0,0 +1,47 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build arm64 && !noasm && !appengine
+// +build arm64,!noasm,!appengine
+
+package array
+
+import (
+ "unsafe"
+
+ "golang.org/x/sys/cpu"
+)
+
+//go:noescape
+func _packBoolsNEON(values, dst unsafe.Pointer, length int)
+
+func packBoolsSIMD(dst []byte, values []bool) int {
+ if !cpu.ARM64.HasASIMD {
+ return 0
+ }
+
+ length := len(values) &^ 31
+ if length == 0 {
+ return 0
+ }
+
+ _packBoolsNEON(
+ unsafe.Pointer(unsafe.SliceData(values)),
+ unsafe.Pointer(unsafe.SliceData(dst)),
+ length,
+ )
+ return length
+}
diff --git a/arrow/array/builder_pack_arm64.s b/arrow/array/builder_pack_arm64.s
new file mode 100644
index 00000000..0fa61a2c
--- /dev/null
+++ b/arrow/array/builder_pack_arm64.s
@@ -0,0 +1,78 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build arm64 && !noasm && !appengine
+// +build arm64,!noasm,!appengine
+
+// Each input byte is a Go bool (0 or 1). Masking nonzero values with the bit
+// weights turns each true value into its output bit's weight. VADDV then
+// reduces each group of eight weighted bytes to one Arrow bitmap byte.
+
+#include "textflag.h"
+
+DATA boolBitWeights<>+0(SB)/8, $0x8040201008040201
+GLOBL boolBitWeights<>(SB), (NOPTR+RODATA), $8
+
+// func _packBoolsNEON(values, dst unsafe.Pointer, length int)
+TEXT ·_packBoolsNEON(SB), NOSPLIT, $0-24
+ MOVD values+0(FP), R0
+ MOVD dst+8(FP), R1
+ MOVD length+16(FP), R2
+
+ MOVD $boolBitWeights<>(SB), R3
+ VLD1 (R3), [V4.B8]
+
+loop:
+ CMP $32, R2
+ BLT done
+
+ VLD1 (R0), [V0.B8]
+ ADD $8, R0, R8
+ VLD1 (R8), [V1.B8]
+ ADD $16, R0, R8
+ VLD1 (R8), [V2.B8]
+ ADD $24, R0, R8
+ VLD1 (R8), [V3.B8]
+
+ VCMTST V0.B8, V0.B8, V0.B8
+ VCMTST V1.B8, V1.B8, V1.B8
+ VCMTST V2.B8, V2.B8, V2.B8
+ VCMTST V3.B8, V3.B8, V3.B8
+ VAND V4.B8, V0.B8, V0.B8
+ VAND V4.B8, V1.B8, V1.B8
+ VAND V4.B8, V2.B8, V2.B8
+ VAND V4.B8, V3.B8, V3.B8
+ VADDV V0.B8, V0
+ VADDV V1.B8, V1
+ VADDV V2.B8, V2
+ VADDV V3.B8, V3
+
+ VMOV V0.B[0], R4
+ VMOV V1.B[0], R5
+ VMOV V2.B[0], R6
+ VMOV V3.B[0], R7
+ MOVB R4, (R1)
+ MOVB R5, 1(R1)
+ MOVB R6, 2(R1)
+ MOVB R7, 3(R1)
+
+ ADD $32, R0
+ ADD $4, R1
+ SUB $32, R2
+ B loop
+
+done:
+ RET
diff --git a/arrow/array/builder_pack_noasm.go
b/arrow/array/builder_pack_noasm.go
new file mode 100644
index 00000000..4b7553f8
--- /dev/null
+++ b/arrow/array/builder_pack_noasm.go
@@ -0,0 +1,24 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build noasm || appengine || (!amd64 && !arm64)
+// +build noasm appengine !amd64,!arm64
+
+package array
+
+func packBoolsSIMD(dst []byte, values []bool) int {
+ return 0
+}
diff --git a/arrow/array/builder_test.go b/arrow/array/builder_test.go
index 2424c139..6773b0aa 100644
--- a/arrow/array/builder_test.go
+++ b/arrow/array/builder_test.go
@@ -70,9 +70,9 @@ func TestBuilder_UnsafeAppendBoolsToBitmap(t *testing.T) {
for _, pattern := range patterns {
for offset := 0; offset < 8; offset++ {
- for length := 1; length <= 33; length++ {
+ for _, length := range []int{1, 2, 3, 7, 8, 15, 16, 31,
32, 33, 63, 64, 65, 127, 128, 129} {
b := &builder{mem: memory.NewGoAllocator()}
- b.init(48)
+ b.init(160)
for i := range b.nullBitmap.Bytes() {
b.nullBitmap.Bytes()[i] = byte(0x5a +
i*31)
}
@@ -126,8 +126,8 @@ func TestPackBoolsToBitmap(t *testing.T) {
for _, pattern := range patterns {
for offset := 0; offset < 8; offset++ {
- for length := 0; length <= 33; length++ {
- got := make([]byte, 8)
+ for _, length := range []int{0, 1, 2, 3, 7, 8, 15, 16,
31, 32, 33, 63, 64, 65, 127, 128, 129} {
+ got := make([]byte, 24)
for i := range got {
got[i] = byte(0x5a + i*31)
}