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 a9f71dce perf(arrow/array): skip full-range SliceEqual slices (#1249)
a9f71dce is described below
commit a9f71dce5e3a863279fe429cfd3612806d5e1258
Author: Minh Vu <[email protected]>
AuthorDate: Wed Sep 2 19:58:49 2026 +0200
perf(arrow/array): skip full-range SliceEqual slices (#1249)
## Summary
- **Skip temporary arrays when both ranges cover their full inputs.**
- Keep the existing `NewSlice` path for partial ranges.
- This also helps `ChunkedEqual` when chunk boundaries line up.
- Add full-range semantic coverage and benchmarks.
## Benchmark
Local Apple M1 Pro. `-benchtime=500ms -count=3`.
| Case | main | change |
| --- | ---: | ---: |
| `SliceEqual` int64, 64 values | ~700 ns, 352 B, 4 allocs | ~516 ns, 0
B, 0 allocs |
| `SliceEqual` string, 64 values | ~677 ns, 384 B, 4 allocs | ~474 ns, 0
B, 0 allocs |
| `ChunkedEqual`, 1,024 chunks x 64 values | ~756 us, 360 KB, 4,096
allocs | ~559 us, 0 B, 0 allocs |
## Tests
- `go test ./arrow/array`
- `go test -race ./arrow/array`
- `go vet -composites=false ./arrow/array`
- `go test ./... -run '^$'`
- `GOOS=linux GOARCH=386 go test -c -o /dev/null ./arrow/array`
---
arrow/array/compare.go | 33 +++++++++
arrow/array/compare_test.go | 64 ++++++++++++++++++
arrow/array/slice_equal_bench_test.go | 124 ++++++++++++++++++++++++++++++++++
3 files changed, 221 insertions(+)
diff --git a/arrow/array/compare.go b/arrow/array/compare.go
index 37ef0594..76e420e3 100644
--- a/arrow/array/compare.go
+++ b/arrow/array/compare.go
@@ -19,6 +19,7 @@ package array
import (
"fmt"
"math"
+ "reflect"
"github.com/apache/arrow-go/v18/arrow"
"github.com/apache/arrow-go/v18/arrow/bitutil"
@@ -354,6 +355,12 @@ func Equal(left, right arrow.Array) bool {
// SliceEqual reports whether slices left[lbeg:lend] and right[rbeg:rend] are
equal.
func SliceEqual(left arrow.Array, lbeg, lend int64, right arrow.Array, rbeg,
rend int64) bool {
+ if lbeg == 0 && lend == int64(left.Len()) &&
+ rbeg == 0 && rend == int64(right.Len()) &&
+ canEqualDirectly(left, right) {
+ return Equal(left, right)
+ }
+
l := NewSlice(left, lbeg, lend)
defer l.Release()
r := NewSlice(right, rbeg, rend)
@@ -362,6 +369,32 @@ func SliceEqual(left arrow.Array, lbeg, lend int64, right
arrow.Array, rbeg, ren
return Equal(l, r)
}
+// canEqualDirectly reports whether Equal can handle both arrays without first
+// normalizing them through NewSlice. Equal uses concrete type assertions, so
+// generic arrow.Array implementations and mismatched concrete types must keep
+// the normalization path.
+func canEqualDirectly(left, right arrow.Array) bool {
+ if reflect.TypeOf(left) != reflect.TypeOf(right) {
+ return false
+ }
+
+ switch left.(type) {
+ case *Null, *Boolean, *FixedSizeBinary, *Binary, *String,
+ *LargeBinary, *LargeString, *BinaryView, *StringView,
+ *Int8, *Int16, *Int32, *Int64, *Uint8, *Uint16, *Uint32,
*Uint64,
+ *Float16, *Float32, *Float64,
+ *Decimal32, *Decimal64, *Decimal128, *Decimal256,
+ *Date32, *Date64, *Time32, *Time64, *Timestamp,
+ *List, *LargeList, *ListView, *LargeListView, *FixedSizeList,
+ *Struct, *MonthInterval, *DayTimeInterval,
*MonthDayNanoInterval,
+ *Duration, *Map, ExtensionArray, *Dictionary, *SparseUnion,
+ *DenseUnion, *RunEndEncoded:
+ return true
+ default:
+ return false
+ }
+}
+
type listOffset interface {
int32 | int64
}
diff --git a/arrow/array/compare_test.go b/arrow/array/compare_test.go
index 4c5d0373..f7af93c9 100644
--- a/arrow/array/compare_test.go
+++ b/arrow/array/compare_test.go
@@ -85,6 +85,70 @@ func TestArraySliceEqual(t *testing.T) {
}
}
+func TestArraySliceEqualFullRange(t *testing.T) {
+ builder := array.NewInt64Builder(memory.DefaultAllocator)
+ builder.AppendValues([]int64{1, 2, 3}, nil)
+ arr := builder.NewInt64Array()
+ builder.Release()
+ defer arr.Release()
+
+ short := array.NewSlice(arr, 0, 2)
+ defer short.Release()
+ shifted := array.NewSlice(arr, 1, 3)
+ defer shifted.Release()
+
+ assert.True(t, array.SliceEqual(arr, 0, int64(arr.Len()), arr, 0,
int64(arr.Len())))
+ assert.True(t, array.SliceEqual(shifted, 0, int64(shifted.Len()),
shifted, 0, int64(shifted.Len())))
+ assert.False(t, array.SliceEqual(arr, 0, int64(arr.Len()), short, 0,
int64(short.Len())))
+}
+
+type arrayWrapper struct {
+ arrow.Array
+}
+
+func TestArraySliceEqualFullRangeGenericArray(t *testing.T) {
+ builder := array.NewInt64Builder(memory.DefaultAllocator)
+ builder.AppendValues([]int64{1, 2, 3}, nil)
+ arr := builder.NewInt64Array()
+ builder.Release()
+ defer arr.Release()
+
+ wrapped := arrayWrapper{Array: arr}
+ assert.True(t, array.SliceEqual(
+ wrapped, 0, int64(wrapped.Len()),
+ wrapped, 0, int64(wrapped.Len()),
+ ))
+ assert.True(t, array.SliceEqual(
+ arr, 0, int64(arr.Len()),
+ wrapped, 0, int64(wrapped.Len()),
+ ))
+
+ left := arrow.NewChunked(arrow.PrimitiveTypes.Int64,
[]arrow.Array{wrapped})
+ right := arrow.NewChunked(arrow.PrimitiveTypes.Int64,
[]arrow.Array{wrapped})
+ assert.True(t, array.ChunkedEqual(left, right))
+ left.Release()
+ right.Release()
+}
+
+func TestArraySliceEqualFullRangeMismatchedConcreteTypes(t *testing.T) {
+ binaryBuilder := array.NewBinaryBuilder(memory.DefaultAllocator,
arrow.BinaryTypes.String)
+ binaryBuilder.Append([]byte("value"))
+ binary := binaryBuilder.NewArray()
+ binaryBuilder.Release()
+ defer binary.Release()
+
+ stringBuilder := array.NewStringBuilder(memory.DefaultAllocator)
+ stringBuilder.Append("value")
+ str := stringBuilder.NewArray()
+ stringBuilder.Release()
+ defer str.Release()
+
+ assert.True(t, array.SliceEqual(
+ binary, 0, int64(binary.Len()),
+ str, 0, int64(str.Len()),
+ ))
+}
+
func TestListEqualByValidRuns(t *testing.T) {
for _, dt := range []arrow.DataType{
arrow.ListOf(arrow.PrimitiveTypes.Int32),
diff --git a/arrow/array/slice_equal_bench_test.go
b/arrow/array/slice_equal_bench_test.go
new file mode 100644
index 00000000..24a99012
--- /dev/null
+++ b/arrow/array/slice_equal_bench_test.go
@@ -0,0 +1,124 @@
+// 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/memory"
+)
+
+func BenchmarkSliceEqualFullRange(b *testing.B) {
+ tests := []struct {
+ name string
+ newArray func() arrow.Array
+ }{
+ {name: "int64_64", newArray: func() arrow.Array {
+ return makeSliceEqualInt64Array(64)
+ }},
+ {name: "string_64", newArray: func() arrow.Array {
+ return makeSliceEqualStringArray(64)
+ }},
+ }
+
+ for _, test := range tests {
+ arr := test.newArray()
+ b.Run(test.name, func(b *testing.B) {
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ if !array.SliceEqual(arr, 0, int64(arr.Len()),
arr, 0, int64(arr.Len())) {
+ b.Fatal("array should equal itself")
+ }
+ }
+ })
+ arr.Release()
+ }
+}
+
+func BenchmarkChunkedEqualFullChunks(b *testing.B) {
+ tests := []struct {
+ name string
+ numChunks int
+ chunkLength int
+ }{
+ {name: "64chunks_1024values", numChunks: 64, chunkLength: 1024},
+ {name: "1024chunks_64values", numChunks: 1024, chunkLength: 64},
+ }
+
+ for _, test := range tests {
+ left, right := makeSliceEqualChunkedArrays(test.numChunks,
test.chunkLength)
+ b.Run(test.name, func(b *testing.B) {
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ if !array.ChunkedEqual(left, right) {
+ b.Fatal("chunked arrays should be
equal")
+ }
+ }
+ })
+ left.Release()
+ right.Release()
+ }
+}
+
+func makeSliceEqualInt64Array(length int) arrow.Array {
+ builder := array.NewInt64Builder(memory.DefaultAllocator)
+ defer builder.Release()
+
+ values := make([]int64, length)
+ for i := range values {
+ values[i] = int64(i)
+ }
+ builder.AppendValues(values, nil)
+ return builder.NewInt64Array()
+}
+
+func makeSliceEqualStringArray(length int) arrow.Array {
+ builder := array.NewStringBuilder(memory.DefaultAllocator)
+ defer builder.Release()
+
+ values := make([]string, length)
+ for i := range values {
+ values[i] = "value"
+ }
+ builder.AppendValues(values, nil)
+ return builder.NewStringArray()
+}
+
+func makeSliceEqualChunkedArrays(numChunks, chunkLength int) (*arrow.Chunked,
*arrow.Chunked) {
+ chunks := make([]arrow.Array, numChunks)
+ values := make([]int64, chunkLength)
+ for i := 0; i < numChunks; i++ {
+ builder := array.NewInt64Builder(memory.DefaultAllocator)
+ for j := range values {
+ values[j] = int64(i*chunkLength + j)
+ }
+ builder.AppendValues(values, nil)
+ chunks[i] = builder.NewInt64Array()
+ builder.Release()
+ }
+
+ left := arrow.NewChunked(arrow.PrimitiveTypes.Int64, chunks)
+ right := arrow.NewChunked(arrow.PrimitiveTypes.Int64, chunks)
+ for _, chunk := range chunks {
+ chunk.Release()
+ }
+ return left, right
+}