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 f554e65f perf(arrow/array): append dictionary indices directly (#1313)
f554e65f is described below

commit f554e65fcf1522177b2f6cf1c8c4a29b1311a599
Author: Minh Vu <[email protected]>
AuthorDate: Fri Sep 18 22:20:20 2026 +0200

    perf(arrow/array): append dictionary indices directly (#1313)
    
    ## What does this change?
    
    - `DictionaryBuilder.AppendIndices` used to allocate a new typed slice
    on every call.
    - Convert the input values directly into the final typed index buffer.
    - Keep the existing bulk validity bitmap path.
    - Add coverage for all eight index types, nullable appends, split
    appends, and invalid validity lengths.
    - No public API changes.
    
    ## Benchmark
    
    Command:
    
    ```
    go test ./arrow/array -run '^$' -bench 
'^BenchmarkDictionaryBuilderAppendIndices$' -benchmem -benchtime=300ms -count=8
    ```
    
    Apple M1 Pro, Go 1.26.3. The benchmark uses 65,536 indices and a
    256-entry dictionary.
    
    | Case | Before ns/op | After ns/op | Before B/op | After B/op | Allocs
    |
    | --- | ---: | ---: | ---: | ---: | ---: |
    | int32, non-null | 110,370 | 75,398 | 815,908 | 553,762 | 18 -> 17 |
    | int32, nullable | 116,578 | 75,895 | 815,907 | 553,762 | 18 -> 17 |
    
    This is about 32% less time and allocation bytes for the non-null case,
    and about 35% less time for the nullable case.
    
    The Parquet decoder's separate `[]uint64 -> []int` scratch conversion is
    intentionally outside this PR.
    
    ## Tests
    
    - `go test ./...`
    - `go test -race ./arrow/array`
    - `go vet ./arrow/array`
---
 arrow/array/dictionary.go                          | 73 ++++++++---------
 .../dictionary_append_indices_benchmark_test.go    | 91 ++++++++++++++++++++++
 arrow/array/dictionary_test.go                     | 37 +++++++++
 3 files changed, 160 insertions(+), 41 deletions(-)

diff --git a/arrow/array/dictionary.go b/arrow/array/dictionary.go
index fe7d66be..19661cca 100644
--- a/arrow/array/dictionary.go
+++ b/arrow/array/dictionary.go
@@ -518,6 +518,12 @@ type IndexBuilder struct {
        UnsafeAppend func(int)
 }
 
+func appendDictionaryIndices[T arrow.IntType | arrow.UintType](dst []T, 
indices []int) {
+       for i, idx := range indices {
+               dst[i] = T(idx)
+       }
+}
+
 func createIndexBuilder(mem memory.Allocator, dt arrow.FixedWidthDataType) 
(ret IndexBuilder, err error) {
        ret = IndexBuilder{Builder: NewBuilder(mem, dt)}
        switch dt.ID() {
@@ -1181,57 +1187,42 @@ func (b *dictionaryBuilder) IndexBuilder() IndexBuilder 
{
 }
 
 func (b *dictionaryBuilder) AppendIndices(indices []int, valid []bool) {
-       b.length += len(indices)
+       if len(indices) != len(valid) && len(valid) != 0 {
+               panic("len(indices) != len(valid) && len(valid) != 0")
+       }
+
+       if len(indices) == 0 {
+               return
+       }
+
+       b.idxBuilder.Reserve(len(indices))
        switch idxbldr := b.idxBuilder.Builder.(type) {
        case *Int8Builder:
-               vals := make([]int8, len(indices))
-               for i, v := range indices {
-                       vals[i] = int8(v)
-               }
-               idxbldr.AppendValues(vals, valid)
+               appendDictionaryIndices(idxbldr.rawData[idxbldr.length:], 
indices)
+               idxbldr.unsafeAppendBoolsToBitmap(valid, len(indices))
        case *Int16Builder:
-               vals := make([]int16, len(indices))
-               for i, v := range indices {
-                       vals[i] = int16(v)
-               }
-               idxbldr.AppendValues(vals, valid)
+               appendDictionaryIndices(idxbldr.rawData[idxbldr.length:], 
indices)
+               idxbldr.unsafeAppendBoolsToBitmap(valid, len(indices))
        case *Int32Builder:
-               vals := make([]int32, len(indices))
-               for i, v := range indices {
-                       vals[i] = int32(v)
-               }
-               idxbldr.AppendValues(vals, valid)
+               appendDictionaryIndices(idxbldr.rawData[idxbldr.length:], 
indices)
+               idxbldr.unsafeAppendBoolsToBitmap(valid, len(indices))
        case *Int64Builder:
-               vals := make([]int64, len(indices))
-               for i, v := range indices {
-                       vals[i] = int64(v)
-               }
-               idxbldr.AppendValues(vals, valid)
+               appendDictionaryIndices(idxbldr.rawData[idxbldr.length:], 
indices)
+               idxbldr.unsafeAppendBoolsToBitmap(valid, len(indices))
        case *Uint8Builder:
-               vals := make([]uint8, len(indices))
-               for i, v := range indices {
-                       vals[i] = uint8(v)
-               }
-               idxbldr.AppendValues(vals, valid)
+               appendDictionaryIndices(idxbldr.rawData[idxbldr.length:], 
indices)
+               idxbldr.unsafeAppendBoolsToBitmap(valid, len(indices))
        case *Uint16Builder:
-               vals := make([]uint16, len(indices))
-               for i, v := range indices {
-                       vals[i] = uint16(v)
-               }
-               idxbldr.AppendValues(vals, valid)
+               appendDictionaryIndices(idxbldr.rawData[idxbldr.length:], 
indices)
+               idxbldr.unsafeAppendBoolsToBitmap(valid, len(indices))
        case *Uint32Builder:
-               vals := make([]uint32, len(indices))
-               for i, v := range indices {
-                       vals[i] = uint32(v)
-               }
-               idxbldr.AppendValues(vals, valid)
+               appendDictionaryIndices(idxbldr.rawData[idxbldr.length:], 
indices)
+               idxbldr.unsafeAppendBoolsToBitmap(valid, len(indices))
        case *Uint64Builder:
-               vals := make([]uint64, len(indices))
-               for i, v := range indices {
-                       vals[i] = uint64(v)
-               }
-               idxbldr.AppendValues(vals, valid)
+               appendDictionaryIndices(idxbldr.rawData[idxbldr.length:], 
indices)
+               idxbldr.unsafeAppendBoolsToBitmap(valid, len(indices))
        }
+       b.length += len(indices)
 }
 
 func (b *dictionaryBuilder) DictionarySize() int {
diff --git a/arrow/array/dictionary_append_indices_benchmark_test.go 
b/arrow/array/dictionary_append_indices_benchmark_test.go
new file mode 100644
index 00000000..5850fe9d
--- /dev/null
+++ b/arrow/array/dictionary_append_indices_benchmark_test.go
@@ -0,0 +1,91 @@
+// 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"
+       "strconv"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+)
+
+func BenchmarkDictionaryBuilderAppendIndices(b *testing.B) {
+       const (
+               length      = 1 << 16
+               cardinality = 1 << 7
+       )
+
+       indices := make([]int, length)
+       valid := make([]bool, length)
+       for i := range indices {
+               indices[i] = i % cardinality
+               valid[i] = i%10 != 0
+       }
+
+       dictionaryValues := make([]string, cardinality)
+       for i := range dictionaryValues {
+               dictionaryValues[i] = strconv.Itoa(i)
+       }
+
+       indexTypes := []arrow.DataType{
+               arrow.PrimitiveTypes.Int8,
+               arrow.PrimitiveTypes.Uint8,
+               arrow.PrimitiveTypes.Int16,
+               arrow.PrimitiveTypes.Uint16,
+               arrow.PrimitiveTypes.Int32,
+               arrow.PrimitiveTypes.Uint32,
+               arrow.PrimitiveTypes.Int64,
+               arrow.PrimitiveTypes.Uint64,
+       }
+
+       for _, indexType := range indexTypes {
+               indexType := indexType
+               b.Run(fmt.Sprintf("%s/non-null", indexType), func(b *testing.B) 
{
+                       benchmarkDictionaryBuilderAppendIndices(b, indexType, 
indices, nil, dictionaryValues)
+               })
+               b.Run(fmt.Sprintf("%s/nullable", indexType), func(b *testing.B) 
{
+                       benchmarkDictionaryBuilderAppendIndices(b, indexType, 
indices, valid, dictionaryValues)
+               })
+       }
+}
+
+func benchmarkDictionaryBuilderAppendIndices(b *testing.B, indexType 
arrow.DataType, indices []int, valid []bool, dictionaryValues []string) {
+       mem := memory.NewGoAllocator()
+       dictBuilder := array.NewStringBuilder(mem)
+       dictBuilder.AppendValues(dictionaryValues, nil)
+       dictionary := dictBuilder.NewStringArray()
+       dictBuilder.Release()
+       defer dictionary.Release()
+
+       builder := array.NewDictionaryBuilderWithDict(mem, 
&arrow.DictionaryType{
+               IndexType: indexType,
+               ValueType: arrow.BinaryTypes.String,
+       }, dictionary)
+       defer builder.Release()
+
+       b.ReportAllocs()
+       b.SetBytes(int64(len(indices) * 
indexType.(arrow.FixedWidthDataType).Bytes()))
+       b.ResetTimer()
+       for b.Loop() {
+               builder.AppendIndices(indices, valid)
+               arr := builder.NewDictionaryArray()
+               arr.Release()
+       }
+}
diff --git a/arrow/array/dictionary_test.go b/arrow/array/dictionary_test.go
index 2324b743..e6335189 100644
--- a/arrow/array/dictionary_test.go
+++ b/arrow/array/dictionary_test.go
@@ -1946,6 +1946,43 @@ func TestDictionaryAppendIndices(t *testing.T) {
                        assert.Equal(t, fmt.Sprint(indices), 
arrIndices.String())
                })
        }
+
+       valid := []bool{true, false, true, false, true, true, false, true}
+       for _, typ := range indexTypes {
+               t.Run(fmt.Sprintf("%s with validity", typ), func(t *testing.T) {
+                       scoped := memory.NewCheckedAllocatorScope(mem)
+                       defer scoped.CheckSize(t)
+
+                       dictType := &arrow.DictionaryType{
+                               IndexType: typ, ValueType: dict.DataType()}
+                       bldr := array.NewDictionaryBuilderWithDict(mem, 
dictType, dict)
+                       defer bldr.Release()
+
+                       bldr.AppendIndices(indices[:3], valid[:3])
+                       bldr.AppendIndices(indices[3:], valid[3:])
+
+                       arr := bldr.NewDictionaryArray()
+                       defer arr.Release()
+
+                       assert.EqualValues(t, len(indices), arr.Len())
+                       assert.EqualValues(t, 3, arr.NullN())
+                       for i, wantValid := range valid {
+                               assert.Equal(t, !wantValid, arr.IsNull(i))
+                               assert.Equal(t, indices[i], 
arr.GetValueIndex(i))
+                       }
+               })
+       }
+
+       t.Run("validity length mismatch", func(t *testing.T) {
+               dictType := &arrow.DictionaryType{
+                       IndexType: arrow.PrimitiveTypes.Int32, ValueType: 
dict.DataType()}
+               bldr := array.NewDictionaryBuilderWithDict(mem, dictType, dict)
+               defer bldr.Release()
+
+               assert.PanicsWithValue(t, "len(indices) != len(valid) && 
len(valid) != 0", func() {
+                       bldr.AppendIndices([]int{0}, []bool{true, false})
+               })
+       })
 }
 
 type panicAllocator struct {

Reply via email to