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 a00312ac perf(parquet): avoid scanning plain byte-array offsets (#1362)
a00312ac is described below

commit a00312ac02beb07af6e22276b71c91b3f0b89ea2
Author: Minh Vu <[email protected]>
AuthorDate: Tue Oct 13 16:28:11 2026 +0200

    perf(parquet): avoid scanning plain byte-array offsets (#1362)
    
    ### What
    Avoid scanning PLAIN Arrow byte-array offsets to compute encoded size.
    
    ### Why
    The payload size is the last offset minus the first offset, so the
    pre-pass is redundant.
    
    ### Implementation
    Compute payload bytes from boundary offsets and add four bytes per
    value.
    
    ### Benchmark
    At 65,536 values on AMD EPYC, median time before → after:
    - 0-byte values: 281.0 → 244.4 µs (-13.0%)
    - 4-byte values: 296.8 → 275.2 µs (-7.3%)
    - 16-byte values: 332.8 → 301.5 µs (-9.4%)
    - 64-byte values: 375.5 → 335.2 µs (-10.7%)
    - Allocations: 0 → 0
---
 .../internal/encoding/byte_array_arrow_encoder.go  |   9 +-
 .../encoding/byte_array_arrow_encoder_test.go      | 146 +++++++++++++++++++++
 2 files changed, 150 insertions(+), 5 deletions(-)

diff --git a/parquet/internal/encoding/byte_array_arrow_encoder.go 
b/parquet/internal/encoding/byte_array_arrow_encoder.go
index b5253a55..c9ec4b8f 100644
--- a/parquet/internal/encoding/byte_array_arrow_encoder.go
+++ b/parquet/internal/encoding/byte_array_arrow_encoder.go
@@ -32,14 +32,13 @@ func putArrowPlain[T arrowByteArrayOffset](sink 
*PooledBufferWriter, values []by
                return
        }
 
-       encodedSize := 0
-       for i := 0; i < len(offsets)-1; i++ {
-               encodedSize += int(offsets[i+1]-offsets[i]) + 4
-       }
+       numValues := len(offsets) - 1
+       // Slices and valid runs can start at a nonzero offset in the data 
buffer.
+       encodedSize := int(offsets[numValues]-offsets[0]) + 4*numValues
 
        sink.Reserve(encodedSize)
        out := sink.buf.Buf()[sink.pos : sink.pos+encodedSize]
-       for i := 0; i < len(offsets)-1; i++ {
+       for i := 0; i < numValues; i++ {
                start := int(offsets[i])
                end := int(offsets[i+1])
                binary.LittleEndian.PutUint32(out, uint32(end-start))
diff --git a/parquet/internal/encoding/byte_array_arrow_encoder_test.go 
b/parquet/internal/encoding/byte_array_arrow_encoder_test.go
index 949d5e65..30a7e528 100644
--- a/parquet/internal/encoding/byte_array_arrow_encoder_test.go
+++ b/parquet/internal/encoding/byte_array_arrow_encoder_test.go
@@ -124,6 +124,152 @@ func TestByteArrayArrowEncodersMatchByteArrayInput(t 
*testing.T) {
        }
 }
 
+func TestPlainByteArrayArrowOffsets(t *testing.T) {
+       t.Run("int32", testPlainByteArrayArrowOffsets[int32])
+       t.Run("int64", testPlainByteArrayArrowOffsets[int64])
+}
+
+func testPlainByteArrayArrowOffsets[T arrowByteArrayOffset](t *testing.T) {
+       t.Helper()
+       values, data, offsets32, _ := arrowByteArrayInput()
+       offsets := make([]T, len(offsets32))
+       for i, offset := range offsets32 {
+               offsets[i] = T(offset)
+       }
+       for _, tc := range []struct {
+               name            string
+               offsets         []T
+               validBits       []byte
+               validBitsOffset int64
+               want            []parquet.ByteArray
+       }{
+               {name: "nil"},
+               {name: "empty-slice", offsets: offsets[3:4]},
+               {name: "empty-values", offsets: []T{offsets[2], offsets[2], 
offsets[2]}, want: []parquet.ByteArray{{}, {}}},
+               {name: "sliced", offsets: offsets[1:7], want: values[1:6]},
+               {name: "whole", offsets: offsets, want: values},
+               {
+                       name: "spaced-slice", offsets: offsets[1:],
+                       validBits: []byte{0b01011000, 0}, validBitsOffset: 3,
+                       want: []parquet.ByteArray{values[1], values[2], 
values[4]},
+               },
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       enc := NewEncoder(parquet.Types.ByteArray, 
parquet.Encodings.Plain, false, nil, 
memory.DefaultAllocator).(*PlainByteArrayEncoder)
+                       defer enc.Release()
+                       enc.PutByteArray([]byte("before"))
+                       want := []parquet.ByteArray{[]byte("before")}
+                       for range 2 {
+                               putArrowPlainSpaced(enc.sink, data, tc.offsets, 
tc.validBits, tc.validBitsOffset)
+                               want = append(want, tc.want...)
+                       }
+                       enc.PutByteArray([]byte("after"))
+                       want = append(want, []byte("after"))
+                       got, err := enc.FlushValues()
+                       require.NoError(t, err)
+                       defer got.Release()
+                       require.Equal(t, encodedByteArrays(t, 
parquet.Encodings.Plain, want, false, nil, 0), got.Bytes())
+               })
+       }
+}
+
+// Check that the total-span size calculation preserves the plain encoding
+// for variable-width values, sliced offsets, and disjoint valid runs.
+func TestPlainByteArrayArrowVariableLengthSlices(t *testing.T) {
+       t.Run("int32", testPlainByteArrayArrowVariableLengthSlices[int32])
+       t.Run("int64", testPlainByteArrayArrowVariableLengthSlices[int64])
+}
+
+func testPlainByteArrayArrowVariableLengthSlices[T arrowByteArrayOffset](t 
*testing.T) {
+       t.Helper()
+       const nvalues = 129
+       values := make([]parquet.ByteArray, nvalues)
+       data := []byte("prefix outside the first value")
+       offsets := make([]T, nvalues+1)
+       for i := range values {
+               offsets[i] = T(len(data))
+               values[i] = make([]byte, (i*i*17+i*3)%57)
+               for j := range values[i] {
+                       values[i][j] = byte(i + j)
+               }
+               data = append(data, values[i]...)
+       }
+       offsets[nvalues] = T(len(data))
+
+       for _, span := range []struct {
+               name       string
+               begin, end int
+       }{
+               {"whole", 0, nvalues},
+               {"middle", 9, 93},
+               {"single", 63, 64},
+               {"empty", 48, 48},
+               {"tail", nvalues - 12, nvalues},
+       } {
+               for _, selection := range []string{"all", "sparse", 
"long-runs", "none"} {
+                       t.Run(span.name+"/"+selection, func(t *testing.T) {
+                               var validBits []byte
+                               if selection != "all" {
+                                       validBits = make([]byte, 
bitutil.BytesForBits(int64(span.end-span.begin+3)))
+                               }
+                               want := []parquet.ByteArray{[]byte("before")}
+                               for i := span.begin; i < span.end; i++ {
+                                       valid := selection == "all" ||
+                                               selection == "sparse" && i%11 < 
3 ||
+                                               selection == "long-runs" && 
(i/16)%2 == 0
+                                       if valid {
+                                               want = append(want, values[i])
+                                               if validBits != nil {
+                                                       
bitutil.SetBit(validBits, 3+i-span.begin)
+                                               }
+                                       }
+                               }
+                               want = append(want, []byte("after"))
+
+                               enc := NewEncoder(parquet.Types.ByteArray, 
parquet.Encodings.Plain, false, nil, 
memory.DefaultAllocator).(*PlainByteArrayEncoder)
+                               defer enc.Release()
+                               enc.PutByteArray([]byte("before"))
+                               putArrowPlainSpaced(enc.sink, data, 
offsets[span.begin:span.end+1], validBits, 3)
+                               enc.PutByteArray([]byte("after"))
+                               got, err := enc.FlushValues()
+                               require.NoError(t, err)
+                               defer got.Release()
+                               require.Equal(t, encodedByteArrays(t, 
parquet.Encodings.Plain, want, false, nil, 0), got.Bytes())
+                       })
+               }
+       }
+}
+
+func BenchmarkPlainByteArrayPutArrow(b *testing.B) {
+       b.Run("int32", benchmarkPlainByteArrayPutArrow[int32])
+       b.Run("int64", benchmarkPlainByteArrayPutArrow[int64])
+}
+
+func benchmarkPlainByteArrayPutArrow[T arrowByteArrayOffset](b *testing.B) {
+       for _, nvalues := range []int{64, 65536} {
+               for _, width := range []int{0, 4, 16, 64} {
+                       b.Run(fmt.Sprintf("values=%d/bytes=%d", nvalues, 
width), func(b *testing.B) {
+                               data := make([]byte, 4+nvalues*width)
+                               offsets := make([]T, nvalues+1)
+                               for i := range offsets {
+                                       offsets[i] = T(4 + i*width)
+                               }
+                               enc := NewEncoder(parquet.Types.ByteArray, 
parquet.Encodings.Plain, false, nil, 
memory.DefaultAllocator).(*PlainByteArrayEncoder)
+                               defer enc.Release()
+                               enc.sink.Reserve(nvalues * (4 + width))
+                               b.SetBytes(int64(nvalues * (4 + width)))
+                               b.ReportAllocs()
+                               for b.Loop() {
+                                       // Reuse capacity to isolate encoding 
from result-buffer allocation.
+                                       enc.sink.pos = 0
+                                       putArrowPlain(enc.sink, data, offsets)
+                               }
+                               b.ReportMetric(float64(nvalues), "values/op")
+                       })
+               }
+       }
+}
+
 func TestDeltaByteArrayArrowEncoderPreservesStateAcrossBatches(t *testing.T) {
        values, data, offsets32, offsets64 := arrowByteArrayInput()
 

Reply via email to