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 4d3157dc perf(parquet): batch DELTA_LENGTH byte-array decoding and 
discard (#1415)
4d3157dc is described below

commit 4d3157dc1c741a40d0ece928ed3414e3d7f17742
Author: Minh Vu <[email protected]>
AuthorDate: Mon Oct 26 18:54:50 2026 +0100

    perf(parquet): batch DELTA_LENGTH byte-array decoding and discard (#1415)
    
    ## What
    Batch DELTA_LENGTH byte-array decoding and skipping.
    
    ## Why
    Large batches repeat decoder state updates for each value.
    
    ## Implementation
    - Decode from local slices and update state once per batch.
    - Sum validated lengths and advance the payload once when skipping.
    - Add mixed-length coverage and package benchmarks.
    
    ## Benchmark
    **1.62x to 2.00x faster decode; 2.43x to 2.74x faster discard** in
    isolated loop benchmarks.
---
 .../internal/encoding/delta_length_byte_array.go   |  19 ++-
 .../encoding/delta_length_byte_array_batch_test.go | 176 +++++++++++++++++++++
 2 files changed, 190 insertions(+), 5 deletions(-)

diff --git a/parquet/internal/encoding/delta_length_byte_array.go 
b/parquet/internal/encoding/delta_length_byte_array.go
index c9daf35f..51656c5b 100644
--- a/parquet/internal/encoding/delta_length_byte_array.go
+++ b/parquet/internal/encoding/delta_length_byte_array.go
@@ -177,9 +177,12 @@ func (d *DeltaLengthByteArrayDecoder) decodeOne() 
parquet.ByteArray {
 
 func (d *DeltaLengthByteArrayDecoder) Discard(n int) (int, error) {
        n = min(n, d.nvals)
-       for i := 0; i < n; i++ {
-               d.data = d.data[d.lengths[i]:]
+       var skip int
+       for _, length := range d.lengths[:n] {
+               skip += int(length)
        }
+       // SetData already verified that the lengths fit within the payload.
+       d.data = d.data[skip:]
        d.nvals -= n
        d.lengths = d.lengths[n:]
        return n, nil
@@ -189,9 +192,15 @@ func (d *DeltaLengthByteArrayDecoder) Discard(n int) (int, 
error) {
 // or runs out of values in the column to decode, then returns the number of 
values actually decoded.
 func (d *DeltaLengthByteArrayDecoder) Decode(out []parquet.ByteArray) (int, 
error) {
        max := utils.Min(len(out), d.nvals)
-       for i := 0; i < max; i++ {
-               out[i] = d.decodeOne()
-       }
+       data := d.data
+       for i, length := range d.lengths[:max] {
+               out[i] = data[:length:length]
+               data = data[length:]
+       }
+       // Commit decoder state once per batch instead of once per value.
+       d.data = data
+       d.lengths = d.lengths[max:]
+       d.nvals -= max
        return max, nil
 }
 
diff --git a/parquet/internal/encoding/delta_length_byte_array_batch_test.go 
b/parquet/internal/encoding/delta_length_byte_array_batch_test.go
new file mode 100644
index 00000000..87aae868
--- /dev/null
+++ b/parquet/internal/encoding/delta_length_byte_array_batch_test.go
@@ -0,0 +1,176 @@
+// 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 encoding
+
+import (
+       "bytes"
+       "fmt"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/arrow-go/v18/parquet"
+       "github.com/stretchr/testify/require"
+)
+
+func deltaLengthBatchValues(n int, pattern string) []parquet.ByteArray {
+       values := make([]parquet.ByteArray, n)
+       for i := range values {
+               var length int
+               switch pattern {
+               case "empty":
+                       length = 0
+               case "fixed16":
+                       length = 16
+               case "varied":
+                       length = (i * 17) % 129
+               }
+               value := make([]byte, length)
+               for j := range value {
+                       value[j] = byte((i + j) % 251)
+               }
+               values[i] = value
+       }
+       return values
+}
+
+func encodeDeltaLengthBatchValues(values []parquet.ByteArray) []byte {
+       enc := NewEncoder(parquet.Types.ByteArray, 
parquet.Encodings.DeltaLengthByteArray,
+               false, nil, memory.DefaultAllocator).(ByteArrayEncoder)
+       defer enc.Release()
+       enc.Put(values)
+       buf, err := enc.FlushValues()
+       if err != nil {
+               panic(err)
+       }
+       defer buf.Release()
+       return append([]byte(nil), buf.Bytes()...)
+}
+
+func TestDeltaLengthByteArrayDecoderMixedBatchOperations(t *testing.T) {
+       for _, n := range []int{1, 17, 257, 4096} {
+               for _, pattern := range []string{"empty", "fixed16", "varied"} {
+                       t.Run(fmt.Sprintf("%d/%s", n, pattern), func(t 
*testing.T) {
+                               values := deltaLengthBatchValues(n, pattern)
+                               encoded := encodeDeltaLengthBatchValues(values)
+                               dec := NewDecoder(parquet.Types.ByteArray, 
parquet.Encodings.DeltaLengthByteArray,
+                                       nil, 
memory.DefaultAllocator).(*DeltaLengthByteArrayDecoder)
+
+                               // Run twice to cover reuse of the decoder's 
scratch storage.
+                               for repeat := 0; repeat < 2; repeat++ {
+                                       require.NoError(t, dec.SetData(n, 
encoded))
+                                       pos := 0
+                                       steps := []struct {
+                                               discard bool
+                                               count   int
+                                       }{
+                                               {false, 1},
+                                               {true, 2},
+                                               {false, 9},
+                                               {true, 13},
+                                               {false, 257},
+                                               {true, 511},
+                                               {false, 2048},
+                                       }
+                                       for _, step := range steps {
+                                               count := min(step.count, n-pos)
+                                               if step.discard {
+                                                       skipped, err := 
dec.Discard(count)
+                                                       require.NoError(t, err)
+                                                       require.Equal(t, count, 
skipped)
+                                               } else {
+                                                       out := 
make([]parquet.ByteArray, count)
+                                                       decoded, err := 
dec.Decode(out)
+                                                       require.NoError(t, err)
+                                                       require.Equal(t, count, 
decoded)
+                                                       for i, value := range 
out {
+                                                               require.True(t, 
bytes.Equal(values[pos+i], value), "value %d", pos+i)
+                                                       }
+                                               }
+                                               pos += count
+                                       }
+                                       remaining := n - pos
+                                       out := make([]parquet.ByteArray, 
remaining)
+                                       decoded, err := dec.Decode(out)
+                                       require.NoError(t, err)
+                                       require.Equal(t, remaining, decoded)
+                                       for i, value := range out {
+                                               require.True(t, 
bytes.Equal(values[pos+i], value), "value %d", pos+i)
+                                       }
+                                       require.Equal(t, 0, dec.nvals)
+                                       decoded, err = 
dec.Decode(make([]parquet.ByteArray, 1))
+                                       require.NoError(t, err)
+                                       require.Zero(t, decoded)
+                                       skipped, err := dec.Discard(1)
+                                       require.NoError(t, err)
+                                       require.Zero(t, skipped)
+                               }
+                       })
+               }
+       }
+}
+
+func BenchmarkDeltaLengthByteArrayBatchDecode(b *testing.B) {
+       benchmarkDeltaLengthByteArrayBatch(b, false)
+}
+
+func BenchmarkDeltaLengthByteArrayBatchDiscard(b *testing.B) {
+       benchmarkDeltaLengthByteArrayBatch(b, true)
+}
+
+func benchmarkDeltaLengthByteArrayBatch(b *testing.B, discard bool) {
+       for _, n := range []int{4096, 65536} {
+               for _, pattern := range []string{"empty", "fixed16", "varied"} {
+                       values := deltaLengthBatchValues(n, pattern)
+                       encoded := encodeDeltaLengthBatchValues(values)
+                       var totalBytes int
+                       for _, v := range values {
+                               totalBytes += len(v)
+                       }
+                       for _, batchSize := range []int{256, n} {
+                               b.Run(fmt.Sprintf("%d/%s/batch-%d", n, pattern, 
batchSize), func(b *testing.B) {
+                                       dec := 
NewDecoder(parquet.Types.ByteArray, parquet.Encodings.DeltaLengthByteArray,
+                                               nil, 
memory.DefaultAllocator).(*DeltaLengthByteArrayDecoder)
+                                       out := make([]parquet.ByteArray, 
batchSize)
+                                       b.SetBytes(int64(totalBytes))
+                                       b.ReportAllocs()
+                                       for b.Loop() {
+                                               b.StopTimer()
+                                               if err := dec.SetData(n, 
encoded); err != nil {
+                                                       b.Fatal(err)
+                                               }
+                                               b.StartTimer()
+                                               remaining := n
+                                               for remaining > 0 {
+                                                       count := min(batchSize, 
remaining)
+                                                       var decoded int
+                                                       var err error
+                                                       if discard {
+                                                               decoded, err = 
dec.Discard(count)
+                                                       } else {
+                                                               decoded, err = 
dec.Decode(out[:count])
+                                                       }
+                                                       if err != nil || 
decoded != count {
+                                                               
b.Fatalf("processed %d of %d values: %v", decoded, count, err)
+                                                       }
+                                                       remaining -= count
+                                               }
+                                       }
+                               })
+                       }
+               }
+       }
+}

Reply via email to