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 9c86a0cf perf(arrow/array): copy view buffers directly (#1324)
9c86a0cf is described below

commit 9c86a0cf736b1eb7642d2bec6507a19c5b4e9806
Author: Minh Vu <[email protected]>
AuthorDate: Fri Sep 25 18:01:49 2026 +0200

    perf(arrow/array): copy view buffers directly (#1324)
    
    **What**
    
    - Copy BinaryView headers and ListView sizes directly from `ArrayData`
    during concatenation.
    
    **Why**
    
    - Avoid one temporary `memory.Buffer` wrapper per input chunk.
    - Reduce allocations for fragmented view arrays.
    
    **Implementation**
    
    - Use the existing `concatFixedWidthBuffers` helper for BinaryView
    headers and ListView/LargeListView sizes.
    - Added a benchmark for 64, 1,024, and 8,192 chunks.
    - Existing concatenate tests cover sliced and nullable view arrays.
    
    Benchmark on an Apple M1 Pro:
    
    | Case | Runtime before -> after | Allocations before -> after |
    | --- | ---: | ---: |
    | BinaryView, 1,024 chunks | 1.02ms -> 0.93ms | 1,050 -> 25 |
    | BinaryView, 8,192 chunks | roughly flat | 8,227 -> 34 |
    | ListView, 8,192 chunks | 2.27ms -> 1.92ms | 26,477 -> 18,388 |
    | LargeListView, 8,192 chunks | 2.28ms -> 1.95ms | 26,124 -> 17,304 |
    
    Tests:
    
    - `go test ./arrow/array -count=1`
    - `go vet ./arrow/array`
    - `git diff --check`
---
 arrow/array/concat.go                   |  12 +++-
 arrow/array/concat_test.go              | 102 ++++++++++++++++++++++++++++++++
 arrow/array/concat_view_failure_test.go |  59 ++++++++++++++++++
 3 files changed, 170 insertions(+), 3 deletions(-)

diff --git a/arrow/array/concat.go b/arrow/array/concat.go
index a138717e..4079208c 100644
--- a/arrow/array/concat.go
+++ b/arrow/array/concat.go
@@ -154,6 +154,12 @@ func concatBuffers(bufs []*memory.Buffer, mem 
memory.Allocator) *memory.Buffer {
 
 func concatFixedWidthBuffers(data []arrow.ArrayData, idx, byteWidth, length 
int, mem memory.Allocator) *memory.Buffer {
        out := memory.NewResizableBuffer(mem)
+       success := false
+       defer func() {
+               if !success {
+                       out.Release()
+               }
+       }()
        out.Resize(length * byteWidth)
        dst := out.Bytes()
        for _, d := range data {
@@ -167,6 +173,7 @@ func concatFixedWidthBuffers(data []arrow.ArrayData, idx, 
byteWidth, length int,
                copy(dst, buf.Bytes()[begin:begin+nbytes])
                dst = dst[nbytes:]
        }
+       success = true
        return out
 }
 
@@ -611,8 +618,7 @@ func concatListView(data []arrow.ArrayData, offsetType 
arrow.FixedWidthDataType,
        }
 
        // Concatenate the sizes
-       sizeBuffers := gatherBuffersFixedWidthType(data, 2, offsetType)
-       sizeBuffer := concatBuffers(sizeBuffers, mem)
+       sizeBuffer := concatFixedWidthBuffers(data, 2, offsetType.Bytes(), 
out.length, mem)
 
        out.childData = []arrow.ArrayData{values}
        out.buffers[1] = offsetBuffer
@@ -720,7 +726,7 @@ func concat(data []arrow.ArrayData, mem memory.Allocator) 
(arr arrow.ArrayData,
                        }
                }
 
-               out.buffers[1] = concatBuffers(gatherFixedBuffers(data, 1, 
arrow.ViewHeaderSizeBytes), mem)
+               out.buffers[1] = concatFixedWidthBuffers(data, 1, 
arrow.ViewHeaderSizeBytes, out.length, mem)
 
                var (
                        s                  = 
arrow.ViewHeaderTraits.CastFromBytes(out.buffers[1].Bytes())
diff --git a/arrow/array/concat_test.go b/arrow/array/concat_test.go
index 1eabbac7..e1831e26 100644
--- a/arrow/array/concat_test.go
+++ b/arrow/array/concat_test.go
@@ -141,6 +141,108 @@ func BenchmarkConcatenateFixedWidth(b *testing.B) {
        }
 }
 
+func BenchmarkConcatenateViews(b *testing.B) {
+       const totalValues = 1 << 16
+
+       makeBinaryView := func(mem memory.Allocator) arrow.Array {
+               builder := array.NewBinaryViewBuilder(mem)
+               value := []byte("01234567890123456789012345678901")
+               builder.Reserve(totalValues)
+               for i := 0; i < totalValues; i++ {
+                       builder.Append(value)
+               }
+               result := builder.NewArray()
+               builder.Release()
+               return result
+       }
+
+       makeListView := func(mem memory.Allocator, large bool) arrow.Array {
+               valid := make([]bool, totalValues)
+               values := make([]int8, totalValues)
+               for i := range valid {
+                       valid[i] = true
+               }
+
+               var builder array.VarLenListLikeBuilder
+               if large {
+                       builder = array.NewLargeListViewBuilder(mem, 
arrow.PrimitiveTypes.Int8)
+                       offsets := make([]int64, totalValues)
+                       sizes := make([]int64, totalValues)
+                       for i := range offsets {
+                               offsets[i] = int64(i)
+                               sizes[i] = 1
+                       }
+                       
builder.(*array.LargeListViewBuilder).AppendValuesWithSizes(offsets, sizes, 
valid)
+               } else {
+                       builder = array.NewListViewBuilder(mem, 
arrow.PrimitiveTypes.Int8)
+                       offsets := make([]int32, totalValues)
+                       sizes := make([]int32, totalValues)
+                       for i := range offsets {
+                               offsets[i] = int32(i)
+                               sizes[i] = 1
+                       }
+                       
builder.(*array.ListViewBuilder).AppendValuesWithSizes(offsets, sizes, valid)
+               }
+               
builder.ValueBuilder().(*array.Int8Builder).AppendValues(values, nil)
+               result := builder.NewArray()
+               builder.Release()
+               return result
+       }
+
+       tests := []struct {
+               name string
+               make func(memory.Allocator) arrow.Array
+       }{
+               {"binary_view", makeBinaryView},
+               {"list_view", func(mem memory.Allocator) arrow.Array {
+                       return makeListView(mem, false)
+               }},
+               {"large_list_view", func(mem memory.Allocator) arrow.Array {
+                       return makeListView(mem, true)
+               }},
+       }
+
+       for _, tt := range tests {
+               tt := tt
+               b.Run(tt.name, func(b *testing.B) {
+                       mem := memory.NewGoAllocator()
+                       backing := tt.make(mem)
+                       defer backing.Release()
+
+                       for _, chunkCount := range []int{64, 1024, 8192} {
+                               chunkCount := chunkCount
+                               b.Run(fmt.Sprintf("chunks=%d", chunkCount), 
func(b *testing.B) {
+                                       chunkSize := totalValues / chunkCount
+                                       inputs := make([]arrow.Array, 
chunkCount)
+                                       for i := range inputs {
+                                               begin := int64(i * chunkSize)
+                                               inputs[i] = 
array.NewSlice(backing, begin, begin+int64(chunkSize))
+                                       }
+                                       defer func() {
+                                               for _, input := range inputs {
+                                                       input.Release()
+                                               }
+                                       }()
+
+                                       b.SetBytes(int64(totalValues))
+                                       b.ReportAllocs()
+                                       b.ResetTimer()
+                                       for i := 0; i < b.N; i++ {
+                                               result, err := 
array.Concatenate(inputs, mem)
+                                               if err != nil {
+                                                       b.Fatal(err)
+                                               }
+                                               if result.Len() != totalValues {
+                                                       b.Fatalf("result length 
= %d, want %d", result.Len(), totalValues)
+                                               }
+                                               result.Release()
+                                       }
+                               })
+                       }
+               })
+       }
+}
+
 func TestConcatenateFixedWidthSlices(t *testing.T) {
        mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
        defer mem.AssertSize(t, 0)
diff --git a/arrow/array/concat_view_failure_test.go 
b/arrow/array/concat_view_failure_test.go
new file mode 100644
index 00000000..1858209e
--- /dev/null
+++ b/arrow/array/concat_view_failure_test.go
@@ -0,0 +1,59 @@
+// 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
+
+import (
+       "fmt"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/stretchr/testify/require"
+)
+
+func TestConcatViewShortHeaderReleasesDestination(t *testing.T) {
+       for _, dtype := range []arrow.DataType{&arrow.BinaryViewType{}, 
&arrow.StringViewType{}} {
+               for _, tc := range []struct {
+                       headerBytes int
+                       offset      int
+               }{
+                       {0, 0},
+                       {1, 0},
+                       {arrow.ViewHeaderSizeBytes - 1, 0},
+                       {arrow.ViewHeaderSizeBytes, 1},
+               } {
+                       t.Run(fmt.Sprintf("%s/bytes=%d/offset=%d", 
dtype.Name(), tc.headerBytes, tc.offset), func(t *testing.T) {
+                               mem := 
memory.NewCheckedAllocator(memory.DefaultAllocator)
+                               defer mem.AssertSize(t, 0)
+                               header := memory.NewBufferBytes(make([]byte, 
tc.headerBytes))
+                               defer header.Release()
+                               variadic := memory.NewResizableBuffer(mem)
+                               variadic.Resize(32)
+                               defer variadic.Release()
+                               data := NewData(dtype, 1, []*memory.Buffer{nil, 
header, variadic}, nil, 0, tc.offset)
+                               defer data.Release()
+
+                               result, err := concat([]arrow.ArrayData{data}, 
mem)
+                               if result != nil {
+                                       defer result.Release()
+                               }
+                               require.Error(t, err)
+                               require.Nil(t, result)
+                       })
+               }
+       }
+}

Reply via email to