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 9b1b0958 perf(arrow/array): optimize bulk appends for binary builders 
(#1188)
9b1b0958 is described below

commit 9b1b09582b9912e627852759fb1de13a075e2da7
Author: Minh Vu <[email protected]>
AuthorDate: Wed Aug 26 17:40:45 2026 +0200

    perf(arrow/array): optimize bulk appends for binary builders (#1188)
    
    ## Summary
    
    - reserve the full batch once in `BinaryBuilder.AppendNulls` and
    `AppendEmptyValues`
    - fill the repeated current offsets directly for both 32-bit and 64-bit
    offset buffers
    - update the validity bitmap and counters once per batch
    - add coverage and benchmarks for Binary, LargeBinary, String, and
    LargeString
    
    This follows the same batching shape used by [Arrow C++ binary
    
builders](https://github.com/apache/arrow/blob/485499fd02ea2b0c323d67871fbe96aae4232504/cpp/src/arrow/array/builder_binary.h#L100-L140).
    
    ## Benchmark
    
    65,536 appended values, representative medians on darwin/arm64:
    
    | Builder | Operation | Before | After | Change |
    | --- | --- | ---: | ---: | ---: |
    | Binary | nulls | 403 us | 93 us | -77% |
    | LargeBinary | nulls | 475 us | 158 us | -67% |
    | String | nulls | 402 us | 85 us | -79% |
    | String | empty | 458 us | 84 us | -82% |
    | LargeString | nulls | 532 us | 159 us | -70% |
    | LargeString | empty | 517 us | 154 us | -70% |
    
    Allocations per build drop from 27 to 8 for 32-bit offsets and from 28
    to 8 for 64-bit offsets. Single-value performance stays about the same.
    
    ## Tests
    
    - `go test ./arrow/array -count=1`
    - `go test -race ./arrow/array -run
    TestBinaryBuilderBulkAppendNullsAndEmptyValues -count=1`
    - `go vet ./arrow/array`
---
 arrow/array/binarybuilder.go           |  38 +++++-
 arrow/array/binarybuilder_bulk_test.go | 233 +++++++++++++++++++++++++++++++++
 2 files changed, 267 insertions(+), 4 deletions(-)

diff --git a/arrow/array/binarybuilder.go b/arrow/array/binarybuilder.go
index 2391bbe2..8d7c5977 100644
--- a/arrow/array/binarybuilder.go
+++ b/arrow/array/binarybuilder.go
@@ -25,6 +25,7 @@ import (
        "unsafe"
 
        "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/bitutil"
        "github.com/apache/arrow-go/v18/arrow/internal/debug"
        "github.com/apache/arrow-go/v18/arrow/memory"
        "github.com/apache/arrow-go/v18/internal/json"
@@ -127,9 +128,15 @@ func (b *BinaryBuilder) AppendNull() {
 }
 
 func (b *BinaryBuilder) AppendNulls(n int) {
-       for i := 0; i < n; i++ {
-               b.AppendNull()
+       if n <= 0 {
+               return
        }
+
+       b.Reserve(n)
+       b.appendCurrentOffsets(n)
+       bitutil.SetBitsTo(b.nullBitmap.Bytes(), int64(b.length), int64(n), 
false)
+       b.length += n
+       b.nulls += n
 }
 
 func (b *BinaryBuilder) AppendEmptyValue() {
@@ -139,9 +146,13 @@ func (b *BinaryBuilder) AppendEmptyValue() {
 }
 
 func (b *BinaryBuilder) AppendEmptyValues(n int) {
-       for i := 0; i < n; i++ {
-               b.AppendEmptyValue()
+       if n <= 0 {
+               return
        }
+
+       b.Reserve(n)
+       b.appendCurrentOffsets(n)
+       b.unsafeAppendBoolsToBitmap(nil, n)
 }
 
 // AppendValues will append the values in the v slice. The valid slice 
determines which values
@@ -348,6 +359,25 @@ func (b *BinaryBuilder) appendNextOffset() {
        b.appendOffsetVal(numBytes)
 }
 
+func (b *BinaryBuilder) appendCurrentOffsets(n int) {
+       numBytes := b.values.Len()
+       debug.Assert(uint64(numBytes) <= b.maxCapacity, "exceeded maximum 
capacity of binary array")
+       start := b.offsets.Len() * b.offsetByteWidth
+       b.offsets.Advance(n * b.offsetByteWidth)
+       switch b.offsetByteWidth {
+       case arrow.Int32SizeBytes:
+               offsets := 
arrow.Int32Traits.CastFromBytes(b.offsets.Bytes()[start:])
+               for i := range offsets {
+                       offsets[i] = int32(numBytes)
+               }
+       case arrow.Int64SizeBytes:
+               offsets := 
arrow.Int64Traits.CastFromBytes(b.offsets.Bytes()[start:])
+               for i := range offsets {
+                       offsets[i] = int64(numBytes)
+               }
+       }
+}
+
 func (b *BinaryBuilder) AppendValueFromString(s string) error {
        if s == NullValueStr {
                b.AppendNull()
diff --git a/arrow/array/binarybuilder_bulk_test.go 
b/arrow/array/binarybuilder_bulk_test.go
new file mode 100644
index 00000000..8448cb30
--- /dev/null
+++ b/arrow/array/binarybuilder_bulk_test.go
@@ -0,0 +1,233 @@
+// 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 (
+       "fmt"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+type binaryBuilderFactory func(memory.Allocator) array.Builder
+
+var binaryBuilderFactories = []struct {
+       name string
+       new  binaryBuilderFactory
+}{
+       {"binary", func(mem memory.Allocator) array.Builder {
+               return array.NewBinaryBuilder(mem, arrow.BinaryTypes.Binary)
+       }},
+       {"large_binary", func(mem memory.Allocator) array.Builder {
+               return array.NewBinaryBuilder(mem, 
arrow.BinaryTypes.LargeBinary)
+       }},
+       {"string", func(mem memory.Allocator) array.Builder {
+               return array.NewStringBuilder(mem)
+       }},
+       {"large_string", func(mem memory.Allocator) array.Builder {
+               return array.NewLargeStringBuilder(mem)
+       }},
+}
+
+func TestBinaryBuilderBulkAppendNullsAndEmptyValues(t *testing.T) {
+       for _, tc := range binaryBuilderFactories {
+               t.Run(tc.name, func(t *testing.T) {
+                       mem := 
memory.NewCheckedAllocator(memory.NewGoAllocator())
+                       defer mem.AssertSize(t, 0)
+
+                       builder := tc.new(mem)
+                       defer builder.Release()
+                       appendBinaryBuilderValue(builder, "abc")
+                       builder.AppendNulls(5)
+                       builder.AppendEmptyValues(4)
+                       appendBinaryBuilderValue(builder, "de")
+
+                       arr := builder.NewArray().(array.BinaryLike)
+                       defer arr.Release()
+                       require.NoError(t, arr.(interface{ ValidateFull() error 
}).ValidateFull())
+
+                       assert.Equal(t, 11, arr.Len())
+                       assert.Equal(t, 5, arr.NullN())
+                       assert.Equal(t, []byte("abcde"), arr.ValueBytes())
+                       for i := range arr.Len() {
+                               assert.Equal(t, i == 0 || i >= 6, 
arr.IsValid(i), "value %d", i)
+                               switch i {
+                               case 0:
+                                       assert.Equal(t, int64(0), 
arr.ValueOffset64(i))
+                                       assert.Equal(t, 3, arr.ValueLen(i))
+                               case 10:
+                                       assert.Equal(t, int64(3), 
arr.ValueOffset64(i))
+                                       assert.Equal(t, 2, arr.ValueLen(i))
+                               default:
+                                       assert.Equal(t, int64(3), 
arr.ValueOffset64(i))
+                                       assert.Zero(t, arr.ValueLen(i))
+                               }
+                       }
+               })
+       }
+}
+
+func appendBinaryBuilderValue(builder array.Builder, value string) {
+       switch builder := builder.(type) {
+       case *array.BinaryBuilder:
+               builder.Append([]byte(value))
+       case *array.StringBuilder:
+               builder.Append(value)
+       case *array.LargeStringBuilder:
+               builder.Append(value)
+       default:
+               panic(fmt.Sprintf("unexpected binary builder %T", builder))
+       }
+}
+
+func TestBinaryBuilderBulkAppendMatchesScalar(t *testing.T) {
+       starts := []int{0, 1, 7, 8, 9, 15, 16, 17}
+       batchSizes := []int{-1, 0, 1, 2, 7, 8, 9, 16, 17}
+       operations := []struct {
+               name   string
+               bulk   func(array.Builder, int)
+               scalar func(array.Builder, int)
+       }{
+               {
+                       name: "nulls",
+                       bulk: func(builder array.Builder, n int) {
+                               builder.AppendNulls(n)
+                       },
+                       scalar: func(builder array.Builder, n int) {
+                               for i := 0; i < n; i++ {
+                                       builder.AppendNull()
+                               }
+                       },
+               },
+               {
+                       name: "empty_values",
+                       bulk: func(builder array.Builder, n int) {
+                               builder.AppendEmptyValues(n)
+                       },
+                       scalar: func(builder array.Builder, n int) {
+                               for i := 0; i < n; i++ {
+                                       builder.AppendEmptyValue()
+                               }
+                       },
+               },
+       }
+
+       for _, factory := range binaryBuilderFactories {
+               t.Run(factory.name, func(t *testing.T) {
+                       for operationIndex, operation := range operations {
+                               t.Run(operation.name, func(t *testing.T) {
+                                       reuseOperation := 
operations[1-operationIndex]
+                                       for _, start := range starts {
+                                               for _, batchSize := range 
batchSizes {
+                                                       
t.Run(fmt.Sprintf("start_%d_batch_%d", start, batchSize), func(t *testing.T) {
+                                                               mem := 
memory.NewCheckedAllocator(memory.NewGoAllocator())
+                                                               defer 
mem.AssertSize(t, 0)
+
+                                                               bulk := 
factory.new(mem)
+                                                               defer 
bulk.Release()
+                                                               scalar := 
factory.new(mem)
+                                                               defer 
scalar.Release()
+
+                                                               
appendBinaryBuilderPrefix(bulk, start)
+                                                               
appendBinaryBuilderPrefix(scalar, start)
+                                                               
operation.bulk(bulk, batchSize)
+                                                               
operation.scalar(scalar, batchSize)
+                                                               
assertBinaryBuilderArrayParity(t, bulk, scalar)
+
+                                                               
appendBinaryBuilderPrefix(bulk, start)
+                                                               
appendBinaryBuilderPrefix(scalar, start)
+                                                               
reuseOperation.bulk(bulk, 9)
+                                                               
reuseOperation.scalar(scalar, 9)
+                                                               
assertBinaryBuilderArrayParity(t, bulk, scalar)
+                                                       })
+                                               }
+                                       }
+                               })
+                       }
+               })
+       }
+}
+
+func appendBinaryBuilderPrefix(builder array.Builder, n int) {
+       for i := 0; i < n; i++ {
+               switch i % 4 {
+               case 0:
+                       appendBinaryBuilderValue(builder, 
fmt.Sprintf("value-%d", i))
+               case 1:
+                       builder.AppendNull()
+               case 2:
+                       builder.AppendEmptyValue()
+               case 3:
+                       appendBinaryBuilderValue(builder, 
fmt.Sprintf("tail-%d", i))
+               }
+       }
+}
+
+func assertBinaryBuilderArrayParity(t *testing.T, bulk, scalar array.Builder) {
+       t.Helper()
+
+       assert.Equal(t, scalar.Len(), bulk.Len())
+       assert.Equal(t, scalar.NullN(), bulk.NullN())
+
+       bulkArray := bulk.NewArray()
+       defer bulkArray.Release()
+       scalarArray := scalar.NewArray()
+       defer scalarArray.Release()
+
+       require.NoError(t, bulkArray.(interface{ ValidateFull() error 
}).ValidateFull())
+       require.NoError(t, scalarArray.(interface{ ValidateFull() error 
}).ValidateFull())
+       assert.True(t, array.Equal(bulkArray, scalarArray))
+}
+
+func BenchmarkBinaryBuilderBulkAppend(b *testing.B) {
+       for _, tc := range binaryBuilderFactories {
+               b.Run(tc.name, func(b *testing.B) {
+                       for _, rows := range []int{1, 16, 1024, 65536} {
+                               b.Run(fmt.Sprintf("rows_%d", rows), func(b 
*testing.B) {
+                                       b.Run("nulls", func(b *testing.B) {
+                                               
benchmarkBinaryBuilderBulkAppend(b, tc.new, rows, false)
+                                       })
+                                       b.Run("empty", func(b *testing.B) {
+                                               
benchmarkBinaryBuilderBulkAppend(b, tc.new, rows, true)
+                                       })
+                               })
+                       }
+               })
+       }
+}
+
+func benchmarkBinaryBuilderBulkAppend(b *testing.B, factory 
binaryBuilderFactory, rows int, empty bool) {
+       builder := factory(memory.DefaultAllocator)
+       defer builder.Release()
+       b.ReportAllocs()
+
+       for b.Loop() {
+               if empty {
+                       builder.AppendEmptyValues(rows)
+               } else {
+                       builder.AppendNulls(rows)
+               }
+
+               arr := builder.NewArray()
+               arr.Release()
+       }
+}

Reply via email to