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 802431b7 perf(arrow/array): compare fixed-size binary values in bulk
(#1347)
802431b7 is described below
commit 802431b7521df5465c684bdc60abd921cf92f2fe
Author: Minh Vu <[email protected]>
AuthorDate: Tue Oct 6 16:24:58 2026 +0200
perf(arrow/array): compare fixed-size binary values in bulk (#1347)
### What
- Compare valid fixed-size binary values in contiguous byte ranges.
- Keep scalar comparison for short or fragmented arrays.
### Why
- Reduce per-value checks for larger arrays.
### Implementation
- Add null, offset, and validity bitmap cases.
- Benchmark 8/32/128-byte values at 4/8/64/1K/64K items with all-valid,
1%-null, alternating-null, and clustered-null patterns.
### Benchmark results
- Compared with `upstream/main` (`954bc067`) on an Apple M1 Pro with Go
1.26.3; median of 5 runs.
- At 65,536 values, all-valid was 1.4–16.1x faster, 1%-null 1.3–12.5x,
and clustered-null 2.1–13.8x.
- Alternating-null results were within 7% of baseline.
---
arrow/array/binary.go | 4 +
arrow/array/fixedsize_binary.go | 45 +++
.../fixedsize_binary_equality_benchmark_test.go | 131 ++++++++
arrow/array/fixedsize_binary_equality_test.go | 345 +++++++++++++++++++++
4 files changed, 525 insertions(+)
diff --git a/arrow/array/binary.go b/arrow/array/binary.go
index c2fde2af..1095d090 100644
--- a/arrow/array/binary.go
+++ b/arrow/array/binary.go
@@ -465,6 +465,10 @@ func useScalarVariableWidthEquality(values arrow.Array)
bool {
if values.NullN() == 0 {
return false
}
+ return useScalarEqualityForValidityRuns(values)
+}
+
+func useScalarEqualityForValidityRuns(values arrow.Array) bool {
if values.Len() <= 64 || len(values.NullBitmapBytes()) == 0 {
return true
}
diff --git a/arrow/array/fixedsize_binary.go b/arrow/array/fixedsize_binary.go
index 567bcba0..b14846b0 100644
--- a/arrow/array/fixedsize_binary.go
+++ b/arrow/array/fixedsize_binary.go
@@ -23,6 +23,8 @@ import (
"strings"
"github.com/apache/arrow-go/v18/arrow"
+ "github.com/apache/arrow-go/v18/arrow/bitutil"
+ "github.com/apache/arrow-go/v18/internal/bitutils"
"github.com/apache/arrow-go/v18/internal/json"
)
@@ -117,6 +119,49 @@ func (a *FixedSizeBinary) MarshalJSON() ([]byte, error) {
}
func arrayEqualFixedSizeBinary(left, right *FixedSizeBinary) bool {
+ if left.Len() < 8 {
+ return arrayEqualFixedSizeBinaryScalar(left, right)
+ }
+
+ width := int(left.bytewidth)
+ leftStart := left.Offset() * width
+ leftEnd := leftStart + left.Len()*width
+ rightStart := right.Offset() * width
+ rightEnd := rightStart + right.Len()*width
+ leftBitmap := left.NullBitmapBytes()
+ if len(leftBitmap) == 0 ||
+ (left.NullN() == 0 && bitutil.CountSetBits(leftBitmap,
left.Offset(), left.Len()) == left.Len()) {
+ return bytes.Equal(left.valueBytes[leftStart:leftEnd],
right.valueBytes[rightStart:rightEnd])
+ }
+ if useScalarEqualityForValidityRuns(left) {
+ return arrayEqualFixedSizeBinaryScalar(left, right)
+ }
+
+ runs := bitutils.NewSetBitRunReader(
+ leftBitmap, int64(left.Offset()), int64(left.Len()),
+ )
+ for {
+ run := runs.NextRun()
+ if run.Length == 0 {
+ return true
+ }
+
+ start := int(run.Pos)
+ end := start + int(run.Length)
+ leftRunStart := (left.Offset() + start) * width
+ leftRunEnd := (left.Offset() + end) * width
+ rightRunStart := (right.Offset() + start) * width
+ rightRunEnd := (right.Offset() + end) * width
+ if !bytes.Equal(
+ left.valueBytes[leftRunStart:leftRunEnd],
+ right.valueBytes[rightRunStart:rightRunEnd],
+ ) {
+ return false
+ }
+ }
+}
+
+func arrayEqualFixedSizeBinaryScalar(left, right *FixedSizeBinary) bool {
for i := 0; i < left.Len(); i++ {
if left.IsNull(i) {
continue
diff --git a/arrow/array/fixedsize_binary_equality_benchmark_test.go
b/arrow/array/fixedsize_binary_equality_benchmark_test.go
new file mode 100644
index 00000000..e9e22f81
--- /dev/null
+++ b/arrow/array/fixedsize_binary_equality_benchmark_test.go
@@ -0,0 +1,131 @@
+// 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.
+
+package array_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/apache/arrow-go/v18/arrow/array"
+)
+
+var fixedSizeBinaryEqualityResult bool
+
+func BenchmarkFixedSizeBinaryEquality(b *testing.B) {
+ const length = 64 * 1024
+ for _, width := range []int{8, 32, 128} {
+ for _, size := range []int{4, 8, 64, 1024, length} {
+ for _, pattern := range []struct {
+ name string
+ valid func(int) []bool
+ }{
+ {name: "all_valid"},
+ {name: "materialized_all_valid", valid: func(n
int) []bool {
+ valid := make([]bool, n)
+ for i := range valid {
+ valid[i] = true
+ }
+ return valid
+ }},
+ {name: "one_percent_null", valid: func(n int)
[]bool {
+ valid := make([]bool, n)
+ for i := range valid {
+ valid[i] = i%100 != 0
+ }
+ return valid
+ }},
+ {name: "alternating_null", valid: func(n int)
[]bool {
+ valid := make([]bool, n)
+ for i := range valid {
+ valid[i] = i%2 == 0
+ }
+ return valid
+ }},
+ {name: "clustered_null", valid: func(n int)
[]bool {
+ valid := make([]bool, n)
+ for i := range valid {
+ valid[i] = i >= n/2
+ }
+ return valid
+ }},
+ } {
+ b.Run(fmt.Sprintf("width_%d/length_%d/%s",
width, size, pattern.name), func(b *testing.B) {
+ leftValues := make([]byte, size*width)
+ for i := range leftValues {
+ leftValues[i] = byte(i*31 +
i/width*17)
+ }
+ rightValues := append([]byte(nil),
leftValues...)
+ var valid []bool
+ if pattern.valid != nil {
+ valid = pattern.valid(size)
+ }
+ nulls := 0
+ for i, isValid := range valid {
+ if !isValid {
+ nulls++
+ rightValues[i*width]++
+ }
+ }
+
+ left :=
makeFixedSizeBinaryEqualityArray(width, leftValues, valid, size, 0, nulls)
+ right :=
makeFixedSizeBinaryEqualityArray(width, rightValues, valid, size, 0, nulls)
+ b.Cleanup(left.Release)
+ b.Cleanup(right.Release)
+
+ b.ReportAllocs()
+ b.SetBytes(int64(size * width))
+ for b.Loop() {
+ fixedSizeBinaryEqualityResult =
array.Equal(left, right)
+ }
+ })
+ }
+ }
+ }
+}
+
+func BenchmarkFixedSizeBinaryEqualityMismatch(b *testing.B) {
+ const size = 64 * 1024
+ for _, width := range []int{8, 32, 128} {
+ for _, mismatch := range []struct {
+ name string
+ row int
+ }{
+ {name: "first", row: 0},
+ {name: "last", row: size - 1},
+ } {
+ b.Run(fmt.Sprintf("width_%d/%s", width, mismatch.name),
func(b *testing.B) {
+ leftValues := make([]byte, size*width)
+ for i := range leftValues {
+ leftValues[i] = byte(i*31 + i/width*17)
+ }
+ rightValues := append([]byte(nil),
leftValues...)
+ rightValues[mismatch.row*width]++
+
+ left := makeFixedSizeBinaryEqualityArray(width,
leftValues, nil, size, 0, 0)
+ right :=
makeFixedSizeBinaryEqualityArray(width, rightValues, nil, size, 0, 0)
+ b.Cleanup(left.Release)
+ b.Cleanup(right.Release)
+
+ b.ReportAllocs()
+ b.SetBytes(int64(size * width))
+ for b.Loop() {
+ fixedSizeBinaryEqualityResult =
array.Equal(left, right)
+ }
+ })
+ }
+ }
+}
diff --git a/arrow/array/fixedsize_binary_equality_test.go
b/arrow/array/fixedsize_binary_equality_test.go
new file mode 100644
index 00000000..3c06a2e4
--- /dev/null
+++ b/arrow/array/fixedsize_binary_equality_test.go
@@ -0,0 +1,345 @@
+// 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.
+
+package array_test
+
+import (
+ "testing"
+
+ "github.com/apache/arrow-go/v18/arrow"
+ "github.com/apache/arrow-go/v18/arrow/array"
+ "github.com/apache/arrow-go/v18/arrow/bitutil"
+ "github.com/apache/arrow-go/v18/arrow/memory"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestFixedSizeBinaryEqualityByValidRuns(t *testing.T) {
+ valid := []bool{true, true, false, true, false, false, true, true}
+ leftNulls := []byte("aaaabbbbLEFTddddLEFTLEFTgggghhhh")
+ rightNulls := []byte("aaaabbbbxxxxddddyyyyzzzzgggghhhh")
+
+ leftOffsetValues := concatFixedSizeBinaryRows("LPRF", "one1", "left",
"two2", "thre", "LSUF")
+ rightOffsetValues := concatFixedSizeBinaryRows("RPRF", "free", "one1",
"diff", "two2", "thre", "RSUF")
+ leftOffsetValid := []bool{true, true, false, true, true, true}
+ rightOffsetValid := []bool{true, true, true, false, true, true, true}
+
+ allValid := make([]bool, 8)
+ for i := range allValid {
+ allValid[i] = true
+ }
+
+ tests := []struct {
+ name string
+ leftValues, rightValues []byte
+ leftValid, rightValid []bool
+ length int
+ leftOffset, rightOffset int
+ leftNulls, rightNulls int
+ want bool
+ }{
+ {
+ name: "equal values",
+ leftValues: concatFixedSizeBinaryRows("aaaa", "bbbb",
"cccc", "dddd", "eeee", "ffff", "gggg", "hhhh"),
+ rightValues: concatFixedSizeBinaryRows("aaaa", "bbbb",
"cccc", "dddd", "eeee", "ffff", "gggg", "hhhh"),
+ length: 8,
+ want: true,
+ },
+ {
+ name: "ignore bytes under null slots",
+ leftValues: leftNulls,
+ rightValues: rightNulls,
+ leftValid: valid,
+ rightValid: valid,
+ length: len(valid),
+ leftNulls: 3,
+ rightNulls: 3,
+ want: true,
+ },
+ {
+ name: "detect mismatch in a later valid run",
+ leftValues: concatFixedSizeBinaryRows("aaaa", "bbbb",
"cccc", "dddd", "eeee", "ffff", "gggg", "hhhh"),
+ rightValues: concatFixedSizeBinaryRows("aaaa", "bbbb",
"cccc", "dddd", "eeee", "ffff", "GGGG", "hhhh"),
+ leftValid: valid,
+ rightValid: valid,
+ length: 8,
+ leftNulls: 3,
+ rightNulls: 3,
+ want: false,
+ },
+ {
+ name: "different physical offsets with nulls",
+ leftValues: leftOffsetValues,
+ rightValues: rightOffsetValues,
+ leftValid: leftOffsetValid,
+ rightValid: rightOffsetValid,
+ length: 4,
+ leftOffset: 1,
+ rightOffset: 2,
+ leftNulls: 1,
+ rightNulls: 1,
+ want: true,
+ },
+ {
+ name: "materialized bitmap remains authoritative
with zero declared nulls",
+ leftValues: concatFixedSizeBinaryRows("aaaa", "LEFT",
"cccc", "dddd", "eeee", "ffff", "gggg", "hhhh"),
+ rightValues: concatFixedSizeBinaryRows("aaaa", "DIFF",
"cccc", "dddd", "eeee", "ffff", "gggg", "hhhh"),
+ leftValid: []bool{true, false, true, true, true,
true, true, true},
+ rightValid: []bool{true, false, true, true, true,
true, true, true},
+ length: 8,
+ want: true,
+ },
+ {
+ name: "absent and all-valid materialized
bitmaps",
+ leftValues: concatFixedSizeBinaryRows("aaaa", "bbbb",
"cccc", "dddd", "eeee", "ffff", "gggg", "hhhh"),
+ rightValues: concatFixedSizeBinaryRows("aaaa", "bbbb",
"cccc", "dddd", "eeee", "ffff", "gggg", "hhhh"),
+ rightValid: allValid,
+ length: 8,
+ want: true,
+ },
+ {
+ name: "both materialized all-valid bitmaps",
+ leftValues: concatFixedSizeBinaryRows("aaaa", "bbbb",
"cccc", "dddd", "eeee", "ffff", "gggg", "hhhh"),
+ rightValues: concatFixedSizeBinaryRows("aaaa", "bbbb",
"cccc", "dddd", "eeee", "ffff", "gggg", "hhhh"),
+ leftValid: allValid,
+ rightValid: allValid,
+ length: 8,
+ want: true,
+ },
+ {
+ name: "different validity positions with the
same null count",
+ leftValues: concatFixedSizeBinaryRows("aaaa", "bbbb",
"cccc", "dddd", "eeee", "ffff", "gggg", "hhhh"),
+ rightValues: concatFixedSizeBinaryRows("aaaa", "bbbb",
"cccc", "dddd", "eeee", "ffff", "gggg", "hhhh"),
+ leftValid: []bool{true, false, true, true, true,
true, true, true},
+ rightValid: []bool{true, true, false, true, true,
true, true, true},
+ length: 8,
+ leftNulls: 1,
+ rightNulls: 1,
+ want: false,
+ },
+ {
+ name: "all-null payloads are ignored",
+ leftValues: concatFixedSizeBinaryRows("LEFT", "LEFT",
"LEFT", "LEFT", "LEFT", "LEFT", "LEFT", "LEFT"),
+ rightValues: concatFixedSizeBinaryRows("DIFF", "DIFF",
"DIFF", "DIFF", "DIFF", "DIFF", "DIFF", "DIFF"),
+ leftValid: []bool{false, false, false, false, false,
false, false, false},
+ rightValid: []bool{false, false, false, false, false,
false, false, false},
+ length: 8,
+ leftNulls: 8,
+ rightNulls: 8,
+ want: true,
+ },
+ {
+ name: "empty arrays without buffers",
+ length: 0,
+ want: true,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ left := makeFixedSizeBinaryEqualityArray(4,
tc.leftValues, tc.leftValid, tc.length, tc.leftOffset, tc.leftNulls)
+ defer left.Release()
+ right := makeFixedSizeBinaryEqualityArray(4,
tc.rightValues, tc.rightValid, tc.length, tc.rightOffset, tc.rightNulls)
+ defer right.Release()
+
+ assert.Equal(t, tc.want, array.Equal(left, right))
+ assert.Equal(t, tc.want, array.Equal(right, left))
+ })
+ }
+}
+
+func TestFixedSizeBinaryEqualityLongValidRuns(t *testing.T) {
+ const (
+ width = 4
+ length = 256
+ )
+
+ valid := make([]bool, length)
+ leftValues := make([]byte, length*width)
+ rightValues := make([]byte, length*width)
+ nulls := 0
+ for i := range length {
+ valid[i] = (i/8)%2 == 0
+ if !valid[i] {
+ nulls++
+ }
+ for j := range width {
+ value := byte(i*7 + j*11)
+ leftValues[i*width+j] = value
+ rightValues[i*width+j] = value
+ }
+ if !valid[i] {
+ rightValues[i*width]++
+ }
+ }
+
+ left := makeFixedSizeBinaryEqualityArray(width, leftValues, valid,
length, 0, nulls)
+ defer left.Release()
+ right := makeFixedSizeBinaryEqualityArray(width, rightValues, valid,
length, 0, nulls)
+ assert.True(t, array.Equal(left, right))
+ right.Release()
+
+ rightValues[240*width]++
+ right = makeFixedSizeBinaryEqualityArray(width, rightValues, valid,
length, 0, nulls)
+ defer right.Release()
+ assert.False(t, array.Equal(left, right))
+}
+
+func TestFixedSizeBinaryEqualityWithZeroDeclaredNullCount(t *testing.T) {
+ const (
+ width = 4
+ length = 128
+ )
+
+ valid := make([]bool, length)
+ leftValues := make([]byte, length*width)
+ rightValues := make([]byte, length*width)
+ for i := range length {
+ valid[i] = (i/8)%2 == 0
+ for j := range width {
+ value := byte(i*7 + j*11)
+ leftValues[i*width+j] = value
+ rightValues[i*width+j] = value
+ }
+ if !valid[i] {
+ rightValues[i*width]++
+ }
+ }
+
+ left := makeFixedSizeBinaryEqualityArray(width, leftValues, valid,
length, 0, 0)
+ defer left.Release()
+ right := makeFixedSizeBinaryEqualityArray(width, rightValues, valid,
length, 0, 0)
+ defer right.Release()
+
+ assert.True(t, array.Equal(left, right))
+}
+
+func TestFixedSizeBinaryEqualityWithDifferentLongOffsets(t *testing.T) {
+ const (
+ width = 4
+ length = 128
+ leftOffset = 1
+ rightOffset = 3
+ )
+
+ leftValues := make([]byte, (leftOffset+length)*width)
+ rightValues := make([]byte, (rightOffset+length)*width)
+ leftValid := make([]bool, leftOffset+length)
+ rightValid := make([]bool, rightOffset+length)
+ nulls := 0
+ for i := range length {
+ isValid := (i/8)%2 == 0
+ leftValid[leftOffset+i] = isValid
+ rightValid[rightOffset+i] = isValid
+ if !isValid {
+ nulls++
+ }
+ for j := range width {
+ value := byte(i*7 + j*11)
+ leftValues[(leftOffset+i)*width+j] = value
+ rightValues[(rightOffset+i)*width+j] = value
+ }
+ if !isValid {
+ rightValues[(rightOffset+i)*width]++
+ }
+ }
+
+ left := makeFixedSizeBinaryEqualityArray(width, leftValues, leftValid,
length, leftOffset, nulls)
+ defer left.Release()
+ right := makeFixedSizeBinaryEqualityArray(width, rightValues,
rightValid, length, rightOffset, nulls)
+ assert.True(t, array.Equal(left, right))
+ right.Release()
+
+ rightValues[(rightOffset+112)*width]++
+ right = makeFixedSizeBinaryEqualityArray(width, rightValues,
rightValid, length, rightOffset, nulls)
+ defer right.Release()
+ assert.False(t, array.Equal(left, right))
+}
+
+func TestFixedSizeBinaryEqualityWithFragmentedValidity(t *testing.T) {
+ const (
+ width = 4
+ length = 128
+ )
+
+ valid := make([]bool, length)
+ leftValues := make([]byte, length*width)
+ rightValues := make([]byte, length*width)
+ nulls := 0
+ for i := range length {
+ valid[i] = i%2 == 0
+ if !valid[i] {
+ nulls++
+ }
+ for j := range width {
+ value := byte(i*7 + j*11)
+ leftValues[i*width+j] = value
+ rightValues[i*width+j] = value
+ }
+ if !valid[i] {
+ rightValues[i*width]++
+ }
+ }
+
+ left := makeFixedSizeBinaryEqualityArray(width, leftValues, valid,
length, 0, nulls)
+ defer left.Release()
+ right := makeFixedSizeBinaryEqualityArray(width, rightValues, valid,
length, 0, nulls)
+ assert.True(t, array.Equal(left, right))
+ right.Release()
+
+ rightValues[126*width]++
+ right = makeFixedSizeBinaryEqualityArray(width, rightValues, valid,
length, 0, nulls)
+ defer right.Release()
+ assert.False(t, array.Equal(left, right))
+}
+
+func makeFixedSizeBinaryEqualityArray(
+ width int, values []byte, valid []bool, length, offset, nulls int,
+) *array.FixedSizeBinary {
+ var validityBuffer *memory.Buffer
+ if valid != nil {
+ validity := make([]byte, (len(valid)+7)/8)
+ for i, isValid := range valid {
+ if isValid {
+ bitutil.SetBit(validity, i)
+ }
+ }
+ validityBuffer = memory.NewBufferBytes(validity)
+ defer validityBuffer.Release()
+ }
+
+ valuesBuffer := memory.NewBufferBytes(values)
+ defer valuesBuffer.Release()
+
+ data := array.NewData(
+ &arrow.FixedSizeBinaryType{ByteWidth: width},
+ length,
+ []*memory.Buffer{validityBuffer, valuesBuffer},
+ nil,
+ nulls,
+ offset,
+ )
+ defer data.Release()
+
+ return array.NewFixedSizeBinaryData(data)
+}
+
+func concatFixedSizeBinaryRows(rows ...string) []byte {
+ var values []byte
+ for _, row := range rows {
+ values = append(values, row...)
+ }
+ return values
+}