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 0a16b24c perf(arrow/array): optimize bulk appends for struct builders 
(#1189)
0a16b24c is described below

commit 0a16b24c97965de8a2ab71fe57c8832a95396e5b
Author: Minh Vu <[email protected]>
AuthorDate: Thu Aug 27 23:57:08 2026 +0200

    perf(arrow/array): optimize bulk appends for struct builders (#1189)
    
    ## Summary
    
    - reserve the struct and its children once for the full batch
    - update the parent validity bitmap and counters once
    - dispatch one bulk null or empty append to each child builder
    - preserve the existing Go behavior where null structs append null child
    values
    - add mixed nested-struct coverage and width-scaled benchmarks
    
    This uses the same bulk-dispatch orientation as [Arrow C++
    
StructBuilder](https://github.com/apache/arrow/blob/485499fd02ea2b0c323d67871fbe96aae4232504/cpp/src/arrow/array/builder_nested.h#L774-L807),
    while keeping Go child-null semantics unchanged.
    
    ## Benchmark
    
    65,536 appended structs, representative medians on Apple M1 Pro:
    
    | Fields | Operation | Before | After | Change |
    | ---: | --- | ---: | ---: | ---: |
    | 1 | nulls | 457 us | 245 us | -46% |
    | 1 | empty | 639 us | 301 us | -53% |
    | 4 | nulls | 1.40 ms | 0.82 ms | -41% |
    | 4 | empty | 1.93 ms | 1.07 ms | -44% |
    | 16 | nulls | 5.27 ms | 2.86 ms | -46% |
    | 16 | empty | 6.96 ms | 3.86 ms | -45% |
    | 64 | nulls | 17.37 ms | 11.20 ms | -36% |
    | 64 | empty | 27.31 ms | 15.25 ms | -44% |
    
    Allocations per build also drop:
    
    - 1 field: 48 to 23
    - 4 fields: 132 to 53
    - 16 fields: 470 to 175
    - 64 fields: 1,846 to about 687
    
    Single-value performance stays about the same.
    
    ## Tests
    
    - `go test -p 1 ./...`
    - `go test -race ./arrow/array -run
    TestStructBuilderBulkAppendNullsAndEmptyValues -count=1`
    - `go vet ./arrow/array`
    
    Co-authored-by: Matt Topol <[email protected]>
---
 arrow/array/struct.go           |  22 +++++--
 arrow/array/struct_bulk_test.go | 126 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 144 insertions(+), 4 deletions(-)

diff --git a/arrow/array/struct.go b/arrow/array/struct.go
index 49bcc570..a9d80835 100644
--- a/arrow/array/struct.go
+++ b/arrow/array/struct.go
@@ -407,8 +407,16 @@ func (b *StructBuilder) AppendValues(valids []bool) {
 func (b *StructBuilder) AppendNull() { b.Append(false) }
 
 func (b *StructBuilder) AppendNulls(n int) {
-       for i := 0; i < n; i++ {
-               b.AppendNull()
+       if n <= 0 {
+               return
+       }
+
+       b.Reserve(n)
+       bitutil.SetBitsTo(b.nullBitmap.Bytes(), int64(b.length), int64(n), 
false)
+       b.length += n
+       b.nulls += n
+       for _, f := range b.fields {
+               f.AppendNulls(n)
        }
 }
 
@@ -420,8 +428,14 @@ func (b *StructBuilder) AppendEmptyValue() {
 }
 
 func (b *StructBuilder) AppendEmptyValues(n int) {
-       for i := 0; i < n; i++ {
-               b.AppendEmptyValue()
+       if n <= 0 {
+               return
+       }
+
+       b.Reserve(n)
+       b.unsafeAppendBoolsToBitmap(nil, n)
+       for _, f := range b.fields {
+               f.AppendEmptyValues(n)
        }
 }
 
diff --git a/arrow/array/struct_bulk_test.go b/arrow/array/struct_bulk_test.go
new file mode 100644
index 00000000..3e7f8f7a
--- /dev/null
+++ b/arrow/array/struct_bulk_test.go
@@ -0,0 +1,126 @@
+// 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"
+)
+
+func TestStructBuilderBulkAppendNullsAndEmptyValues(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+       defer mem.AssertSize(t, 0)
+
+       nestedType := arrow.StructOf(arrow.Field{Name: "value", Type: 
arrow.PrimitiveTypes.Int32})
+       dtype := arrow.StructOf(
+               arrow.Field{Name: "integer", Type: arrow.PrimitiveTypes.Int64},
+               arrow.Field{Name: "string", Type: arrow.BinaryTypes.String},
+               arrow.Field{Name: "boolean", Type: 
arrow.FixedWidthTypes.Boolean},
+               arrow.Field{Name: "nested", Type: nestedType},
+       )
+       builder := array.NewStructBuilder(mem, dtype)
+       defer builder.Release()
+
+       appendStructBuilderValue(builder, 10, "a", true, 100)
+       builder.AppendNulls(5)
+       builder.AppendEmptyValues(4)
+       appendStructBuilderValue(builder, 20, "b", false, 200)
+
+       arr := builder.NewStructArray()
+       defer arr.Release()
+       require.NoError(t, arr.ValidateFull())
+       require.Equal(t, 11, arr.Len())
+       require.Equal(t, 5, arr.NullN())
+
+       for i := range arr.Len() {
+               wantValid := i == 0 || i >= 6
+               assert.Equal(t, wantValid, arr.IsValid(i), "parent value %d", i)
+               for field := 0; field < arr.NumField(); field++ {
+                       assert.Equal(t, wantValid, arr.Field(field).IsValid(i), 
"field %d value %d", field, i)
+               }
+       }
+
+       assert.Equal(t, []int64{10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20}, 
arr.Field(0).(*array.Int64).Int64Values())
+       assert.Equal(t, "a", arr.Field(1).(*array.String).Value(0))
+       assert.Equal(t, "b", arr.Field(1).(*array.String).Value(10))
+       for i := 1; i < 10; i++ {
+               assert.Empty(t, arr.Field(1).(*array.String).Value(i))
+       }
+
+       nested := arr.Field(3).(*array.Struct)
+       assert.Equal(t, 5, nested.NullN())
+       nestedValues := nested.Field(0).(*array.Int32)
+       assert.Equal(t, []int32{100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 200}, 
nestedValues.Int32Values())
+       for i := range nestedValues.Len() {
+               assert.Equal(t, i == 0 || i >= 6, nestedValues.IsValid(i), 
"nested value %d", i)
+       }
+}
+
+func appendStructBuilderValue(builder *array.StructBuilder, integer int64, str 
string, boolean bool, nestedValue int32) {
+       builder.Append(true)
+       builder.FieldBuilder(0).(*array.Int64Builder).Append(integer)
+       builder.FieldBuilder(1).(*array.StringBuilder).Append(str)
+       builder.FieldBuilder(2).(*array.BooleanBuilder).Append(boolean)
+       nested := builder.FieldBuilder(3).(*array.StructBuilder)
+       nested.Append(true)
+       nested.FieldBuilder(0).(*array.Int32Builder).Append(nestedValue)
+}
+
+func BenchmarkStructBuilderBulkAppend(b *testing.B) {
+       for _, fields := range []int{1, 4, 16, 64} {
+               b.Run(fmt.Sprintf("fields_%d", fields), func(b *testing.B) {
+                       for _, rows := range []int{1, 1024, 65536} {
+                               b.Run(fmt.Sprintf("rows_%d", rows), func(b 
*testing.B) {
+                                       b.Run("nulls", func(b *testing.B) {
+                                               
benchmarkStructBuilderBulkAppend(b, fields, rows, false)
+                                       })
+                                       b.Run("empty", func(b *testing.B) {
+                                               
benchmarkStructBuilderBulkAppend(b, fields, rows, true)
+                                       })
+                               })
+                       }
+               })
+       }
+}
+
+func benchmarkStructBuilderBulkAppend(b *testing.B, fields, rows int, empty 
bool) {
+       structFields := make([]arrow.Field, fields)
+       for i := range structFields {
+               structFields[i] = arrow.Field{Name: fmt.Sprintf("field_%d", i), 
Type: arrow.PrimitiveTypes.Int64}
+       }
+       builder := array.NewStructBuilder(memory.DefaultAllocator, 
arrow.StructOf(structFields...))
+       defer builder.Release()
+       b.ReportAllocs()
+
+       for b.Loop() {
+               if empty {
+                       builder.AppendEmptyValues(rows)
+               } else {
+                       builder.AppendNulls(rows)
+               }
+
+               arr := builder.NewStructArray()
+               arr.Release()
+       }
+}

Reply via email to