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 a7a6c19b perf(parquet): reuse DELTA decoder scratch (#1322)
a7a6c19b is described below

commit a7a6c19b6b8db92e3d01463356e69f26ac569b33
Author: Minh Vu <[email protected]>
AuthorDate: Mon Sep 21 19:11:36 2026 +0200

    perf(parquet): reuse DELTA decoder scratch (#1322)
    
    **What**
    
    - Reuse DELTA length and prefix scratch buffers across decoder pages.
    - Add tests for scratch reuse and page-to-page correctness.
    
    **Why**
    
    - The column reader caches decoders by encoding.
    - Each page currently allocates fresh length and prefix arrays even when
    the decoder already has enough capacity.
    
    **Implementation**
    
    - Keep stable backing buffers for decoded lengths and prefix lengths.
    - Expose sliced views to the existing Decode and Discard paths.
    - Keep the existing page-size behavior when a larger page needs more
    capacity.
    - The existing repeated `SetData + Decode` benchmark now shows 18 to 15
    allocations/op and about 512 KiB less allocation for a full 65,536-value
    prefix-heavy page.
    
    Tests:
    
    - `go test ./parquet/internal/encoding -count=1`
    - `go test ./parquet/file -run
    
'^(TestWithEOFReader|TestInvalidHeaders|TestInvalidFooter|TestIncompleteMetadata|TestDeltaLengthByteArrayPackingWithNulls|TestDeltaBinaryPackedMultipleBatches|TestPageStreaming.*|TestPrimitiveReader|TestFullSeekRow|TestSkipEmptyRepeatedRows)$'
    -count=1`
    - `go test -race ./parquet/internal/encoding -run
    
'TestDelta(ByteArrayDecoderReusesPageScratch|LengthByteArrayDecoderReusesLengthScratch|ByteArrayDecoderKeepsResultsAcrossPages|ByteArrayDecoderRejectsInvalidPrefixes)$'
    -count=1`
    - `go vet ./parquet/internal/encoding`
---
 parquet/internal/encoding/delta_byte_array.go      |  9 ++++-
 .../encoding/delta_byte_array_decode_test.go       | 36 ++++++++++++++++++
 .../internal/encoding/delta_length_byte_array.go   | 13 +++++--
 .../encoding/delta_length_byte_array_test.go       | 44 ++++++++++++++++++++++
 4 files changed, 98 insertions(+), 4 deletions(-)

diff --git a/parquet/internal/encoding/delta_byte_array.go 
b/parquet/internal/encoding/delta_byte_array.go
index b2ed5bf5..13428aca 100644
--- a/parquet/internal/encoding/delta_byte_array.go
+++ b/parquet/internal/encoding/delta_byte_array.go
@@ -163,6 +163,7 @@ type DeltaByteArrayDecoder struct {
        *DeltaLengthByteArrayDecoder
 
        prefixLengths []int32
+       prefixScratch []int32
        lastVal       parquet.ByteArray
 }
 
@@ -189,7 +190,13 @@ func (d *DeltaByteArrayDecoder) SetData(nvalues int, data 
[]byte) error {
                return fmt.Errorf("parquet: delta prefix count %d exceeds value 
count %d", prefixLenDec.totalValues, nvalues)
        }
 
-       d.prefixLengths = make([]int32, prefixLenDec.ValuesLeft())
+       prefixCount := prefixLenDec.ValuesLeft()
+       if cap(d.prefixScratch) < prefixCount {
+               d.prefixScratch = make([]int32, prefixCount)
+       } else {
+               d.prefixScratch = d.prefixScratch[:prefixCount]
+       }
+       d.prefixLengths = d.prefixScratch
        // decode all the prefix lengths first so we know how many bytes it 
took to get the
        // prefix lengths for nvalues
        decoded, err := prefixLenDec.Decode(d.prefixLengths)
diff --git a/parquet/internal/encoding/delta_byte_array_decode_test.go 
b/parquet/internal/encoding/delta_byte_array_decode_test.go
index 1a5ea5f2..588e6b41 100644
--- a/parquet/internal/encoding/delta_byte_array_decode_test.go
+++ b/parquet/internal/encoding/delta_byte_array_decode_test.go
@@ -151,6 +151,42 @@ func TestDeltaByteArrayDecoderKeepsResultsAcrossPages(t 
*testing.T) {
        requireDecodedStrings(t, secondOut, secondValues)
 }
 
+func TestDeltaByteArrayDecoderReusesPageScratch(t *testing.T) {
+       firstValues := []string{
+               "partition/000/value/000", "partition/000/value/001",
+               "partition/001/value/000", "partition/001/value/001",
+       }
+       secondValues := []string{"partition/100/value/000", 
"partition/100/value/001"}
+       dec := NewDecoder(parquet.Types.ByteArray, 
parquet.Encodings.DeltaByteArray,
+               nil, memory.DefaultAllocator).(*DeltaByteArrayDecoder)
+
+       firstData := encodeDeltaByteArrayPage(t, firstValues)
+       require.NoError(t, dec.SetData(len(firstValues), firstData))
+       lengthStart := &dec.lengthScratch[0]
+       prefixStart := &dec.prefixScratch[0]
+       lengthCap := cap(dec.lengthScratch)
+       prefixCap := cap(dec.prefixScratch)
+
+       firstOut := make([]parquet.ByteArray, len(firstValues))
+       decoded, err := dec.Decode(firstOut)
+       require.NoError(t, err)
+       require.Equal(t, len(firstValues), decoded)
+       requireDecodedStrings(t, firstOut, firstValues)
+
+       secondData := encodeDeltaByteArrayPage(t, secondValues)
+       require.NoError(t, dec.SetData(len(secondValues), secondData))
+       require.Equal(t, lengthCap, cap(dec.lengthScratch))
+       require.Equal(t, prefixCap, cap(dec.prefixScratch))
+       require.Same(t, lengthStart, &dec.lengthScratch[0])
+       require.Same(t, prefixStart, &dec.prefixScratch[0])
+
+       secondOut := make([]parquet.ByteArray, len(secondValues))
+       decoded, err = dec.Decode(secondOut)
+       require.NoError(t, err)
+       require.Equal(t, len(secondValues), decoded)
+       requireDecodedStrings(t, secondOut, secondValues)
+}
+
 func TestDeltaByteArrayDecoderDecodeSpaced(t *testing.T) {
        values := []string{"a/000", "a/001", "b/000", "b/001"}
        data := encodeDeltaByteArrayPage(t, values)
diff --git a/parquet/internal/encoding/delta_length_byte_array.go 
b/parquet/internal/encoding/delta_length_byte_array.go
index 4b74ff1d..c06a3bb3 100644
--- a/parquet/internal/encoding/delta_length_byte_array.go
+++ b/parquet/internal/encoding/delta_length_byte_array.go
@@ -105,8 +105,9 @@ func (enc *DeltaLengthByteArrayEncoder) FlushValues() 
(Buffer, error) {
 type DeltaLengthByteArrayDecoder struct {
        decoder
 
-       mem     memory.Allocator
-       lengths []int32
+       mem           memory.Allocator
+       lengths       []int32
+       lengthScratch []int32
 }
 
 // Type returns the underlying type which is handled by this encoder, 
ByteArrays only.
@@ -130,7 +131,13 @@ func (d *DeltaLengthByteArrayDecoder) SetData(nvalues int, 
data []byte) error {
        if dec.totalValues > uint64(nvalues) {
                return fmt.Errorf("parquet: delta length count %d exceeds value 
count %d", dec.totalValues, nvalues)
        }
-       d.lengths = make([]int32, dec.totalValues)
+       lengthCount := int(dec.totalValues)
+       if cap(d.lengthScratch) < lengthCount {
+               d.lengthScratch = make([]int32, lengthCount)
+       } else {
+               d.lengthScratch = d.lengthScratch[:lengthCount]
+       }
+       d.lengths = d.lengthScratch
        decoded, err := dec.Decode(d.lengths)
        if err != nil {
                return err
diff --git a/parquet/internal/encoding/delta_length_byte_array_test.go 
b/parquet/internal/encoding/delta_length_byte_array_test.go
index 7ed52d8a..dec0e200 100644
--- a/parquet/internal/encoding/delta_length_byte_array_test.go
+++ b/parquet/internal/encoding/delta_length_byte_array_test.go
@@ -78,3 +78,47 @@ func TestDeltaLengthByteArrayEncoderPreservesBatches(t 
*testing.T) {
                })
        }
 }
+
+func TestDeltaLengthByteArrayDecoderReusesLengthScratch(t *testing.T) {
+       firstValues := []parquet.ByteArray{
+               parquet.ByteArray("partition/000/value/000"),
+               parquet.ByteArray("partition/000/value/001"),
+               parquet.ByteArray("partition/001/value/000"),
+       }
+       secondValues := 
[]parquet.ByteArray{parquet.ByteArray("partition/100/value/000")}
+       encode := func(values []parquet.ByteArray) []byte {
+               t.Helper()
+               enc := NewEncoder(parquet.Types.ByteArray, 
parquet.Encodings.DeltaLengthByteArray,
+                       false, nil, memory.DefaultAllocator).(ByteArrayEncoder)
+               defer enc.Release()
+               enc.Put(values)
+               buf, err := enc.FlushValues()
+               require.NoError(t, err)
+               defer buf.Release()
+               return append([]byte(nil), buf.Bytes()...)
+       }
+
+       dec := NewDecoder(parquet.Types.ByteArray, 
parquet.Encodings.DeltaLengthByteArray,
+               nil, memory.DefaultAllocator).(*DeltaLengthByteArrayDecoder)
+       firstData := encode(firstValues)
+       require.NoError(t, dec.SetData(len(firstValues), firstData))
+       lengthStart := &dec.lengthScratch[0]
+       lengthCap := cap(dec.lengthScratch)
+
+       firstOut := make([]parquet.ByteArray, len(firstValues))
+       decoded, err := dec.Decode(firstOut)
+       require.NoError(t, err)
+       require.Equal(t, len(firstValues), decoded)
+       require.Equal(t, firstValues, firstOut)
+
+       secondData := encode(secondValues)
+       require.NoError(t, dec.SetData(len(secondValues), secondData))
+       require.Equal(t, lengthCap, cap(dec.lengthScratch))
+       require.Same(t, lengthStart, &dec.lengthScratch[0])
+
+       secondOut := make([]parquet.ByteArray, len(secondValues))
+       decoded, err = dec.Decode(secondOut)
+       require.NoError(t, err)
+       require.Equal(t, len(secondValues), decoded)
+       require.Equal(t, secondValues, secondOut)
+}

Reply via email to