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 6e4e241c perf(arrow/array): compare union values in runs (#1244)
6e4e241c is described below
commit 6e4e241c4e6c85b149632b2070d0ef55b6eb982d
Author: Minh Vu <[email protected]>
AuthorDate: Fri Aug 28 23:46:31 2026 +0200
perf(arrow/array): compare union values in runs (#1244)
## Summary
- **Batch consecutive union values with the same type code.**
- Sparse unions compare one child slice per run.
- Dense unions batch only when both child offset sequences are
contiguous.
- Apply the same path to exact and approximate equality.
- Add sliced, null, skipped-offset, mismatch, and benchmark coverage.
## Benchmark
Local Apple M1 Pro run with 65,536 rows and 64-row type runs:
- Sparse `Equal`: **14.8 ms -> 0.95 ms**, **262,150 -> 4,102 allocs/op**
- Dense `Equal`: **13.9 ms -> 1.04 ms**, **262,150 -> 4,102 allocs/op**
- Alternating type codes stay roughly flat, as expected.
## Tests
- `go test ./...`
- `go vet ./arrow/array`
- `GOOS=linux GOARCH=386 go build ./arrow/array/...`
---
arrow/array/union.go | 55 +++++++--
arrow/array/union_equal_benchmark_test.go | 184 ++++++++++++++++++++++++++++++
arrow/array/union_equal_test.go | 151 ++++++++++++++++++++++++
3 files changed, 378 insertions(+), 12 deletions(-)
diff --git a/arrow/array/union.go b/arrow/array/union.go
index 061845b9..17138f8b 100644
--- a/arrow/array/union.go
+++ b/arrow/array/union.go
@@ -451,18 +451,20 @@ func arraySparseUnionEqual(l, r *SparseUnion) bool {
childIDs := l.unionType.ChildIDs()
leftCodes, rightCodes := l.RawTypeCodes(), r.RawTypeCodes()
- for i := 0; i < l.data.length; i++ {
+ for i := 0; i < l.data.length; {
typeID := leftCodes[i]
if typeID != rightCodes[i] {
return false
}
+ end := sparseUnionRunEnd(leftCodes, rightCodes, typeID, i,
l.data.length)
childNum := childIDs[typeID]
- eq := SliceEqual(l.children[childNum], int64(i), int64(i+1),
- r.children[childNum], int64(i), int64(i+1))
+ eq := SliceEqual(l.children[childNum], int64(i), int64(end),
+ r.children[childNum], int64(i), int64(end))
if !eq {
return false
}
+ i = end
}
return true
}
@@ -471,22 +473,32 @@ func arraySparseUnionApproxEqual(l, r *SparseUnion, opt
equalOption) bool {
childIDs := l.unionType.ChildIDs()
leftCodes, rightCodes := l.RawTypeCodes(), r.RawTypeCodes()
- for i := 0; i < l.data.length; i++ {
+ for i := 0; i < l.data.length; {
typeID := leftCodes[i]
if typeID != rightCodes[i] {
return false
}
+ end := sparseUnionRunEnd(leftCodes, rightCodes, typeID, i,
l.data.length)
childNum := childIDs[typeID]
- eq := sliceApproxEqual(l.children[childNum], int64(i),
int64(i+1),
- r.children[childNum], int64(i), int64(i+1), opt)
+ eq := sliceApproxEqual(l.children[childNum], int64(i),
int64(end),
+ r.children[childNum], int64(i), int64(end), opt)
if !eq {
return false
}
+ i = end
}
return true
}
+func sparseUnionRunEnd(leftCodes, rightCodes []arrow.UnionTypeCode, typeID
arrow.UnionTypeCode, start, length int) int {
+ end := start + 1
+ for end < length && leftCodes[end] == typeID && rightCodes[end] ==
typeID {
+ end++
+ }
+ return end
+}
+
// DenseUnion represents an array where each logical value is taken from
// a single child, at a specific offset. A buffer of 8-bit type ids
// indicates which child a given logical value is to be taken from and
@@ -702,18 +714,22 @@ func arrayDenseUnionEqual(l, r *DenseUnion) bool {
leftCodes, rightCodes := l.RawTypeCodes(), r.RawTypeCodes()
leftOffsets, rightOffsets := l.RawValueOffsets(), r.RawValueOffsets()
- for i := 0; i < l.data.length; i++ {
+ for i := 0; i < l.data.length; {
typeID := leftCodes[i]
if typeID != rightCodes[i] {
return false
}
+ end := denseUnionRunEnd(leftCodes, rightCodes, leftOffsets,
rightOffsets, typeID, i, l.data.length)
childNum := childIDs[typeID]
- eq := SliceEqual(l.children[childNum], int64(leftOffsets[i]),
int64(leftOffsets[i]+1),
- r.children[childNum], int64(rightOffsets[i]),
int64(rightOffsets[i]+1))
+ leftStart, leftEnd := int64(leftOffsets[i]),
int64(leftOffsets[end-1])+1
+ rightStart, rightEnd := int64(rightOffsets[i]),
int64(rightOffsets[end-1])+1
+ eq := SliceEqual(l.children[childNum], leftStart, leftEnd,
+ r.children[childNum], rightStart, rightEnd)
if !eq {
return false
}
+ i = end
}
return true
}
@@ -723,22 +739,37 @@ func arrayDenseUnionApproxEqual(l, r *DenseUnion, opt
equalOption) bool {
leftCodes, rightCodes := l.RawTypeCodes(), r.RawTypeCodes()
leftOffsets, rightOffsets := l.RawValueOffsets(), r.RawValueOffsets()
- for i := 0; i < l.data.length; i++ {
+ for i := 0; i < l.data.length; {
typeID := leftCodes[i]
if typeID != rightCodes[i] {
return false
}
+ end := denseUnionRunEnd(leftCodes, rightCodes, leftOffsets,
rightOffsets, typeID, i, l.data.length)
childNum := childIDs[typeID]
- eq := sliceApproxEqual(l.children[childNum],
int64(leftOffsets[i]), int64(leftOffsets[i]+1),
- r.children[childNum], int64(rightOffsets[i]),
int64(rightOffsets[i]+1), opt)
+ leftStart, leftEnd := int64(leftOffsets[i]),
int64(leftOffsets[end-1])+1
+ rightStart, rightEnd := int64(rightOffsets[i]),
int64(rightOffsets[end-1])+1
+ eq := sliceApproxEqual(l.children[childNum], leftStart, leftEnd,
+ r.children[childNum], rightStart, rightEnd, opt)
if !eq {
return false
}
+ i = end
}
return true
}
+func denseUnionRunEnd(leftCodes, rightCodes []arrow.UnionTypeCode,
leftOffsets, rightOffsets []int32, typeID arrow.UnionTypeCode, start, length
int) int {
+ end := start + 1
+ for end < length &&
+ leftCodes[end] == typeID && rightCodes[end] == typeID &&
+ int64(leftOffsets[end]) == int64(leftOffsets[end-1])+1 &&
+ int64(rightOffsets[end]) == int64(rightOffsets[end-1])+1 {
+ end++
+ }
+ return end
+}
+
// UnionBuilder is a convenience interface for building Union arrays of
// either Dense or Sparse mode.
type UnionBuilder interface {
diff --git a/arrow/array/union_equal_benchmark_test.go
b/arrow/array/union_equal_benchmark_test.go
new file mode 100644
index 00000000..7d04c562
--- /dev/null
+++ b/arrow/array/union_equal_benchmark_test.go
@@ -0,0 +1,184 @@
+// 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"
+ "github.com/apache/arrow-go/v18/arrow/array"
+ "github.com/apache/arrow-go/v18/arrow/memory"
+)
+
+var benchmarkUnionEqualResult bool
+
+func BenchmarkUnionEqual(b *testing.B) {
+ const rows = 65536
+
+ for _, mode := range []string{"sparse", "dense"} {
+ for _, pattern := range []string{"one-type", "runs-64",
"alternating"} {
+ for _, mismatch := range []bool{false, true} {
+ for _, comparison := range []struct {
+ name string
+ fn func(arrow.Array, arrow.Array) bool
+ }{
+ {name: "equal", fn: array.Equal},
+ {name: "approx", fn: func(left, right
arrow.Array) bool {
+ return array.ApproxEqual(left,
right)
+ }},
+ } {
+ name := fmt.Sprintf("%s/%s/%s/%s",
mode, pattern, comparison.name, unionEqualBenchmarkMismatchName(mismatch))
+ b.Run(name, func(b *testing.B) {
+ left :=
makeUnionEqualBenchmarkArray(b, mode, pattern, rows, false)
+ right :=
makeUnionEqualBenchmarkArray(b, mode, pattern, rows, mismatch)
+ defer left.Release()
+ defer right.Release()
+
+ b.ReportAllocs()
+ b.SetBytes(int64(rows))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+
benchmarkUnionEqualResult = comparison.fn(left, right)
+ }
+ })
+ }
+ }
+ }
+ }
+}
+
+func unionEqualBenchmarkMismatchName(mismatch bool) string {
+ if mismatch {
+ return "mismatch-last"
+ }
+ return "equal"
+}
+
+func makeUnionEqualBenchmarkArray(b *testing.B, mode, pattern string, rows
int, mismatch bool) arrow.Array {
+ b.Helper()
+
+ typeIDs := make([]int8, rows)
+ offsets := make([]int32, rows)
+ sparseInts := make([]int32, rows)
+ sparseStrings := make([]string, rows)
+ denseInts := make([]int32, 0, rows)
+ denseStrings := make([]string, 0, rows)
+ childOffsets := [2]int32{}
+
+ for i := 0; i < rows; i++ {
+ childID := unionEqualBenchmarkChildID(pattern, i)
+ typeIDs[i] = int8(childID)
+
+ if mode == "sparse" {
+ sparseInts[i] = int32(i)
+ sparseStrings[i] = fmt.Sprintf("value-%d", i)
+ if mismatch && i == rows-1 {
+ if childID == 0 {
+ sparseInts[i]++
+ } else {
+ sparseStrings[i] = "different"
+ }
+ }
+ continue
+ }
+
+ offsets[i] = childOffsets[childID]
+ if childID == 0 {
+ value := int32(i)
+ if mismatch && i == rows-1 {
+ value++
+ }
+ denseInts = append(denseInts, value)
+ } else {
+ value := fmt.Sprintf("value-%d", i)
+ if mismatch && i == rows-1 {
+ value = "different"
+ }
+ denseStrings = append(denseStrings, value)
+ }
+ childOffsets[childID]++
+ }
+
+ typeIDsArray := makeUnionEqualBenchmarkInt8Array(b, typeIDs)
+ defer typeIDsArray.Release()
+ if mode == "sparse" {
+ intArray := makeUnionEqualBenchmarkInt32Array(b, sparseInts)
+ defer intArray.Release()
+ stringArray := makeUnionEqualBenchmarkStringArray(b,
sparseStrings)
+ defer stringArray.Release()
+
+ result, err := array.NewSparseUnionFromArrays(typeIDsArray,
[]arrow.Array{intArray, stringArray})
+ if err != nil {
+ b.Fatal(err)
+ }
+ return result
+ }
+
+ offsetsArray := makeUnionEqualBenchmarkInt32Array(b, offsets)
+ defer offsetsArray.Release()
+ intArray := makeUnionEqualBenchmarkInt32Array(b, denseInts)
+ defer intArray.Release()
+ stringArray := makeUnionEqualBenchmarkStringArray(b, denseStrings)
+ defer stringArray.Release()
+
+ result, err := array.NewDenseUnionFromArrays(typeIDsArray,
offsetsArray, []arrow.Array{intArray, stringArray})
+ if err != nil {
+ b.Fatal(err)
+ }
+ return result
+}
+
+func unionEqualBenchmarkChildID(pattern string, index int) int {
+ switch pattern {
+ case "one-type":
+ return 0
+ case "runs-64":
+ return (index / 64) % 2
+ case "alternating":
+ return index % 2
+ default:
+ panic("unsupported union equality benchmark pattern")
+ }
+}
+
+func makeUnionEqualBenchmarkInt8Array(b *testing.B, values []int8) arrow.Array
{
+ b.Helper()
+ builder := array.NewInt8Builder(memory.DefaultAllocator)
+ builder.AppendValues(values, nil)
+ result := builder.NewInt8Array()
+ builder.Release()
+ return result
+}
+
+func makeUnionEqualBenchmarkInt32Array(b *testing.B, values []int32)
arrow.Array {
+ b.Helper()
+ builder := array.NewInt32Builder(memory.DefaultAllocator)
+ builder.AppendValues(values, nil)
+ result := builder.NewInt32Array()
+ builder.Release()
+ return result
+}
+
+func makeUnionEqualBenchmarkStringArray(b *testing.B, values []string)
arrow.Array {
+ b.Helper()
+ builder := array.NewStringBuilder(memory.DefaultAllocator)
+ builder.AppendValues(values, nil)
+ result := builder.NewStringArray()
+ builder.Release()
+ return result
+}
diff --git a/arrow/array/union_equal_test.go b/arrow/array/union_equal_test.go
new file mode 100644
index 00000000..e997b970
--- /dev/null
+++ b/arrow/array/union_equal_test.go
@@ -0,0 +1,151 @@
+// 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 (
+ "strings"
+ "testing"
+
+ "github.com/apache/arrow-go/v18/arrow"
+ "github.com/apache/arrow-go/v18/arrow/array"
+ "github.com/apache/arrow-go/v18/arrow/memory"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSparseUnionEqualRuns(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+ defer mem.AssertSize(t, 0)
+
+ left := newSparseUnionEqualTestArray(t, mem,
+ `[0, 0, 0, 1, 1, 0, 0, 1]`,
+ `[10, null, 12, 13, 14, 15, 16, 17]`,
+ `["a", "b", "c", "d", "e", "f", "g", "h"]`)
+ defer left.Release()
+ right := newSparseUnionEqualTestArray(t, mem,
+ `[0, 0, 0, 1, 1, 0, 0, 1]`,
+ `[10, null, 12, 13, 14, 15, 16, 17]`,
+ `["a", "b", "c", "d", "e", "f", "g", "h"]`)
+ defer right.Release()
+
+ assert.True(t, array.Equal(left, right))
+ assert.True(t, array.ApproxEqual(left, right))
+
+ leftSlice := array.NewSlice(left, 1, 7)
+ defer leftSlice.Release()
+ rightSlice := array.NewSlice(right, 1, 7)
+ defer rightSlice.Release()
+ assert.True(t, array.Equal(leftSlice, rightSlice))
+ assert.True(t, array.ApproxEqual(leftSlice, rightSlice))
+
+ different := newSparseUnionEqualTestArray(t, mem,
+ `[0, 0, 0, 1, 1, 0, 0, 1]`,
+ `[10, 99, 12, 13, 14, 15, 16, 17]`,
+ `["a", "b", "c", "d", "e", "f", "g", "h"]`)
+ defer different.Release()
+ assert.False(t, array.Equal(left, different))
+ assert.False(t, array.ApproxEqual(left, different))
+}
+
+func TestDenseUnionEqualRuns(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+ defer mem.AssertSize(t, 0)
+
+ left := newDenseUnionEqualTestArray(t, mem,
+ `[0, 0, 0, 1, 1, 0]`,
+ `[0, 2, 3, 0, 1, 4]`,
+ `[10, 99, 20, null, 40]`,
+ `["a", "b"]`)
+ defer left.Release()
+ right := newDenseUnionEqualTestArray(t, mem,
+ `[0, 0, 0, 1, 1, 0]`,
+ `[0, 2, 3, 0, 1, 4]`,
+ `[10, 88, 20, null, 40]`,
+ `["a", "b"]`)
+ defer right.Release()
+
+ assert.True(t, array.Equal(left, right))
+ assert.True(t, array.ApproxEqual(left, right))
+
+ leftSlice := array.NewSlice(left, 1, 5)
+ defer leftSlice.Release()
+ rightSlice := array.NewSlice(right, 1, 5)
+ defer rightSlice.Release()
+ assert.True(t, array.Equal(leftSlice, rightSlice))
+ assert.True(t, array.ApproxEqual(leftSlice, rightSlice))
+
+ leftWithDifferentOffsets := newDenseUnionEqualTestArray(t, mem,
+ `[0, 0, 0, 0]`,
+ `[0, 1, 2, 3]`,
+ `[10, 20, 30, 40]`,
+ `[]`)
+ defer leftWithDifferentOffsets.Release()
+ rightWithDifferentOffsets := newDenseUnionEqualTestArray(t, mem,
+ `[0, 0, 0, 0]`,
+ `[1, 3, 4, 5]`,
+ `[99, 10, 88, 20, 30, 40]`,
+ `[]`)
+ defer rightWithDifferentOffsets.Release()
+ assert.True(t, array.Equal(leftWithDifferentOffsets,
rightWithDifferentOffsets))
+ assert.True(t, array.ApproxEqual(leftWithDifferentOffsets,
rightWithDifferentOffsets))
+
+ different := newDenseUnionEqualTestArray(t, mem,
+ `[0, 0, 0, 1, 1, 0]`,
+ `[0, 2, 3, 0, 1, 4]`,
+ `[10, 88, 21, null, 40]`,
+ `["a", "b"]`)
+ defer different.Release()
+ assert.False(t, array.Equal(left, different))
+ assert.False(t, array.ApproxEqual(left, different))
+}
+
+func newSparseUnionEqualTestArray(t *testing.T, mem memory.Allocator, typeIDs,
ints, stringsJSON string) *array.SparseUnion {
+ t.Helper()
+ typeIDsArray := newUnionEqualTestArray(t, mem,
arrow.PrimitiveTypes.Int8, typeIDs)
+ defer typeIDsArray.Release()
+ intArray := newUnionEqualTestArray(t, mem, arrow.PrimitiveTypes.Int32,
ints)
+ defer intArray.Release()
+ stringArray := newUnionEqualTestArray(t, mem, arrow.BinaryTypes.String,
stringsJSON)
+ defer stringArray.Release()
+
+ result, err := array.NewSparseUnionFromArrays(typeIDsArray,
[]arrow.Array{intArray, stringArray})
+ require.NoError(t, err)
+ return result
+}
+
+func newDenseUnionEqualTestArray(t *testing.T, mem memory.Allocator, typeIDs,
offsets, ints, stringsJSON string) *array.DenseUnion {
+ t.Helper()
+ typeIDsArray := newUnionEqualTestArray(t, mem,
arrow.PrimitiveTypes.Int8, typeIDs)
+ defer typeIDsArray.Release()
+ offsetsArray := newUnionEqualTestArray(t, mem,
arrow.PrimitiveTypes.Int32, offsets)
+ defer offsetsArray.Release()
+ intArray := newUnionEqualTestArray(t, mem, arrow.PrimitiveTypes.Int32,
ints)
+ defer intArray.Release()
+ stringArray := newUnionEqualTestArray(t, mem, arrow.BinaryTypes.String,
stringsJSON)
+ defer stringArray.Release()
+
+ result, err := array.NewDenseUnionFromArrays(typeIDsArray,
offsetsArray, []arrow.Array{intArray, stringArray})
+ require.NoError(t, err)
+ return result
+}
+
+func newUnionEqualTestArray(t *testing.T, mem memory.Allocator, dtype
arrow.DataType, values string) arrow.Array {
+ t.Helper()
+ result, _, err := array.FromJSON(mem, dtype, strings.NewReader(values))
+ require.NoError(t, err)
+ return result
+}