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 2c703211 perf(arrow/array): write bulk binary offsets and payloads 
directly (#1413)
2c703211 is described below

commit 2c703211a38567399ba1bd0491a82e4c0b6b2c73
Author: Minh Vu <[email protected]>
AuthorDate: Tue Oct 27 17:15:44 2026 +0100

    perf(arrow/array): write bulk binary offsets and payloads directly (#1413)
    
    ### Rationale for this change
    
    `AppendValues`/`AppendStringValues` on the Binary, LargeBinary, String
    and LargeString builders still wrote each offset and payload separately
    after reserving space.
    
    ### What changes are included in this PR?
    
    - A shared generic helper, `appendBinaryValues[T ~string | ~[]byte]`,
    writes the offsets directly into the reserved offset buffer and copies
    the payloads in a single pass, for both 32- and 64-bit offsets.
    - Nulls keep their payload bytes, and the validity bitmap and terminal
    offset are handled as before.
    
    ### Are these changes tested?
    
    Yes: `TestBinaryBuilderDirectBulkOffsetsAcrossBatches` (in
    `binarybuilder_bulk_test.go`) covers unaligned prefixes, repeated
    batches, nulls, empty values and mixing with scalar appends for all four
    builder types.
    
    `BenchmarkBinaryBuilderAppendValues` on amd64 (pinned, GOMAXPROCS=1,
    `main` → this PR) is about 20–37% faster on the stable cases, e.g.
    string 1024 rows width 4: 8.74 → 5.50 µs; large_string 65536 rows width
    4: 782 → 574 µs.
    
    ### Are there any user-facing changes?
    
    No API changes; faster bulk appends to binary and string builders.
    
    ---------
    
    Co-authored-by: Matt Topol <[email protected]>
---
 arrow/array/binarybuilder.go           | 75 +++++++++++++++++----------------
 arrow/array/binarybuilder_bulk_test.go | 76 ++++++++++++++++++++++++++++++++++
 2 files changed, 115 insertions(+), 36 deletions(-)

diff --git a/arrow/array/binarybuilder.go b/arrow/array/binarybuilder.go
index 166bf6ac..761ab3e2 100644
--- a/arrow/array/binarybuilder.go
+++ b/arrow/array/binarybuilder.go
@@ -164,60 +164,63 @@ func (b *BinaryBuilder) AppendEmptyValues(n int) {
 // in v are valid (not null). The valid slice must either be empty or be equal 
in length to v. If empty,
 // all values in v are appended and considered valid.
 func (b *BinaryBuilder) AppendValues(v [][]byte, valid []bool) {
-       if len(v) != len(valid) && len(valid) != 0 {
-               panic("len(v) != len(valid) && len(valid) != 0")
-       }
-
-       if len(v) == 0 {
-               return
-       }
-
-       b.Reserve(len(v))
-       b.reserveOffsetCapacity(len(v))
-
-       // Pre-calculate total data size to minimize allocations
-       totalDataSize := 0
-       for _, vv := range v {
-               totalDataSize += len(vv)
-       }
-       b.ReserveData(totalDataSize)
-
-       for _, vv := range v {
-               b.unsafeAppendNextOffset()
-               b.values.unsafeAppend(vv)
-       }
-
-       b.unsafeAppendBoolsToBitmap(valid, len(v))
+       appendBinaryValues(b, v, valid)
 }
 
 // AppendStringValues will append the values in the v slice. The valid slice 
determines which values
 // in v are valid (not null). The valid slice must either be empty or be equal 
in length to v. If empty,
 // all values in v are appended and considered valid.
 func (b *BinaryBuilder) AppendStringValues(v []string, valid []bool) {
-       if len(v) != len(valid) && len(valid) != 0 {
+       appendBinaryValues(b, v, valid)
+}
+
+// appendBinaryValues writes offsets and payloads in one pass after reserving
+// both buffers. The terminal offset is appended by newData, as for scalar
+// appends. Invalid slots still retain their payload and offsets.
+func appendBinaryValues[T ~string | ~[]byte](b *BinaryBuilder, values []T, 
valid []bool) {
+       if len(values) != len(valid) && len(valid) != 0 {
                panic("len(v) != len(valid) && len(valid) != 0")
        }
-
-       if len(v) == 0 {
+       if len(values) == 0 {
                return
        }
 
-       b.Reserve(len(v))
-       b.reserveOffsetCapacity(len(v))
+       b.Reserve(len(values))
+       b.reserveOffsetCapacity(len(values))
 
-       // Pre-calculate total data size to minimize allocations
        totalDataSize := 0
-       for _, vv := range v {
-               totalDataSize += len(vv)
+       for _, v := range values {
+               totalDataSize += len(v)
        }
        b.ReserveData(totalDataSize)
 
-       for _, vv := range v {
-               b.unsafeAppendNextOffset()
-               b.values.unsafeAppend([]byte(vv))
+       pos := b.values.length
+       debug.Assert(uint64(pos+totalDataSize) <= b.maxCapacity, "exceeded 
maximum capacity of binary array")
+       data := b.values.bytes
+
+       switch b.offsetByteWidth {
+       case arrow.Int32SizeBytes:
+               offsets := b.offsets.(*int32BufferBuilder)
+               end := offsets.length + len(values)*arrow.Int32SizeBytes
+               dst := 
arrow.Int32Traits.CastFromBytes(offsets.bytes[offsets.length:end])
+               for i, v := range values {
+                       dst[i] = int32(pos)
+                       pos += copy(data[pos:], v)
+               }
+               offsets.length = end
+       case arrow.Int64SizeBytes:
+               offsets := b.offsets.(*int64BufferBuilder)
+               end := offsets.length + len(values)*arrow.Int64SizeBytes
+               dst := 
arrow.Int64Traits.CastFromBytes(offsets.bytes[offsets.length:end])
+               for i, v := range values {
+                       dst[i] = int64(pos)
+                       pos += copy(data[pos:], v)
+               }
+               offsets.length = end
        }
 
-       b.unsafeAppendBoolsToBitmap(valid, len(v))
+       b.values.length = pos
+       b.unsafeAppendBoolsToBitmap(valid, len(values))
 }
 
 func (b *BinaryBuilder) UnsafeAppend(v []byte) {
diff --git a/arrow/array/binarybuilder_bulk_test.go 
b/arrow/array/binarybuilder_bulk_test.go
index 53ad842f..bcc8a637 100644
--- a/arrow/array/binarybuilder_bulk_test.go
+++ b/arrow/array/binarybuilder_bulk_test.go
@@ -18,7 +18,9 @@
 package array_test
 
 import (
+       "bytes"
        "fmt"
+       "strings"
        "testing"
 
        "github.com/apache/arrow-go/v18/arrow"
@@ -48,6 +50,80 @@ var binaryBuilderFactories = []struct {
        }},
 }
 
+func TestBinaryBuilderDirectBulkOffsetsAcrossBatches(t *testing.T) {
+       for _, factory := range binaryBuilderFactories {
+               t.Run(factory.name, func(t *testing.T) {
+                       for _, prefix := range []int{0, 1, 7, 8, 9, 31, 64} {
+                               t.Run(fmt.Sprintf("prefix_%d", prefix), func(t 
*testing.T) {
+                                       mem := 
memory.NewCheckedAllocator(memory.NewGoAllocator())
+                                       defer mem.AssertSize(t, 0)
+                                       builder := factory.new(mem)
+                                       defer builder.Release()
+
+                                       var expectedPayload []byte
+                                       var expectedOffsets []int64
+                                       var expectedValid []bool
+                                       appendExpected := func(s string, valid 
bool) {
+                                               expectedOffsets = 
append(expectedOffsets, int64(len(expectedPayload)))
+                                               expectedPayload = 
append(expectedPayload, s...)
+                                               expectedValid = 
append(expectedValid, valid)
+                                       }
+                                       for i := 0; i < prefix; i++ {
+                                               s := fmt.Sprintf("prefix_%d", i)
+                                               
appendBinaryBuilderValue(builder, s)
+                                               appendExpected(s, true)
+                                       }
+
+                                       for batch := 0; batch < 5; batch++ {
+                                               vals := make([]string, 17+batch)
+                                               valid := make([]bool, len(vals))
+                                               for i := range vals {
+                                                       if i%7 != 0 {
+                                                               vals[i] = 
strings.Repeat("a", (i+batch*3)%71)
+                                                       }
+                                                       valid[i] = (i+batch)%4 
!= 0
+                                                       appendExpected(vals[i], 
valid[i])
+                                               }
+                                               
appendBinaryBuilderValues(builder, vals, valid)
+                                       }
+
+                                       builder.AppendNulls(3)
+                                       for range 3 {
+                                               appendExpected("", false)
+                                       }
+                                       builder.AppendEmptyValues(2)
+                                       for range 2 {
+                                               appendExpected("", true)
+                                       }
+                                       appendBinaryBuilderValues(builder, 
[]string{"tail", ""}, nil)
+                                       appendExpected("tail", true)
+                                       appendExpected("", true)
+
+                                       arr := 
builder.NewArray().(array.BinaryLike)
+                                       defer arr.Release()
+                                       require.NoError(t, arr.(interface{ 
ValidateFull() error }).ValidateFull())
+                                       require.Equal(t, len(expectedOffsets), 
arr.Len())
+                                       require.True(t, 
bytes.Equal(expectedPayload, arr.ValueBytes()))
+                                       nulls := 0
+                                       for i := range expectedOffsets {
+                                               require.Equal(t, 
expectedOffsets[i], arr.ValueOffset64(i), "offset %d", i)
+                                               require.Equal(t, 
expectedValid[i], arr.IsValid(i), "valid %d", i)
+                                               if !expectedValid[i] {
+                                                       nulls++
+                                               }
+                                               end := len(expectedPayload)
+                                               if i+1 < len(expectedOffsets) {
+                                                       end = 
int(expectedOffsets[i+1])
+                                               }
+                                               require.Equal(t, 
end-int(expectedOffsets[i]), arr.ValueLen(i), "length %d", i)
+                                       }
+                                       require.Equal(t, nulls, arr.NullN())
+                               })
+                       }
+               })
+       }
+}
+
 func TestBinaryBuilderBulkAppendNullsAndEmptyValues(t *testing.T) {
        for _, tc := range binaryBuilderFactories {
                t.Run(tc.name, func(t *testing.T) {

Reply via email to