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 1f29e26d perf(parquet): reuse dictionary RLE decoder across pages 
(#1247)
1f29e26d is described below

commit 1f29e26d300aa9ec3782707db491155fcb27e6d7
Author: Minh Vu <[email protected]>
AuthorDate: Mon Aug 31 17:41:59 2026 +0200

    perf(parquet): reuse dictionary RLE decoder across pages (#1247)
    
    ## Summary
    
    - **Reuse the dictionary RLE decoder between data pages.**
    - Reset the existing `TypedRleDecoder` and its `bytes.Reader` in
    `SetData`.
    - Keep the current bit-width validation and empty-page behavior.
    - Add a page-transition regression test covering partial runs, literal
    runs, and empty data.
    - Add small-page and `SetData` allocation benchmarks.
    
    ## Benchmark
    
    Local Apple M1 Pro run with `GOMAXPROCS=1`: repeated `SetData` improved
    from about **0.6-1.0 us / 4.9 KiB / 3 allocs** to **6.3 ns / 0 B / 0
    allocs**.
    
    ## Tests
    
    - `go test ./parquet/internal/encoding ./parquet/internal/utils`
    - `go test -race ./parquet/internal/encoding ./parquet/internal/utils`
    - `go vet -composites=false ./parquet/internal/encoding
    ./parquet/internal/utils`
    - `GOOS=linux GOARCH=386 go test -c -o /dev/null
    ./parquet/internal/encoding`
    
    ## User-facing changes
    
    No.
---
 parquet/internal/encoding/decoder.go               | 15 ++++-
 .../internal/encoding/encoding_benchmarks_test.go  | 71 ++++++++++++++++++++++
 parquet/internal/encoding/encoding_test.go         | 50 +++++++++++++++
 3 files changed, 134 insertions(+), 2 deletions(-)

diff --git a/parquet/internal/encoding/decoder.go 
b/parquet/internal/encoding/decoder.go
index 3955bf31..e97b5a13 100644
--- a/parquet/internal/encoding/decoder.go
+++ b/parquet/internal/encoding/decoder.go
@@ -107,6 +107,7 @@ type dictDecoder[T parquet.ColumnTypes] struct {
        mem              memory.Allocator
        dictValueDecoder utils.DictionaryConverter[T]
        idxDecoder       *utils.TypedRleDecoder[T]
+       idxReader        bytes.Reader
 
        idxScratchSpace  []uint64
        idxAppendScratch []int
@@ -128,7 +129,7 @@ func (d *dictDecoder[T]) SetData(nvals int, data []byte) 
error {
        d.nvals = nvals
        if len(data) == 0 {
                // no data, bitwidth can safely be 0
-               d.idxDecoder = 
utils.NewTypedRleDecoder[T](bytes.NewReader(data), 0 /* bitwidth */)
+               d.resetIndexDecoder(data, 0 /* bitwidth */)
                return nil
        }
 
@@ -139,10 +140,20 @@ func (d *dictDecoder[T]) SetData(nvals int, data []byte) 
error {
        }
 
        // pass the rest of the data, minus that first byte, to the decoder
-       d.idxDecoder = utils.NewTypedRleDecoder[T](bytes.NewReader(data[1:]), 
int(width))
+       d.resetIndexDecoder(data[1:], int(width))
        return nil
 }
 
+func (d *dictDecoder[T]) resetIndexDecoder(data []byte, width int) {
+       d.idxReader.Reset(data)
+       if d.idxDecoder == nil {
+               d.idxDecoder = utils.NewTypedRleDecoder[T](&d.idxReader, width)
+               return
+       }
+
+       d.idxDecoder.Reset(&d.idxReader, width)
+}
+
 func (d *dictDecoder[T]) discard(n int) (int, error) {
        n = d.idxDecoder.Discard(n)
        d.nvals -= n
diff --git a/parquet/internal/encoding/encoding_benchmarks_test.go 
b/parquet/internal/encoding/encoding_benchmarks_test.go
index 7946e844..43bcc4ea 100644
--- a/parquet/internal/encoding/encoding_benchmarks_test.go
+++ b/parquet/internal/encoding/encoding_benchmarks_test.go
@@ -544,6 +544,77 @@ func BenchmarkDecodeDictByteArray(b *testing.B) {
        }
 }
 
+func BenchmarkDecodeDictByteArrayReuse(b *testing.B) {
+       const nunique = 100
+
+       for _, nvalues := range []int{64, 1024, 8192} {
+               b.Run(fmt.Sprintf("values=%d", nvalues), func(b *testing.B) {
+                       rag := testutils.NewRandomArrayGenerator(0)
+                       dict := rag.ByteArray(nunique, 32, 32, 
0).(*array.String)
+                       indices := rag.Int32(int64(nvalues), 0, nunique-1, 0)
+
+                       values := make([]parquet.ByteArray, nvalues)
+                       for idx := range values {
+                               values[idx] = 
[]byte(dict.Value(int(indices.Value(idx))))
+                       }
+
+                       col := 
schema.NewColumn(schema.NewByteArrayNode("bytearray", 
parquet.Repetitions.Required, -1), 0, 0)
+                       enc := encoding.NewEncoder(parquet.Types.ByteArray, 
parquet.Encodings.PlainDict, true, col, 
memory.DefaultAllocator).(*encoding.DictByteArrayEncoder)
+                       enc.Put(values)
+
+                       dictBuf := make([]byte, enc.DictEncodedSize())
+                       enc.WriteDict(dictBuf)
+                       idxBuf := make([]byte, enc.EstimatedDataEncodedSize())
+                       n, err := enc.WriteIndices(idxBuf)
+                       if err != nil {
+                               b.Fatal(err)
+                       }
+                       idxBuf = idxBuf[:n]
+
+                       dec := encoding.NewDecoder(parquet.Types.ByteArray, 
parquet.Encodings.Plain, col, memory.DefaultAllocator)
+                       if err := dec.SetData(nunique, dictBuf); err != nil {
+                               b.Fatal(err)
+                       }
+                       dictDec := 
encoding.NewDictDecoder(parquet.Types.ByteArray, col, 
memory.DefaultAllocator).(*encoding.DictByteArrayDecoder)
+                       dictDec.SetDict(dec)
+                       out := make([]parquet.ByteArray, nvalues)
+
+                       b.SetBytes(int64(nvalues))
+                       b.ReportAllocs()
+                       b.ResetTimer()
+                       for i := 0; i < b.N; i++ {
+                               if err := dictDec.SetData(nvalues, idxBuf); err 
!= nil {
+                                       b.Fatal(err)
+                               }
+                               decoded, err := dictDec.Decode(out)
+                               if err != nil {
+                                       b.Fatal(err)
+                               }
+                               if decoded != nvalues {
+                                       b.Fatalf("decoded %d values, want %d", 
decoded, nvalues)
+                               }
+                       }
+               })
+       }
+}
+
+func BenchmarkDictDecoderSetDataReuse(b *testing.B) {
+       column := schema.NewColumn(schema.NewInt32Node("int32", 
parquet.Repetitions.Required, -1), 0, 0)
+       decoder := encoding.NewDictDecoder(parquet.Types.Int32, column, 
memory.DefaultAllocator)
+       data := []byte{1}
+       if err := decoder.SetData(1024, data); err != nil {
+               b.Fatal(err)
+       }
+
+       b.ReportAllocs()
+       b.ResetTimer()
+       for i := 0; i < b.N; i++ {
+               if err := decoder.SetData(1024, data); err != nil {
+                       b.Fatal(err)
+               }
+       }
+}
+
 func BenchmarkByteStreamSplitEncodingInt32(b *testing.B) {
        for sz := MINSIZE; sz < MAXSIZE+1; sz *= 2 {
                b.Run(fmt.Sprintf("len %d", sz), func(b *testing.B) {
diff --git a/parquet/internal/encoding/encoding_test.go 
b/parquet/internal/encoding/encoding_test.go
index 4b527332..1589c133 100644
--- a/parquet/internal/encoding/encoding_test.go
+++ b/parquet/internal/encoding/encoding_test.go
@@ -628,6 +628,56 @@ func 
TestDictionaryDecoderRejectsTruncatedPackedLiteralRun(t *testing.T) {
        assert.ErrorIs(t, err, io.ErrUnexpectedEOF)
 }
 
+func encodeDictionaryIndexPage(t *testing.T, values []uint64, width int) 
[]byte {
+       t.Helper()
+
+       data := make([]byte, 1+utils.MaxRLEBufferSize(width, 
len(values))+utils.MinRLEBufferSize(width))
+       data[0] = byte(width)
+       encoder := utils.NewRleEncoder(utils.NewWriterAtBuffer(data[1:]), width)
+       for _, value := range values {
+               require.NoError(t, encoder.Put(value))
+       }
+       return data[:encoder.Flush()+1]
+}
+
+func TestDictionaryDecoderResetsStateBetweenPages(t *testing.T) {
+       column := schema.NewColumn(schema.NewInt32Node("int32", 
parquet.Repetitions.Required, -1), 0, 0)
+       dictionaryData := make([]byte, 8)
+       binary.LittleEndian.PutUint32(dictionaryData, 10)
+       binary.LittleEndian.PutUint32(dictionaryData[4:], 20)
+
+       dictionary := encoding.NewDecoder(parquet.Types.Int32, 
parquet.Encodings.Plain, column, memory.DefaultAllocator)
+       require.NoError(t, dictionary.SetData(2, dictionaryData))
+
+       decoder := encoding.NewDictDecoder(parquet.Types.Int32, column, 
memory.DefaultAllocator)
+       decoder.SetDict(dictionary)
+
+       firstPageValues := make([]uint64, 16)
+       firstPage := encodeDictionaryIndexPage(t, firstPageValues, 1)
+       require.NoError(t, decoder.SetData(len(firstPageValues), firstPage))
+
+       firstOutput := make([]int32, 3)
+       decoded, err := decoder.(encoding.Int32Decoder).Decode(firstOutput)
+       require.NoError(t, err)
+       assert.Equal(t, len(firstOutput), decoded)
+       assert.Equal(t, []int32{10, 10, 10}, firstOutput)
+       assert.Equal(t, len(firstPageValues)-len(firstOutput), 
decoder.ValuesLeft())
+
+       require.NoError(t, decoder.SetData(1, nil))
+       _, err = decoder.(encoding.Int32Decoder).Decode(make([]int32, 1))
+       assert.Error(t, err)
+
+       secondPageValues := []uint64{1, 0, 1, 1, 0, 1, 0, 1}
+       secondPage := encodeDictionaryIndexPage(t, secondPageValues, 1)
+       require.NoError(t, decoder.SetData(len(secondPageValues), secondPage))
+
+       secondOutput := make([]int32, len(secondPageValues))
+       decoded, err = decoder.(encoding.Int32Decoder).Decode(secondOutput)
+       require.NoError(t, err)
+       assert.Equal(t, len(secondOutput), decoded)
+       assert.Equal(t, []int32{20, 10, 20, 20, 10, 20, 10, 20}, secondOutput)
+}
+
 func TestWriteDeltaBitPackedInt32(t *testing.T) {
        column := schema.NewColumn(schema.NewInt32Node("int32", 
parquet.Repetitions.Required, -1), 0, 0)
 

Reply via email to