You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by at...@apache.org on 2026/10/09 16:20:02 UTC

(iceberg-go) branch main updated: perf(transforms): avoid redundant integer truncate modulus (#2145)

This is an automated email from the ASF dual-hosted git repository.

laskoviymishka 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 85222f5c1 perf(transforms): avoid redundant integer truncate modulus (#2145)
85222f5c1 is described below

commit 85222f5c197a1eea13a8cdeceeb12be582c509f3
Author: Minh Vu <vu...@gmail.com>
AuthorDate: Fri Oct 9 18:19:56 2026 +0200

    perf(transforms): avoid redundant integer truncate modulus (#2145)
    
    * perf(transforms): avoid redundant integer truncate modulus
    
    * test(transforms): correct minimum integer truncate expectation
    
    * test(transforms): strengthen integer truncate coverage
    
    Signed-off-by: Minh Vu <vu...@gmail.com>
---
 transforms.go                    | 12 +++++-
 transforms_integer_bench_test.go | 83 ++++++++++++++++++++++++++++++++++++++++
 transforms_test.go               | 23 ++++++++++-
 3 files changed, 115 insertions(+), 3 deletions(-)

diff --git a/transforms.go b/transforms.go
index 6c66797df..5fea782eb 100644
--- a/transforms.go
+++ b/transforms.go
@@ -742,8 +742,12 @@ func (t TruncateTransform) Transformer(src Type) (func(any) any, error) {
 
 			val := v.(int32)
 			width := int32(t.Width)
+			remainder := val % width
+			if remainder < 0 {
+				remainder += width
+			}
 
-			return val - (((val % width) + width) % width)
+			return val - remainder
 		}, nil
 	case Int64Type:
 		return func(v any) any {
@@ -753,8 +757,12 @@ func (t TruncateTransform) Transformer(src Type) (func(any) any, error) {
 
 			val := v.(int64)
 			width := int64(t.Width)
+			remainder := val % width
+			if remainder < 0 {
+				remainder += width
+			}
 
-			return val - (((val % width) + width) % width)
+			return val - remainder
 		}, nil
 	case StringType:
 		return func(v any) any {
diff --git a/transforms_integer_bench_test.go b/transforms_integer_bench_test.go
new file mode 100644
index 000000000..a8849dced
--- /dev/null
+++ b/transforms_integer_bench_test.go
@@ -0,0 +1,83 @@
+// 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_test
+
+import (
+	"math"
+	"testing"
+
+	"github.com/apache/iceberg-go"
+)
+
+var benchmarkIntegerTruncateResult any
+
+func BenchmarkTruncateTransformInteger(b *testing.B) {
+	tests := []struct {
+		name   string
+		typ    iceberg.Type
+		width  int
+		values []any
+	}{
+		{
+			name:  "int32/mixed",
+			typ:   iceberg.PrimitiveTypes.Int32,
+			width: 97,
+			values: []any{
+				int32(-1000), int32(-98), int32(-1), int32(0),
+				int32(1), int32(96), int32(97), int32(1000),
+			},
+		},
+		{
+			name:  "int64/mixed",
+			typ:   iceberg.PrimitiveTypes.Int64,
+			width: 97,
+			values: []any{
+				int64(-1 << 40), int64(-1000), int64(-1), int64(0),
+				int64(1), int64(1000), int64(1 << 40),
+			},
+		},
+		{
+			name:  "int32/width-max",
+			typ:   iceberg.PrimitiveTypes.Int32,
+			width: math.MaxInt32,
+			values: []any{
+				int32(math.MinInt32), int32(-1), int32(0),
+				int32(math.MaxInt32 - 1), int32(math.MaxInt32),
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		b.Run(tt.name, func(b *testing.B) {
+			transformer, err := (iceberg.TruncateTransform{Width: tt.width}).Transformer(tt.typ)
+			if err != nil {
+				b.Fatal(err)
+			}
+
+			b.ReportAllocs()
+			index := 0
+			for b.Loop() {
+				benchmarkIntegerTruncateResult = transformer(tt.values[index])
+				index++
+				if index == len(tt.values) {
+					index = 0
+				}
+			}
+		})
+	}
+}
diff --git a/transforms_test.go b/transforms_test.go
index 493a53441..2024c1ebb 100644
--- a/transforms_test.go
+++ b/transforms_test.go
@@ -1137,8 +1137,24 @@ func TestTruncateTransform(t *testing.T) {
 	}{
 		{10, iceberg.Int32Literal(1), iceberg.Int32Literal(0)},
 		{10, iceberg.Int32Literal(-1), iceberg.Int32Literal(-10)},
+		{10, iceberg.Int32Literal(-10), iceberg.Int32Literal(-10)},
+		{10, iceberg.Int32Literal(-11), iceberg.Int32Literal(-20)},
+		{1, iceberg.Int32Literal(math.MinInt32), iceberg.Int32Literal(math.MinInt32)},
+		{1 << 30, iceberg.Int32Literal((1 << 30) - 1), iceberg.Int32Literal(0)},
+		{(1 << 30) + 1, iceberg.Int32Literal((1 << 30) - 1), iceberg.Int32Literal(0)},
+		{math.MaxInt32, iceberg.Int32Literal(math.MaxInt32 - 1), iceberg.Int32Literal(0)},
+		{math.MaxInt32, iceberg.Int32Literal(math.MaxInt32), iceberg.Int32Literal(math.MaxInt32)},
+		{math.MaxInt32, iceberg.Int32Literal(-1), iceberg.Int32Literal(-math.MaxInt32)},
+		// The final subtraction retains signed integer wraparound at the minimum value.
+		{math.MaxInt32, iceberg.Int32Literal(math.MinInt32), iceberg.Int32Literal(2)},
 		{10, iceberg.Int64Literal(1), iceberg.Int64Literal(0)},
 		{10, iceberg.Int64Literal(-1), iceberg.Int64Literal(-10)},
+		{10, iceberg.Int64Literal(-10), iceberg.Int64Literal(-10)},
+		{10, iceberg.Int64Literal(-11), iceberg.Int64Literal(-20)},
+		{1, iceberg.Int64Literal(math.MinInt64), iceberg.Int64Literal(math.MinInt64)},
+		{10, iceberg.Int64Literal(math.MaxInt64), iceberg.Int64Literal(math.MaxInt64 - 7)},
+		{10, iceberg.Int64Literal(math.MinInt64), iceberg.Int64Literal(math.MaxInt64 - 1)},
+		{math.MaxInt32, iceberg.Int64Literal(math.MaxInt64), iceberg.Int64Literal(math.MaxInt64 - 1)},
 		{50, iceberg.DecimalLiteral{
 			Val:   decimal128.FromI64(1065),
 			Scale: 2,
@@ -1154,8 +1170,13 @@ func TestTruncateTransform(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-		t.Run(fmt.Sprintf("width=%d value=%v", tt.width, tt.value.Any()), func(t *testing.T) {
+		t.Run(fmt.Sprintf("type=%s/width=%d/value=%v", tt.value.Type(), tt.width, tt.value.Any()), func(t *testing.T) {
 			transform := iceberg.TruncateTransform{Width: tt.width}
+			transformer, err := transform.Transformer(tt.value.Type())
+			require.NoError(t, err)
+			assert.Equal(t, tt.expected.Any(), transformer(tt.value.Any()))
+			assert.Nil(t, transformer(nil))
+
 			result := transform.Apply(iceberg.Optional[iceberg.Literal]{Val: tt.value, Valid: true})
 			require.True(t, result.Valid)
 			assert.Equal(t, tt.expected, result.Val)