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 9296b3fa perf(arrow): reduce metadata sorting overhead (#1361)
9296b3fa is described below

commit 9296b3fac5a1daf2ce1f8a124a5227449fc1bdcb
Author: Minh Vu <[email protected]>
AuthorDate: Tue Oct 13 16:30:29 2026 +0200

    perf(arrow): reduce metadata sorting overhead (#1361)
    
    **What**
    - Reduce metadata equality overhead for small metadata.
    
    **Why**
    - Small sorts spend time calling the comparator.
    
    **Implementation**
    - Use direct insertion sort for up to 12 pairs.
    - Keep the current sorter for larger metadata.
    - Keep small-sort locals out of the general sorting frame.
    
    Go 1.26.3, Apple M1 Pro, 6 interleaved samples of 100ms. Compared with
    `c1c8ed0a`.
    
    | Case | Base ns/op | PR ns/op | Change |
    | --- | ---: | ---: | ---: |
    | 8 pairs, matching | 123.6 | 112.9 | -8.62% |
    | 8 pairs, reversed | 233.9 | 192.8 | -17.57% |
    | 12 pairs, reversed | 460.5 | 375.1 | -18.53% |
    | 128 pairs, middle swap | 2,794 | 2,823 | ~ |
    | 2,049 pairs, middle swap | 40,600 | 40,680 | ~ |
    
    `~` means no significant change. Allocations are unchanged.
---
 arrow/metadata_equal_benchmark_test.go | 128 ++++++++++++++++++++++
 arrow/metadata_equal_test.go           | 191 +++++++++++++++++++++++++++++++++
 arrow/schema.go                        |  36 +++++++
 3 files changed, 355 insertions(+)

diff --git a/arrow/metadata_equal_benchmark_test.go 
b/arrow/metadata_equal_benchmark_test.go
new file mode 100644
index 00000000..0e790225
--- /dev/null
+++ b/arrow/metadata_equal_benchmark_test.go
@@ -0,0 +1,128 @@
+// 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 arrow
+
+import (
+       "fmt"
+       "slices"
+       "strings"
+       "testing"
+)
+
+var metadataEqualBenchmarkResult bool
+
+func metadataEqualityBenchmarkPair(size int, pattern string) (Metadata, 
Metadata) {
+       keys, values := make([]string, size), make([]string, size)
+       for i := range keys {
+               keys[i] = fmt.Sprintf("key_%04d", i)
+               values[i] = fmt.Sprintf("value_%04d", i)
+       }
+       left := NewMetadata(keys, values)
+       right := left.clone()
+       if strings.HasPrefix(pattern, "duplicate-") {
+               for i := range left.keys {
+                       left.keys[i] = fmt.Sprintf("key_%04d", i/2)
+                       left.values[i] = fmt.Sprintf("value_%04d", i/2)
+                       right.keys[i], right.values[i] = left.keys[i], 
left.values[i]
+               }
+               pattern = strings.TrimPrefix(pattern, "duplicate-")
+       }
+       switch pattern {
+       case "matching-reversed":
+               slices.Reverse(left.keys)
+               slices.Reverse(left.values)
+               slices.Reverse(right.keys)
+               slices.Reverse(right.values)
+       case "different-value":
+               right.values[size-1] = "different"
+       case "different-key":
+               right.keys[size/2] = "different"
+       case "reversed":
+               slices.Reverse(right.keys)
+               slices.Reverse(right.values)
+       case "middle-swap":
+               pos := size/2 - 1
+               right.keys[pos], right.keys[pos+1] = right.keys[pos+1], 
right.keys[pos]
+               right.values[pos], right.values[pos+1] = right.values[pos+1], 
right.values[pos]
+       }
+       return left, right
+}
+
+func BenchmarkMetadataEqual(b *testing.B) {
+       for _, size := range []int{0, 1, 8, 12, 13, 128} {
+               for _, pattern := range []string{"matching", 
"matching-reversed", "different-value", "reversed", "different-key"} {
+                       if size == 0 && pattern != "matching" || size == 1 && 
(pattern == "matching-reversed" || pattern == "reversed") {
+                               continue
+                       }
+                       b.Run(fmt.Sprintf("size=%d/%s", size, pattern), func(b 
*testing.B) {
+                               left, right := 
metadataEqualityBenchmarkPair(size, pattern)
+                               if got := left.Equal(right); got != (pattern != 
"different-value" && pattern != "different-key") {
+                                       b.Fatalf("unexpected equality: %v", got)
+                               }
+                               b.ReportAllocs()
+                               for b.Loop() {
+                                       metadataEqualBenchmarkResult = 
left.Equal(right)
+                               }
+                       })
+               }
+       }
+}
+
+func BenchmarkMetadataEqualDuplicateKeys(b *testing.B) {
+       for _, size := range []int{8, 12, 13, 128} {
+               for _, pattern := range []string{"matching", "reversed", 
"middle-swap"} {
+                       b.Run(fmt.Sprintf("size=%d/%s", size, pattern), func(b 
*testing.B) {
+                               left, right := 
metadataEqualityBenchmarkPair(size, "duplicate-"+pattern)
+                               if !left.Equal(right) {
+                                       b.Fatal("metadata should be equal")
+                               }
+                               b.ReportAllocs()
+                               for b.Loop() {
+                                       metadataEqualBenchmarkResult = 
left.Equal(right)
+                               }
+                       })
+               }
+       }
+}
+
+func BenchmarkSchemaEqualFieldMetadata(b *testing.B) {
+       const nfields = 16
+       for _, size := range []int{0, 1, 8, 32} {
+               for _, pattern := range []string{"matching", "reversed"} {
+                       if size < 2 && pattern != "matching" {
+                               continue
+                       }
+                       b.Run(fmt.Sprintf("size=%d/%s", size, pattern), func(b 
*testing.B) {
+                               leftMetadata, rightMetadata := 
metadataEqualityBenchmarkPair(size, pattern)
+                               leftFields, rightFields := make([]Field, 
nfields), make([]Field, nfields)
+                               for i := range leftFields {
+                                       name := fmt.Sprintf("field_%d", i)
+                                       leftFields[i] = Field{Name: name, Type: 
PrimitiveTypes.Int64, Metadata: leftMetadata}
+                                       rightFields[i] = Field{Name: name, 
Type: PrimitiveTypes.Int64, Metadata: rightMetadata}
+                               }
+                               left, right := NewSchema(leftFields, nil), 
NewSchema(rightFields, nil)
+                               if !left.Equal(right) {
+                                       b.Fatal("schemas should be equal")
+                               }
+                               b.ReportAllocs()
+                               for b.Loop() {
+                                       metadataEqualBenchmarkResult = 
left.Equal(right)
+                               }
+                       })
+               }
+       }
+}
diff --git a/arrow/metadata_equal_test.go b/arrow/metadata_equal_test.go
new file mode 100644
index 00000000..5aa69350
--- /dev/null
+++ b/arrow/metadata_equal_test.go
@@ -0,0 +1,191 @@
+// 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 arrow
+
+import (
+       "fmt"
+       "slices"
+       "strings"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestMetadataEqual(t *testing.T) {
+       for _, tc := range []struct {
+               name        string
+               left, right Metadata
+               want        bool
+       }{
+               {"empty", Metadata{}, NewMetadata(nil, nil), true},
+               {"empty-map", Metadata{}, MetadataFrom(map[string]string{}), 
true},
+               {"single", NewMetadata([]string{"key"}, []string{"value"}), 
NewMetadata([]string{"key"}, []string{"value"}), true},
+               {"single-copied-strings", NewMetadata([]string{"key"}, 
[]string{"value"}), NewMetadata([]string{strings.Clone("key")}, 
[]string{strings.Clone("value")}), true},
+               {"single-long-key-difference", 
NewMetadata([]string{strings.Repeat("a", 4096) + "x"}, []string{"value"}), 
NewMetadata([]string{strings.Repeat("a", 4096) + "y"}, []string{"value"}), 
false},
+               {"single-long-value-difference", NewMetadata([]string{"key"}, 
[]string{strings.Repeat("a", 4096) + "x"}), NewMetadata([]string{"key"}, 
[]string{strings.Repeat("a", 4096) + "y"}), false},
+               {"different-lengths", Metadata{}, NewMetadata([]string{"key"}, 
[]string{"value"}), false},
+               {"different-key", NewMetadata([]string{"key"}, 
[]string{"value"}), NewMetadata([]string{"other"}, []string{"value"}), false},
+               {"different-value", NewMetadata([]string{"key"}, 
[]string{"value"}), NewMetadata([]string{"key"}, []string{"other"}), false},
+               {"empty-key-value", NewMetadata([]string{""}, []string{""}), 
NewMetadata([]string{""}, []string{""}), true},
+               {"case-sensitive", NewMetadata([]string{"key"}, 
[]string{"value"}), NewMetadata([]string{"Key"}, []string{"value"}), false},
+               {"unicode", NewMetadata([]string{"é", "键"}, []string{"值", ""}), 
NewMetadata([]string{"é", "键"}, []string{"值", ""}), true},
+               {"unicode-normalization", NewMetadata([]string{"é"}, 
[]string{"value"}), NewMetadata([]string{"e\u0301"}, []string{"value"}), false},
+               {"embedded-zero", NewMetadata([]string{"key\x00"}, 
[]string{"value\x00"}), NewMetadata([]string{"key\x00"}, 
[]string{"value\x00"}), true},
+               {"matching-unsorted", NewMetadata([]string{"z", "a", "m"}, 
[]string{"1", "2", "3"}), NewMetadata([]string{"z", "a", "m"}, []string{"1", 
"2", "3"}), true},
+               {"reordered", NewMetadata([]string{"z", "a", "m"}, 
[]string{"1", "2", "3"}), NewMetadata([]string{"a", "m", "z"}, []string{"2", 
"3", "1"}), true},
+               {"reordered-different-value", NewMetadata([]string{"z", "a", 
"m"}, []string{"1", "2", "3"}), NewMetadata([]string{"a", "m", "z"}, 
[]string{"2", "4", "1"}), false},
+               {"duplicate-keys", NewMetadata([]string{"key", "key"}, 
[]string{"1", "2"}), NewMetadata([]string{"key", "key"}, []string{"1", "2"}), 
true},
+               {"duplicate-values-swapped", NewMetadata([]string{"key", 
"key"}, []string{"1", "2"}), NewMetadata([]string{"key", "key"}, []string{"2", 
"1"}), false},
+               {"duplicate-pairs-reordered", NewMetadata([]string{"key", 
"key", "a"}, []string{"v", "v", "a"}), NewMetadata([]string{"a", "key", "key"}, 
[]string{"a", "v", "v"}), true},
+               {"duplicate-distinct-values-reordered", 
NewMetadata([]string{"b", "a", "b"}, []string{"first", "a", "second"}), 
NewMetadata([]string{"a", "b", "b"}, []string{"a", "first", "second"}), true},
+               {"duplicate-distinct-values-swapped", NewMetadata([]string{"b", 
"a", "b"}, []string{"first", "a", "second"}), NewMetadata([]string{"a", "b", 
"b"}, []string{"a", "second", "first"}), false},
+               {"duplicate-counts", NewMetadata([]string{"key", "key", "a"}, 
[]string{"v", "v", "v"}), NewMetadata([]string{"key", "a", "a"}, []string{"v", 
"v", "v"}), false},
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       leftBefore := Metadata{keys: 
slices.Clone(tc.left.keys), values: slices.Clone(tc.left.values)}
+                       rightBefore := Metadata{keys: 
slices.Clone(tc.right.keys), values: slices.Clone(tc.right.values)}
+                       assert.Equal(t, tc.want, tc.left.Equal(tc.right))
+                       assert.Equal(t, tc.want, tc.right.Equal(tc.left))
+                       assert.Equal(t, leftBefore, tc.left)
+                       assert.Equal(t, rightBefore, tc.right)
+               })
+       }
+}
+
+func TestMetadataEqualDuplicateKeys(t *testing.T) {
+       for _, size := range []int{1, 2, 3, 12, 13, 15, 16, 17, 31, 32, 33, 64, 
127, 128, 129, 255, 256, 257, 1024, 2047, 2048, 2049} {
+               t.Run(fmt.Sprint(size), func(t *testing.T) {
+                       keys, values := make([]string, size), make([]string, 
size)
+                       for i := range keys {
+                               keys[i] = fmt.Sprintf("key_%d", i%4)
+                               values[i] = fmt.Sprintf("value_%d", i)
+                       }
+                       left, right := NewMetadata(keys, values), 
NewMetadata(keys, values)
+                       assert.True(t, left.Equal(right))
+                       right.Values()[size/2] = "changed"
+                       assert.False(t, left.Equal(right))
+                       assert.False(t, right.Equal(left))
+               })
+       }
+}
+
+func TestMetadataEqualReordered(t *testing.T) {
+       for _, size := range []int{2, 8, 11, 12, 13, 15, 16, 17, 31, 32, 33, 
127, 128, 129, 256, 257, 1024, 2047, 2048, 2049} {
+               t.Run(fmt.Sprint(size), func(t *testing.T) {
+                       keys, values := make([]string, size), make([]string, 
size)
+                       for i := range keys {
+                               keys[i] = fmt.Sprintf("key_%04d", i)
+                               values[i] = fmt.Sprintf("value_%04d", i)
+                       }
+                       left := NewMetadata(keys, values)
+                       for _, pattern := range []string{"reversed", 
"middle-swap", "last-swap"} {
+                               t.Run(pattern, func(t *testing.T) {
+                                       right := left.clone()
+                                       if pattern == "reversed" {
+                                               slices.Reverse(right.Keys())
+                                               slices.Reverse(right.Values())
+                                       } else {
+                                               pos := size/2 - 1
+                                               if pattern == "last-swap" {
+                                                       pos = size - 2
+                                               }
+                                               right.keys[pos], 
right.keys[pos+1] = right.keys[pos+1], right.keys[pos]
+                                               right.values[pos], 
right.values[pos+1] = right.values[pos+1], right.values[pos]
+                                       }
+                                       leftBefore, rightBefore := 
left.clone(), right.clone()
+                                       assert.True(t, left.Equal(right))
+                                       assert.True(t, right.Equal(left))
+                                       assert.Equal(t, leftBefore, left)
+                                       assert.Equal(t, rightBefore, right)
+                                       right.Values()[size/2] = "changed"
+                                       assert.False(t, left.Equal(right))
+                                       assert.False(t, right.Equal(left))
+                               })
+                       }
+               })
+       }
+}
+
+func TestMetadataEqualPermutations(t *testing.T) {
+       keys := []string{"z", "", "a", "键"}
+       values := []string{"value", "\x00", "", "值"}
+       var permutations [][]int
+       order := []int{0, 1, 2, 3}
+       var permute func(int)
+       permute = func(pos int) {
+               if pos == len(order) {
+                       permutations = append(permutations, slices.Clone(order))
+                       return
+               }
+               for i := pos; i < len(order); i++ {
+                       order[pos], order[i] = order[i], order[pos]
+                       permute(pos + 1)
+                       order[pos], order[i] = order[i], order[pos]
+               }
+       }
+       permute(0)
+       makeMetadata := func(order []int) Metadata {
+               k, v := make([]string, len(order)), make([]string, len(order))
+               for i, idx := range order {
+                       k[i], v[i] = keys[idx], values[idx]
+               }
+               return NewMetadata(k, v)
+       }
+       for _, leftOrder := range permutations {
+               for _, rightOrder := range permutations {
+                       t.Run(fmt.Sprintf("%v/%v", leftOrder, rightOrder), 
func(t *testing.T) {
+                               left, right := makeMetadata(leftOrder), 
makeMetadata(rightOrder)
+                               assert.True(t, left.Equal(right))
+                               assert.True(t, right.Equal(left))
+                       })
+               }
+       }
+}
+
+func TestMetadataEqualSmallDoesNotAllocate(t *testing.T) {
+       left := NewMetadata(
+               []string{"h", "a", "g", "b", "f", "c", "e", "d"},
+               []string{"8", "1", "7", "2", "6", "3", "5", "4"},
+       )
+       right := NewMetadata(
+               []string{"a", "b", "c", "d", "e", "f", "g", "h"},
+               []string{"1", "2", "3", "4", "5", "6", "7", "8"},
+       )
+
+       allocs := testing.AllocsPerRun(100, func() {
+               if !left.Equal(right) {
+                       panic("metadata should be equal")
+               }
+       })
+       assert.Zero(t, allocs)
+}
+
+func TestMetadataEqualAfterMutation(t *testing.T) {
+       left := NewMetadata([]string{"a", "b"}, []string{"1", "2"})
+       right := left.clone()
+       assert.True(t, left.Equal(right))
+       right.Values()[0] = "changed"
+       assert.False(t, left.Equal(right))
+       right.Values()[0] = "1"
+       right.Keys()[0] = "changed"
+       assert.False(t, left.Equal(right))
+       right.Keys()[0] = "a"
+       assert.True(t, left.Equal(right))
+       slices.Reverse(right.Keys())
+       slices.Reverse(right.Values())
+       assert.True(t, left.Equal(right))
+}
diff --git a/arrow/schema.go b/arrow/schema.go
index d3f52584..9172e0b5 100644
--- a/arrow/schema.go
+++ b/arrow/schema.go
@@ -123,6 +123,39 @@ func (md Metadata) clone() Metadata {
        return o
 }
 
+const smallMetadataLimit = 12
+
+func sortSmallMetadataIndices(keys []string, idxes []int) {
+       for i := range idxes {
+               idxes[i] = i
+       }
+       for i := 1; i < len(idxes); i++ {
+               for j := i; j > 0 && keys[idxes[j]] < keys[idxes[j-1]]; j-- {
+                       idxes[j], idxes[j-1] = idxes[j-1], idxes[j]
+               }
+       }
+}
+
+// Keep the small-sort arrays out of Equal's general frame.
+//
+//go:noinline
+func smallMetadataEqual(left, right Metadata) bool {
+       var leftStorage, rightStorage [smallMetadataLimit]int
+       leftIdxes := leftStorage[:len(left.keys)]
+       rightIdxes := rightStorage[:len(right.keys)]
+       sortSmallMetadataIndices(left.keys, leftIdxes)
+       sortSmallMetadataIndices(right.keys, rightIdxes)
+
+       for i := range leftIdxes {
+               j := leftIdxes[i]
+               k := rightIdxes[i]
+               if left.keys[j] != right.keys[k] || left.values[j] != 
right.values[k] {
+                       return false
+               }
+       }
+       return true
+}
+
 func (md Metadata) sortedIndices() []int {
        idxes := make([]int, len(md.keys))
        for i := range idxes {
@@ -139,6 +172,9 @@ func (md Metadata) Equal(rhs Metadata) bool {
        if md.Len() != rhs.Len() {
                return false
        }
+       if md.Len() <= smallMetadataLimit {
+               return smallMetadataEqual(md, rhs)
+       }
 
        idxes := md.sortedIndices()
        rhsIdxes := rhs.sortedIndices()

Reply via email to