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/iceberg-go.git
The following commit(s) were added to refs/heads/main by this push:
new 982042f92 perf(internal): reduce string difference allocations (#2243)
982042f92 is described below
commit 982042f92fe11c621addfad84d94ff25742c4833
Author: Minh Vu <[email protected]>
AuthorDate: Wed Oct 21 22:44:31 2026 +0200
perf(internal): reduce string difference allocations (#2243)
* perf(internal): avoid map overhead in string differences
Signed-off-by: Minh Vu <[email protected]>
* perf(internal): pre-size difference result from input lengths
---------
Signed-off-by: Minh Vu <[email protected]>
---
internal/difference_perf_test.go | 69 ++++++++++++++++++++++++++++++++++++++++
internal/utils.go | 28 +++++++++++++---
2 files changed, 92 insertions(+), 5 deletions(-)
diff --git a/internal/difference_perf_test.go b/internal/difference_perf_test.go
new file mode 100644
index 000000000..2dfd20be1
--- /dev/null
+++ b/internal/difference_perf_test.go
@@ -0,0 +1,69 @@
+// 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 internal_test
+
+import (
+ "fmt"
+ "reflect"
+ "testing"
+
+ "github.com/apache/iceberg-go/internal"
+)
+
+func TestDifferenceFastPaths(t *testing.T) {
+ for _, tc := range []struct {
+ name string
+ a, b []string
+ want []string
+ }{
+ {name: "nil", want: []string{}},
+ {name: "empty input", b: []string{"a", "b", "c"}, want:
[]string{}},
+ {name: "no exclusions", a: []string{"b", "a", "a"}, want:
[]string{"b", "a", "a"}},
+ {name: "one exclusion", a: []string{"a", "b", "a", "c"}, b:
[]string{"a"}, want: []string{"b", "c"}},
+ {name: "two exclusions", a: []string{"a", "b", "c", "b", "d"},
b: []string{"b", "d"}, want: []string{"a", "c"}},
+ {name: "many exclusions", a: []string{"a", "b", "c", "d", "e",
"d"}, b: []string{"b", "d", "x", "d"}, want: []string{"a", "c", "e"}},
+ {name: "all excluded", a: []string{"a", "a"}, b: []string{"a"},
want: []string{}},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ if got := internal.Difference(tc.a, tc.b);
!reflect.DeepEqual(got, tc.want) {
+ t.Fatalf("Difference(%v, %v) = %v, want %v",
tc.a, tc.b, got, tc.want)
+ }
+ })
+ }
+}
+
+var differenceBenchSink []string
+
+func BenchmarkDifference(b *testing.B) {
+ for _, size := range []int{1_024, 8_192} {
+ paths := make([]string, size)
+ for i := range paths {
+ paths[i] =
fmt.Sprintf("data/partition=%06d/file-%06d.parquet", i, i)
+ }
+ for _, excluded := range []int{0, 1, size / 2, size} {
+ b.Run(fmt.Sprintf("entries=%d/exclusions=%d", size,
excluded), func(b *testing.B) {
+ removed := paths[:excluded]
+ b.ReportAllocs()
+ b.ResetTimer()
+ for range b.N {
+ differenceBenchSink =
internal.Difference(paths, removed)
+ }
+ })
+ }
+ }
+}
diff --git a/internal/utils.go b/internal/utils.go
index ca9108f8b..c5de79771 100644
--- a/internal/utils.go
+++ b/internal/utils.go
@@ -84,14 +84,32 @@ func FloorDiv[T constraints.Integer](a, b T) T {
// Helper function to find the difference between two slices (a - b).
func Difference(a, b []string) []string {
- m := make(map[string]bool)
- for _, item := range b {
- m[item] = true
+ if len(a) == 0 {
+ return []string{}
+ }
+ if len(b) == 0 {
+ return append(make([]string, 0, len(a)), a...)
+ }
+ if len(b) <= 2 {
+ diff := make([]string, 0, len(a))
+ for _, item := range a {
+ if !slices.Contains(b, item) {
+ diff = append(diff, item)
+ }
+ }
+
+ return diff
}
- diff := make([]string, 0)
+ m := make(map[string]struct{}, len(b))
+ for _, item := range b {
+ m[item] = struct{}{}
+ }
+ // Leave room for the likely survivors without reserving for every
input.
+ // Append still handles duplicate entries and exclusions outside a.
+ diff := make([]string, 0, max(0, len(a)-len(b)))
for _, item := range a {
- if !m[item] {
+ if _, excluded := m[item]; !excluded {
diff = append(diff, item)
}
}