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 db47815d perf(arrow/array): batch union null and empty appends (#1216)
db47815d is described below

commit db47815d919ffdec0f9433365180d1a881406262
Author: Minh Vu <[email protected]>
AuthorDate: Fri Aug 28 22:15:56 2026 +0200

    perf(arrow/array): batch union null and empty appends (#1216)
    
    ## Summary
    
    - **Batch sparse union appends** through the child bulk APIs.
    - **Write repeated type IDs and dense offsets** directly into their
    buffers.
    - **Keep dense unions compact** by sharing one physical null or empty
    child value.
    - **Treat non-positive counts as no-ops** for both union modes.
    - Add parity, edge-case, and benchmark coverage.
    
    ## Benchmarks
    
    Apple M1 Pro, darwin/arm64, 65,536 rows. Times are rounded medians from
    3 runs.
    
    | Mode | Operation | Before | After | Before allocs | After allocs |
    | --- | --- | ---: | ---: | ---: | ---: |
    | Sparse | nulls | ~1.83 ms | ~0.35 ms | 44 | 34 |
    | Sparse | empty | ~1.83 ms | ~0.33 ms | 44 | 34 |
    | Dense | nulls | ~0.55 ms | ~0.14 ms | 51 | 29 |
    | Dense | empty | ~0.39 ms | ~0.14 ms | ~51 | 29 |
    
    The benchmark uses the same builder setup before and after. Small row
    counts stay close to benchmark noise.
    
    ## Tests
    
    - `go test ./arrow/array -count=1`
    - `go test -race ./arrow/array -run
    
'Test(UnionBuilderBulkAppendNullsAndEmptyValues|UnionBuilderZeroBulkAppendDoesNotMutateChildren|UnionBuilderBulkAppendMatchesScalar)$'
    -count=1`
    - `go vet ./arrow/array`
    - `PARQUET_TEST_DATA="$PWD/parquet-testing/data" go test -count=1 ./...`
---
 arrow/array/union.go                   |  68 ++++++---
 arrow/array/union_builder_bulk_test.go | 257 +++++++++++++++++++++++++++++++++
 2 files changed, 306 insertions(+), 19 deletions(-)

diff --git a/arrow/array/union.go b/arrow/array/union.go
index e6137eae..061845b9 100644
--- a/arrow/array/union.go
+++ b/arrow/array/union.go
@@ -934,6 +934,34 @@ func (b *unionBuilder) newData() *Data {
        return NewData(b.Type(), length, []*memory.Buffer{nil, typesBuffer}, 
childData, 0, 0)
 }
 
+func unsafeAppendRepeatedInt8(b *int8BufferBuilder, value int8, n int) {
+       if n <= 0 {
+               return
+       }
+
+       end := b.length + n
+       if b.capacity < end {
+               b.resize(bitutil.NextPowerOf2(end))
+       }
+       memory.Set(b.bytes[b.length:end], byte(value))
+       b.length = end
+}
+
+func unsafeAppendRepeatedInt32(b *int32BufferBuilder, value int32, n int) {
+       if n <= 0 {
+               return
+       }
+
+       end := b.length + n*arrow.Int32SizeBytes
+       if b.capacity < end {
+               b.resize(bitutil.NextPowerOf2(end))
+       }
+       for i := b.length; i < end; i += arrow.Int32SizeBytes {
+               arrow.Int32Traits.PutValue(b.bytes[i:], value)
+       }
+       b.length = end
+}
+
 // SparseUnionBuilder is used to build a Sparse Union array using the Append
 // methods. You can also add new types to the union on the fly by using
 // AppendChild.
@@ -1002,17 +1030,20 @@ func (b *SparseUnionBuilder) AppendNull() {
 // AppendNulls is identical to calling AppendNull() n times, except
 // it will pre-allocate with reserve for all the nulls beforehand.
 func (b *SparseUnionBuilder) AppendNulls(n int) {
+       if n <= 0 {
+               return
+       }
+
        firstChildCode := b.codes[0]
        b.Reserve(n)
        for _, c := range b.codes {
                b.typeIDtoBuilder[c].Reserve(n)
        }
-       for i := 0; i < n; i++ {
-               b.typesBuilder.AppendValue(firstChildCode)
-               b.typeIDtoBuilder[firstChildCode].AppendNull()
-               for _, c := range b.codes[1:] {
-                       b.typeIDtoBuilder[c].AppendEmptyValue()
-               }
+
+       unsafeAppendRepeatedInt8(b.typesBuilder, firstChildCode, n)
+       b.typeIDtoBuilder[firstChildCode].AppendNulls(n)
+       for _, c := range b.codes[1:] {
+               b.typeIDtoBuilder[c].AppendEmptyValues(n)
        }
 }
 
@@ -1029,16 +1060,19 @@ func (b *SparseUnionBuilder) AppendEmptyValue() {
 // AppendEmptyValues is identical to calling AppendEmptyValue() n times,
 // except it pre-allocates first so it is more efficient.
 func (b *SparseUnionBuilder) AppendEmptyValues(n int) {
+       if n <= 0 {
+               return
+       }
+
        b.Reserve(n)
        firstChildCode := b.codes[0]
        for _, c := range b.codes {
                b.typeIDtoBuilder[c].Reserve(n)
        }
-       for i := 0; i < n; i++ {
-               b.typesBuilder.AppendValue(firstChildCode)
-               for _, c := range b.codes {
-                       b.typeIDtoBuilder[c].AppendEmptyValue()
-               }
+
+       unsafeAppendRepeatedInt8(b.typesBuilder, firstChildCode, n)
+       for _, c := range b.codes {
+               b.typeIDtoBuilder[c].AppendEmptyValues(n)
        }
 }
 
@@ -1249,10 +1283,8 @@ func (b *DenseUnionBuilder) AppendNulls(n int) {
        firstChildCode := b.codes[0]
        childBuilder := b.typeIDtoBuilder[firstChildCode]
        b.Reserve(n)
-       for i := 0; i < n; i++ {
-               b.typesBuilder.AppendValue(firstChildCode)
-               b.offsetsBuilder.AppendValue(int32(childBuilder.Len()))
-       }
+       unsafeAppendRepeatedInt8(b.typesBuilder, firstChildCode, n)
+       unsafeAppendRepeatedInt32(b.offsetsBuilder, int32(childBuilder.Len()), 
n)
        // only append a single null to the child builder, the offsets all 
refer to the same value
        childBuilder.AppendNull()
 }
@@ -1280,10 +1312,8 @@ func (b *DenseUnionBuilder) AppendEmptyValues(n int) {
        firstChildCode := b.codes[0]
        childBuilder := b.typeIDtoBuilder[firstChildCode]
        b.Reserve(n)
-       for i := 0; i < n; i++ {
-               b.typesBuilder.AppendValue(firstChildCode)
-               b.offsetsBuilder.AppendValue(int32(childBuilder.Len()))
-       }
+       unsafeAppendRepeatedInt8(b.typesBuilder, firstChildCode, n)
+       unsafeAppendRepeatedInt32(b.offsetsBuilder, int32(childBuilder.Len()), 
n)
        // only append a single empty value to the child builder, the offsets 
all
        // refer to the same value
        childBuilder.AppendEmptyValue()
diff --git a/arrow/array/union_builder_bulk_test.go 
b/arrow/array/union_builder_bulk_test.go
new file mode 100644
index 00000000..8922871f
--- /dev/null
+++ b/arrow/array/union_builder_bulk_test.go
@@ -0,0 +1,257 @@
+// 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 bulkUnionBuilderFactory func(memory.Allocator) array.UnionBuilder
+
+var bulkUnionBuilderFactories = []struct {
+       name string
+       new  bulkUnionBuilderFactory
+}{
+       {"sparse", func(mem memory.Allocator) array.UnionBuilder {
+               return array.NewSparseUnionBuilder(mem, 
bulkUnionType(arrow.SparseMode).(*arrow.SparseUnionType))
+       }},
+       {"dense", func(mem memory.Allocator) array.UnionBuilder {
+               return array.NewDenseUnionBuilder(mem, 
bulkUnionType(arrow.DenseMode).(*arrow.DenseUnionType))
+       }},
+}
+
+func bulkUnionType(mode arrow.UnionMode) arrow.UnionType {
+       fields := []arrow.Field{
+               {Name: "str", Type: arrow.BinaryTypes.String, Nullable: true},
+               {Name: "i32", Type: arrow.PrimitiveTypes.Int32, Nullable: true},
+               {Name: "f64", Type: arrow.PrimitiveTypes.Float64, Nullable: 
true},
+       }
+       codes := []arrow.UnionTypeCode{8, 13, 7}
+       return arrow.UnionOf(mode, fields, codes)
+}
+
+func TestUnionBuilderBulkAppendNullsAndEmptyValues(t *testing.T) {
+       for _, tc := range bulkUnionBuilderFactories {
+               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()
+
+                       builder.AppendNulls(0)
+                       builder.AppendEmptyValues(0)
+                       builder.AppendNulls(-1)
+                       builder.AppendEmptyValues(-1)
+                       require.Zero(t, builder.Len())
+                       require.Zero(t, builder.Cap())
+
+                       appendBulkUnionPrefix(builder, 3)
+                       builder.AppendNulls(5)
+                       builder.AppendEmptyValues(4)
+
+                       result := builder.NewArray().(array.Union)
+                       defer result.Release()
+                       require.NoError(t, result.ValidateFull())
+
+                       assert.Equal(t, 12, result.Len())
+                       assert.Equal(t, []arrow.UnionTypeCode{8, 13, 7, 8, 8, 
8, 8, 8, 8, 8, 8, 8}, result.RawTypeCodes())
+               })
+       }
+}
+
+func TestUnionBuilderZeroBulkAppendDoesNotMutateChildren(t *testing.T) {
+       tests := []struct {
+               name string
+               new  func(memory.Allocator) array.UnionBuilder
+       }{
+               {"sparse", func(mem memory.Allocator) array.UnionBuilder {
+                       return array.NewEmptySparseUnionBuilder(mem)
+               }},
+               {"dense", func(mem memory.Allocator) array.UnionBuilder {
+                       return array.NewEmptyDenseUnionBuilder(mem)
+               }},
+       }
+
+       for _, tc := range tests {
+               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()
+
+                       builder.AppendNulls(0)
+                       builder.AppendEmptyValues(0)
+                       builder.AppendNulls(-1)
+                       builder.AppendEmptyValues(-1)
+
+                       assert.Zero(t, builder.Len())
+                       assert.Zero(t, builder.Cap())
+               })
+       }
+}
+
+func TestUnionBuilderBulkAppendMatchesScalar(t *testing.T) {
+       starts := []int{0, 1, 2, 7, 8, 15}
+       batchSizes := []int{-1, 0, 1, 2, 7, 16, 17}
+       operations := []struct {
+               name   string
+               bulk   func(array.UnionBuilder, int)
+               scalar func(array.UnionBuilder, int)
+       }{
+               {
+                       name: "nulls",
+                       bulk: func(builder array.UnionBuilder, n int) {
+                               builder.AppendNulls(n)
+                       },
+                       scalar: func(builder array.UnionBuilder, n int) {
+                               for i := 0; i < n; i++ {
+                                       builder.AppendNull()
+                               }
+                       },
+               },
+               {
+                       name: "empty_values",
+                       bulk: func(builder array.UnionBuilder, n int) {
+                               builder.AppendEmptyValues(n)
+                       },
+                       scalar: func(builder array.UnionBuilder, n int) {
+                               for i := 0; i < n; i++ {
+                                       builder.AppendEmptyValue()
+                               }
+                       },
+               },
+       }
+
+       for _, factory := range bulkUnionBuilderFactories {
+               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()
+
+                                                               
appendBulkUnionPrefix(bulk, start)
+                                                               
appendBulkUnionPrefix(scalar, start)
+                                                               
operation.bulk(bulk, batchSize)
+                                                               
operation.scalar(scalar, batchSize)
+                                                               
assertUnionBuilderArrayParity(t, bulk, scalar)
+
+                                                               
appendBulkUnionPrefix(bulk, start)
+                                                               
appendBulkUnionPrefix(scalar, start)
+                                                               
reuseOperation.bulk(bulk, 9)
+                                                               
reuseOperation.scalar(scalar, 9)
+                                                               
assertUnionBuilderArrayParity(t, bulk, scalar)
+                                                       })
+                                               }
+                                       }
+                               })
+                       }
+               })
+       }
+}
+
+func appendBulkUnionPrefix(builder array.UnionBuilder, n int) {
+       codes := []arrow.UnionTypeCode{8, 13, 7}
+       for i := 0; i < n; i++ {
+               childID := i % len(codes)
+               builder.Append(codes[childID])
+
+               switch childID {
+               case 0:
+                       
builder.Child(childID).(*array.StringBuilder).Append(fmt.Sprintf("value-%d", i))
+               case 1:
+                       
builder.Child(childID).(*array.Int32Builder).Append(int32(i))
+               case 2:
+                       
builder.Child(childID).(*array.Float64Builder).Append(float64(i))
+               }
+
+               if builder.Mode() == arrow.SparseMode {
+                       for i := 0; i < len(codes); i++ {
+                               if i != childID {
+                                       builder.Child(i).AppendEmptyValue()
+                               }
+                       }
+               }
+       }
+}
+
+func assertUnionBuilderArrayParity(t *testing.T, bulk, scalar 
array.UnionBuilder) {
+       t.Helper()
+
+       assert.Equal(t, scalar.Len(), bulk.Len())
+
+       bulkArray := bulk.NewArray().(array.Union)
+       defer bulkArray.Release()
+       scalarArray := scalar.NewArray().(array.Union)
+       defer scalarArray.Release()
+
+       require.NoError(t, bulkArray.ValidateFull())
+       require.NoError(t, scalarArray.ValidateFull())
+       assert.True(t, array.Equal(bulkArray, scalarArray))
+}
+
+func BenchmarkUnionBuilderBulkAppend(b *testing.B) {
+       for _, tc := range bulkUnionBuilderFactories {
+               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) {
+                                               
benchmarkUnionBuilderBulkAppend(b, tc.new, rows, false)
+                                       })
+                                       b.Run("empty", func(b *testing.B) {
+                                               
benchmarkUnionBuilderBulkAppend(b, tc.new, rows, true)
+                                       })
+                               })
+                       }
+               })
+       }
+}
+
+func benchmarkUnionBuilderBulkAppend(b *testing.B, factory 
bulkUnionBuilderFactory, 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