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 914040c0b perf(transforms): skip UTF-8 scan for unchanged strings 
(#2222)
914040c0b is described below

commit 914040c0baad3fe09b343ab28ecd202e19b2f325
Author: Minh Vu <[email protected]>
AuthorDate: Mon Oct 19 23:54:21 2026 +0200

    perf(transforms): skip UTF-8 scan for unchanged strings (#2222)
    
    * perf(transforms): skip UTF-8 scan for unchanged truncated strings
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * test(perf): benchmark unchanged and truncated UTF-8 strings
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * perf(transforms): fast-path ASCII prefixes during string truncation
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * style(transforms): satisfy nlreturn in benchmark helper
    
    Signed-off-by: Minh Vu <[email protected]>
    
    ---------
    
    Signed-off-by: Minh Vu <[email protected]>
---
 transforms.go                               |  22 +++++-
 transforms_string_truncate_fastpath_test.go | 110 ++++++++++++++++++++++++++++
 2 files changed, 128 insertions(+), 4 deletions(-)

diff --git a/transforms.go b/transforms.go
index 5fea782eb..92895949f 100644
--- a/transforms.go
+++ b/transforms.go
@@ -804,12 +804,26 @@ func (t TruncateTransform) Transformer(src Type) 
(func(any) any, error) {
                ErrInvalidArgument, src)
 }
 
-// Range yields byte indexes at UTF-8 code point boundaries, so slicing on idx
-// truncates by Unicode characters without copying the whole string to []rune.
+// truncateString truncates by UTF-8 code points without allocating.
+// Strings shorter than the width in bytes need no scan. ASCII prefixes
+// can be sliced directly, while other prefixes still use rune boundaries.
 func truncateString(s string, width int) string {
-       for idx := range s {
+       if width >= len(s) {
+               return s
+       }
+
+       idx := 0
+       for idx < width && s[idx] < utf8.RuneSelf {
+               idx++
+       }
+       if idx == width {
+               return s[:idx]
+       }
+
+       width -= idx
+       for offset := range s[idx:] {
                if width == 0 {
-                       return s[:idx]
+                       return s[:idx+offset]
                }
 
                width--
diff --git a/transforms_string_truncate_fastpath_test.go 
b/transforms_string_truncate_fastpath_test.go
new file mode 100644
index 000000000..a5948d917
--- /dev/null
+++ b/transforms_string_truncate_fastpath_test.go
@@ -0,0 +1,110 @@
+// 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 iceberg
+
+import (
+       "strings"
+       "testing"
+)
+
+// Keep the previous implementation here to make before/after comparisons
+// reproducible on the same toolchain and machine.
+func truncateStringBefore(s string, width int) string {
+       for idx := range s {
+               if width == 0 {
+                       return s[:idx]
+               }
+               width--
+       }
+
+       return s
+}
+
+func TestTruncateStringFastPath(t *testing.T) {
+       for _, tc := range []struct {
+               name, input string
+               width       int
+               want        string
+       }{
+               {"short_ascii_unchanged", "us-east-1", 32, "us-east-1"},
+               {"short_ascii_truncated", "us-east-1", 4, "us-e"},
+               {"unicode_unchanged", "東京🧊Berlin", 32, "東京🧊Berlin"},
+               {"unicode_truncated", "東京🧊Berlin", 4, "東京🧊B"},
+               {"utf8_bytes_exceed_width", "éé", 3, "éé"},
+               {"combining_codepoints", "e\u0301clair", 1, "e"},
+               {"invalid_utf8_prefix", "a\xffb", 2, "a\xff"},
+               {"negative_width", "iceberg", -1, "iceberg"},
+               {"exact_width", "iceberg", 7, "iceberg"},
+               {"empty", "", 1, ""},
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       if got := truncateString(tc.input, tc.width); got != 
tc.want {
+                               t.Fatalf("truncateString(%q, %d) = %q, want 
%q", tc.input, tc.width, got, tc.want)
+                       }
+               })
+       }
+
+       // Compare every width around both byte and code-point boundaries. The
+       // invalid UTF-8 input verifies this optimization preserves the existing
+       // behavior even for strings that cannot appear in spec-compliant data.
+       for _, input := range []string{
+               "", "us-east-1", "東京🧊Berlin", "éé", "a\xffb", 
strings.Repeat("é", 64),
+       } {
+               for width := 0; width <= len(input)+2; width++ {
+                       if got, want := truncateString(input, width), 
truncateStringBefore(input, width); got != want {
+                               t.Fatalf("truncateString(%q, %d) = %q, before = 
%q", input, width, got, want)
+                       }
+               }
+       }
+}
+
+var truncateStringBenchmarkSink string
+
+func BenchmarkTruncateStringFastPath(b *testing.B) {
+       for _, tc := range []struct {
+               name, input string
+               width       int
+       }{
+               {"ascii_short_noop", "us-east-1", 32},
+               {"ascii_short_truncate", "us-east-1", 4},
+               {"ascii_long_noop", strings.Repeat("a", 128), 256},
+               {"unicode_short_noop", "東京🧊Berlin", 32},
+               {"unicode_short_truncate", "東京🧊Berlin", 4},
+               {"unicode_long_noop", strings.Repeat("é", 64), 256},
+               {"unicode_long_truncate", strings.Repeat("é", 64), 16},
+               {"utf8_bytes_exceed_width", "éé", 3},
+       } {
+               b.Run(tc.name, func(b *testing.B) {
+                       for _, implementation := range []struct {
+                               name string
+                               run  func(string, int) string
+                       }{
+                               {"before", truncateStringBefore},
+                               {"after", truncateString},
+                       } {
+                               b.Run(implementation.name, func(b *testing.B) {
+                                       b.ReportAllocs()
+                                       b.ResetTimer()
+                                       for range b.N {
+                                               truncateStringBenchmarkSink = 
implementation.run(tc.input, tc.width)
+                                       }
+                               })
+                       }
+               })
+       }
+}

Reply via email to