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 cc09645c perf(arrow/array): reuse TableReader scratch slices (#1200)
cc09645c is described below
commit cc09645cd46034d0bb9ab9a65cb2c68d0cf2788a
Author: Minh Vu <[email protected]>
AuthorDate: Fri Aug 21 17:43:55 2026 +0200
perf(arrow/array): reuse TableReader scratch slices (#1200)
## What
`TableReader.Next` allocated two column slices for every record batch.
This keeps the batch slice on `TableReader` and reads the current chunks
again after finding the batch size. `NewRecordBatch` copies the input
slice, so the scratch slice can be safely reused on the next call.
## Benchmark
The benchmark reuses one reader across passes. It uses Int32 columns
with 256 rows per chunk.
256 columns x 256 chunks:
- 10.2 ms -> 3.0 ms
- 3.76 MB -> 1.27 MB
- 1024 -> 512 allocations
The benchmark covers 4, 32, and 256 columns with 32 and 256 chunks.
## Tests
- `go test ./arrow/array -count=1`
- `go test -race ./arrow/array -run TestTableReader -count=1`
---
arrow/array/table.go | 19 +++----
arrow/array/table_reader_bench_test.go | 91 ++++++++++++++++++++++++++++++++++
arrow/array/table_test.go | 34 +++++++++++++
3 files changed, 135 insertions(+), 9 deletions(-)
diff --git a/arrow/array/table.go b/arrow/array/table.go
index 9c8f7012..9e3e83c7 100644
--- a/arrow/array/table.go
+++ b/arrow/array/table.go
@@ -308,6 +308,8 @@ type TableReader struct {
chunks []*arrow.Chunked
slots []int // chunk indices
offsets []int64 // chunk offsets
+ // batch is reused as scratch input; NewRecordBatch copies the slice.
+ batch []arrow.Array
}
// NewTableReader returns a new TableReader to iterate over the (possibly
chunked) Table.
@@ -322,6 +324,7 @@ func NewTableReader(tbl arrow.Table, chunkSize int64)
*TableReader {
chunks: make([]*arrow.Chunked, ncols),
slots: make([]int, ncols),
offsets: make([]int64, ncols),
+ batch: make([]arrow.Array, ncols),
}
tr.refCount.Add(1)
tr.tbl.Retain()
@@ -355,8 +358,7 @@ func (tr *TableReader) Next() bool {
// determine the minimum contiguous slice across all columns
chunksz := imin64(tr.max-tr.cur, tr.chksz)
- chunks := make([]arrow.Array, len(tr.chunks))
- for i := range chunks {
+ for i := range tr.chunks {
j := tr.slots[i]
chunk := tr.chunks[i].Chunk(j)
for chunk.Len() == 0 && j+1 < len(tr.chunks[i].Chunks()) {
@@ -369,12 +371,10 @@ func (tr *TableReader) Next() bool {
chunksz = remain
}
- chunks[i] = chunk
}
-
// slice the chunks, advance each chunk slot as appropriate.
- batch := make([]arrow.Array, len(tr.chunks))
- for i, chunk := range chunks {
+ for i := range tr.chunks {
+ chunk := tr.chunks[i].Chunk(tr.slots[i])
var slice arrow.Array
offset := tr.offsets[i]
switch int64(chunk.Len()) - offset {
@@ -393,13 +393,13 @@ func (tr *TableReader) Next() bool {
tr.offsets[i] += chunksz
slice = NewSlice(chunk, offset, offset+chunksz)
}
- batch[i] = slice
+ tr.batch[i] = slice
}
tr.cur += chunksz
- tr.rec = NewRecordBatch(tr.tbl.Schema(), batch, chunksz)
+ tr.rec = NewRecordBatch(tr.tbl.Schema(), tr.batch, chunksz)
- for _, arr := range batch {
+ for _, arr := range tr.batch {
arr.Release()
}
@@ -430,6 +430,7 @@ func (tr *TableReader) Release() {
tr.chunks = nil
tr.slots = nil
tr.offsets = nil
+ tr.batch = nil
}
}
func (tr *TableReader) Err() error { return nil }
diff --git a/arrow/array/table_reader_bench_test.go
b/arrow/array/table_reader_bench_test.go
new file mode 100644
index 00000000..641181dc
--- /dev/null
+++ b/arrow/array/table_reader_bench_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
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/apache/arrow-go/v18/arrow"
+ "github.com/apache/arrow-go/v18/arrow/memory"
+)
+
+func BenchmarkTableReaderNext(b *testing.B) {
+ const rowsPerChunk = 256
+ for _, numCols := range []int{4, 32, 256} {
+ for _, numChunks := range []int{32, 256} {
+ b.Run(fmt.Sprintf("columns=%d/chunks=%d", numCols,
numChunks), func(b *testing.B) {
+ table :=
makeTableReaderBenchmarkTable(memory.DefaultAllocator, numCols, numChunks,
rowsPerChunk)
+ defer table.Release()
+
+ reader := NewTableReader(table, rowsPerChunk)
+ defer reader.Release()
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ var rows int64
+ for reader.Next() {
+ rows +=
reader.RecordBatch().NumRows()
+ }
+ if rows != table.NumRows() {
+ b.Fatalf("invalid row count:
got=%d, want=%d", rows, table.NumRows())
+ }
+
+ reader.cur = 0
+ clear(reader.slots)
+ clear(reader.offsets)
+ }
+ })
+ }
+ }
+}
+
+func makeTableReaderBenchmarkTable(mem memory.Allocator, numCols, numChunks,
rowsPerChunk int) arrow.Table {
+ fields := make([]arrow.Field, numCols)
+ for i := range fields {
+ fields[i] = arrow.Field{Name: fmt.Sprintf("col_%d", i), Type:
arrow.PrimitiveTypes.Int32}
+ }
+ schema := arrow.NewSchema(fields, nil)
+
+ chunks := make([]arrow.Array, numChunks)
+ for i := range chunks {
+ bldr := NewInt32Builder(mem)
+ bldr.Reserve(rowsPerChunk)
+ for j := 0; j < rowsPerChunk; j++ {
+ bldr.Append(int32(i*rowsPerChunk + j))
+ }
+ chunks[i] = bldr.NewInt32Array()
+ bldr.Release()
+ }
+
+ cols := make([]arrow.Column, numCols)
+ for i, field := range fields {
+ chunked := arrow.NewChunked(field.Type, chunks)
+ cols[i] = *arrow.NewColumn(field, chunked)
+ chunked.Release()
+ }
+ table := NewTable(schema, cols, -1)
+
+ for i := range cols {
+ cols[i].Release()
+ }
+ for _, chunk := range chunks {
+ chunk.Release()
+ }
+ return table
+}
diff --git a/arrow/array/table_test.go b/arrow/array/table_test.go
index b1da6f3e..d42c0d10 100644
--- a/arrow/array/table_test.go
+++ b/arrow/array/table_test.go
@@ -1019,6 +1019,40 @@ func TestTableReaderSkipsEmptyChunks(t *testing.T) {
}
}
+func TestTableReaderRetainedRecordBatchSurvivesNext(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ builder := array.NewInt32Builder(mem)
+ builder.AppendValues([]int32{1, 2, 3, 4}, nil)
+ values := builder.NewInt32Array()
+ builder.Release()
+ defer values.Release()
+
+ field := arrow.Field{Name: "values", Type: arrow.PrimitiveTypes.Int32}
+ column := arrow.NewColumnFromArr(field, values)
+ defer column.Release()
+ table := array.NewTable(arrow.NewSchema([]arrow.Field{field}, nil),
[]arrow.Column{column}, -1)
+ defer table.Release()
+
+ reader := array.NewTableReader(table, 2)
+ defer reader.Release()
+ if !reader.Next() {
+ t.Fatal("expected the first record batch")
+ }
+ first := reader.RecordBatch()
+ first.Retain()
+ defer first.Release()
+
+ if !reader.Next() {
+ t.Fatal("expected the second record batch")
+ }
+ got := first.Column(0).(*array.Int32).Value(0)
+ if got != 1 {
+ t.Fatalf("first record batch changed after Next: got=%d,
want=1", got)
+ }
+}
+
func TestTableToString(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)