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 d70b0f05 perf(arrow/ipc): slice already-zero-based offset buffers
(#1191)
d70b0f05 is described below
commit d70b0f050f02010e4339ddedd293d24ab537e713
Author: Minh Vu <[email protected]>
AuthorDate: Wed Aug 26 19:53:35 2026 +0200
perf(arrow/ipc): slice already-zero-based offset buffers (#1191)
### Rationale for this change
IPC currently allocates a new offsets buffer whenever a variable-length
array has unused trailing offsets. For a prefix slice, the first logical
offset is already zero, so the loop only copies every offset and
subtracts zero.
Arrow C++ handles this case by slicing the existing offsets buffer.
### What changes are included in this PR?
- Read the first logical offset instead of treating every sliced array
as needing rebasing.
- Rebase only when that offset is non-zero.
- Use `memory.SliceBuffer` when the required offsets are already
zero-based.
- Add coverage for int32 and int64 offsets, middle slices, and slices
with a non-zero array offset whose first logical value offset is zero.
The benchmark uses a 524,288-value slice from a 1,048,576-value backing
array. Medians from 6 runs on an Apple M1 Pro were:
| prefix slice | main | this PR |
|---|---:|---:|
| int32 time/op | 238 us | 40.7 ns |
| int32 B/op | 2,106,000 | 80 |
| int64 time/op | 283 us | 40.7 ns |
| int64 B/op | 4,203,153 | 80 |
| allocs/op | 8 | 1 |
Unsliced buffers remain allocation-free. Middle slices that need
rebasing are unchanged.
### Are these changes tested?
- `go test ./arrow/ipc -count=1`
- `go test ./arrow/... -count=1`
- `go test ./arrow/ipc -run "^$" -bench
"^BenchmarkGetZeroBasedValueOffsets$" -benchmem -count=6`
### Are there any user-facing changes?
No. The IPC output and public API are unchanged.
---
arrow/ipc/writer.go | 23 +++-------
arrow/ipc/writer_test.go | 115 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 120 insertions(+), 18 deletions(-)
diff --git a/arrow/ipc/writer.go b/arrow/ipc/writer.go
index 24340f44..a771849f 100644
--- a/arrow/ipc/writer.go
+++ b/arrow/ipc/writer.go
@@ -922,29 +922,14 @@ func (w *recordEncoder) getZeroBasedValueOffsets(arr
arrow.Array) *memory.Buffer
dataTypeWidth := arr.DataType().Layout().Buffers[1].ByteWidth
- // if we have a non-zero offset, then the value offsets do not start at
- // zero. we must a) create a new offsets array with shifted offsets and
- // b) slice the values array accordingly
- hasNonZeroOffset := data.Offset() != 0
-
- // or if there are more value offsets than values (the array has been
sliced)
- // we need to trim off the trailing offsets
- hasMoreOffsetsThanValues := offsetBytesNeeded < voffsets.Len()
-
- // or if the offsets do not start from the zero index, we need to shift
them
- // and slice the values array
var firstOffset int64
if dataTypeWidth == 8 {
- firstOffset =
arrow.Int64Traits.CastFromBytes(voffsets.Bytes())[0]
+ firstOffset =
arrow.Int64Traits.CastFromBytes(voffsets.Bytes())[data.Offset()]
} else {
- firstOffset =
int64(arrow.Int32Traits.CastFromBytes(voffsets.Bytes())[0])
+ firstOffset =
int64(arrow.Int32Traits.CastFromBytes(voffsets.Bytes())[data.Offset()])
}
- offsetsDoNotStartFromZero := firstOffset != 0
-
- // determine whether the offsets array should be shifted
- needsTruncateAndShift := hasNonZeroOffset || hasMoreOffsetsThanValues
|| offsetsDoNotStartFromZero
- if needsTruncateAndShift {
+ if firstOffset != 0 {
shiftedOffsets := memory.NewResizableBuffer(w.mem)
shiftedOffsets.Resize(offsetBytesNeeded)
@@ -970,6 +955,8 @@ func (w *recordEncoder) getZeroBasedValueOffsets(arr
arrow.Array) *memory.Buffer
}
voffsets = shiftedOffsets
+ } else if data.Offset() != 0 || offsetBytesNeeded < voffsets.Len() {
+ voffsets = memory.SliceBuffer(voffsets,
data.Offset()*dataTypeWidth, offsetBytesNeeded)
} else {
voffsets.Retain()
}
diff --git a/arrow/ipc/writer_test.go b/arrow/ipc/writer_test.go
index 6d18f5f0..e49c4f54 100644
--- a/arrow/ipc/writer_test.go
+++ b/arrow/ipc/writer_test.go
@@ -308,6 +308,7 @@ func TestGetZeroBasedValueOffsets(t *testing.T) {
offsets := env.getZeroBasedValueOffsets(arr)
defer offsets.Release()
assert.Equal(t, 44, offsets.Len(), "include all offsets if array is not
sliced")
+ assert.Same(t, arr.Data().Buffers()[1], offsets)
sl := array.NewSlice(arr, 0, 4)
defer sl.Release()
@@ -315,6 +316,120 @@ func TestGetZeroBasedValueOffsets(t *testing.T) {
offsets = env.getZeroBasedValueOffsets(sl)
defer offsets.Release()
assert.Equal(t, 20, offsets.Len(), "trim trailing offsets after slice")
+ assert.Same(t, sl.Data().Buffers()[1], offsets.Parent())
+
+ sl = array.NewSlice(arr, 2, 6)
+ defer sl.Release()
+
+ offsets = env.getZeroBasedValueOffsets(sl)
+ defer offsets.Release()
+ assert.Nil(t, offsets.Parent(), "rebase offsets when the first logical
offset is non-zero")
+ assert.Equal(t, []int32{0, 1, 2, 3, 4},
arrow.Int32Traits.CastFromBytes(offsets.Bytes()))
+
+ emptyPrefixBuilder := array.NewStringBuilder(alloc)
+ emptyPrefixBuilder.AppendValues([]string{"", "", "a"}, nil)
+ emptyPrefix := emptyPrefixBuilder.NewArray()
+ emptyPrefixBuilder.Release()
+ defer emptyPrefix.Release()
+
+ sl = array.NewSlice(emptyPrefix, 1, 3)
+ defer sl.Release()
+
+ offsets = env.getZeroBasedValueOffsets(sl)
+ defer offsets.Release()
+ assert.Same(t, sl.Data().Buffers()[1], offsets.Parent())
+ assert.Equal(t, []int32{0, 0, 1},
arrow.Int32Traits.CastFromBytes(offsets.Bytes()))
+
+ largeBuilder := array.NewLargeStringBuilder(alloc)
+ largeBuilder.AppendValues(vals, nil)
+ large := largeBuilder.NewArray()
+ largeBuilder.Release()
+ defer large.Release()
+
+ sl = array.NewSlice(large, 0, 4)
+ defer sl.Release()
+
+ offsets = env.getZeroBasedValueOffsets(sl)
+ defer offsets.Release()
+ assert.Same(t, sl.Data().Buffers()[1], offsets.Parent())
+ assert.Equal(t, []int64{0, 1, 2, 3, 4},
arrow.Int64Traits.CastFromBytes(offsets.Bytes()))
+
+ sl = array.NewSlice(large, 2, 6)
+ defer sl.Release()
+
+ offsets = env.getZeroBasedValueOffsets(sl)
+ defer offsets.Release()
+ assert.Nil(t, offsets.Parent(), "rebase int64 offsets when the first
logical offset is non-zero")
+ assert.Equal(t, []int64{0, 1, 2, 3, 4},
arrow.Int64Traits.CastFromBytes(offsets.Bytes()))
+
+ largeEmptyPrefixBuilder := array.NewLargeStringBuilder(alloc)
+ largeEmptyPrefixBuilder.AppendValues([]string{"", "", "a"}, nil)
+ largeEmptyPrefix := largeEmptyPrefixBuilder.NewArray()
+ largeEmptyPrefixBuilder.Release()
+ defer largeEmptyPrefix.Release()
+
+ sl = array.NewSlice(largeEmptyPrefix, 1, 3)
+ defer sl.Release()
+
+ offsets = env.getZeroBasedValueOffsets(sl)
+ defer offsets.Release()
+ assert.Same(t, sl.Data().Buffers()[1], offsets.Parent())
+ assert.Equal(t, []int64{0, 0, 1},
arrow.Int64Traits.CastFromBytes(offsets.Bytes()))
+}
+
+func BenchmarkGetZeroBasedValueOffsets(b *testing.B) {
+ alloc := memory.NewCheckedAllocator(memory.DefaultAllocator)
+ defer alloc.AssertSize(b, 0)
+
+ const n = 1 << 20
+ values := make([]string, n)
+ for i := range values {
+ values[i] = "x"
+ }
+
+ stringBuilder := array.NewStringBuilder(alloc)
+ stringBuilder.AppendValues(values, nil)
+ strings := stringBuilder.NewArray()
+ stringBuilder.Release()
+ defer strings.Release()
+
+ largeStringBuilder := array.NewLargeStringBuilder(alloc)
+ largeStringBuilder.AppendValues(values, nil)
+ largeStrings := largeStringBuilder.NewArray()
+ largeStringBuilder.Release()
+ defer largeStrings.Release()
+
+ env := &recordEncoder{mem: alloc}
+ for _, tc := range []struct {
+ name string
+ arr arrow.Array
+ }{
+ {name: "int32", arr: strings},
+ {name: "int64", arr: largeStrings},
+ } {
+ b.Run(tc.name, func(b *testing.B) {
+ for _, slice := range []struct {
+ name string
+ begin, end int64
+ }{
+ {name: "full", begin: 0, end: n},
+ {name: "prefix", begin: 0, end: n / 2},
+ {name: "middle", begin: n / 4, end: 3 * n / 4},
+ } {
+ b.Run(slice.name, func(b *testing.B) {
+ arr := array.NewSlice(tc.arr,
slice.begin, slice.end)
+ defer arr.Release()
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ offsets :=
env.getZeroBasedValueOffsets(arr)
+ offsets.Release()
+ }
+ })
+ }
+ })
+ }
}
func TestWriterCatchPanic(t *testing.T) {