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 11dd9545 perf(parquet): copy DELTA_LENGTH Arrow payloads by valid runs 
(#1392)
11dd9545 is described below

commit 11dd954594c6c176595d8649ed6c3ccfeaf072a2
Author: Minh Vu <[email protected]>
AuthorDate: Wed Oct 21 22:05:39 2026 +0200

    perf(parquet): copy DELTA_LENGTH Arrow payloads by valid runs (#1392)
    
    ### What
    - Copy DELTA_LENGTH_BYTE_ARRAY payload bytes once per contiguous valid
    run.
    
    ### Why
    - Writing each short value separately adds unnecessary work.
    
    ### Implementation
    - Use one bulk copy for null-free arrays and one copy per valid bitmap
    run.
    - Keep the length encoding unchanged.
    
    ### Benchmark
    `BenchmarkDeltaLengthArrowContiguousRuns`, 65,536 null-free values; Go
    1.26.3, Apple M1 Pro, median of 5 runs.
    
    | Metric | Before | After | Change |
    |---|---:|---:|---:|
    | 4-byte values (µs/op) | 819 | 487 | 1.68x faster |
    | 16-byte values (µs/op) | 880 | 561 | 1.57x faster |
    | Allocations (allocs/op) | 2,054 | 2,054 | No change |
    
    This measures the DELTA_LENGTH encoder path, not full Parquet file
    encoding.
---
 .../internal/encoding/byte_array_arrow_encoder.go  |  10 +-
 .../encoding/byte_array_arrow_encoder_test.go      | 117 +++++++++++++++++++++
 2 files changed, 121 insertions(+), 6 deletions(-)

diff --git a/parquet/internal/encoding/byte_array_arrow_encoder.go 
b/parquet/internal/encoding/byte_array_arrow_encoder.go
index c9ec4b8f..fa0ec57c 100644
--- a/parquet/internal/encoding/byte_array_arrow_encoder.go
+++ b/parquet/internal/encoding/byte_array_arrow_encoder.go
@@ -102,9 +102,8 @@ func putArrowDeltaLength[T arrowByteArrayOffset](enc 
*DeltaLengthByteArrayEncode
                }
 
                enc.sink.Reserve(totalLen)
-               for i := 0; i < len(offsets)-1; i++ {
-                       
enc.sink.UnsafeWrite(values[int(offsets[i]):int(offsets[i+1])])
-               }
+               // Arrow offsets describe one contiguous byte range when there 
are no nulls.
+               
enc.sink.UnsafeWrite(values[int(offsets[0]):int(offsets[len(offsets)-1])])
                return
        }
 
@@ -130,9 +129,8 @@ func putArrowDeltaLength[T arrowByteArrayOffset](enc 
*DeltaLengthByteArrayEncode
 
        enc.sink.Reserve(totalLen)
        bitutils.VisitSetBitRunsNoErr(validBits, validBitsOffset, 
int64(len(offsets)-1), func(pos, length int64) {
-               for i := pos; i < pos+length; i++ {
-                       
enc.sink.UnsafeWrite(values[int(offsets[i]):int(offsets[i+1])])
-               }
+               // A valid run is contiguous; the gaps between runs can hold 
null payloads.
+               
enc.sink.UnsafeWrite(values[int(offsets[pos]):int(offsets[pos+length])])
        })
 }
 
diff --git a/parquet/internal/encoding/byte_array_arrow_encoder_test.go 
b/parquet/internal/encoding/byte_array_arrow_encoder_test.go
index 30a7e528..16f76ed1 100644
--- a/parquet/internal/encoding/byte_array_arrow_encoder_test.go
+++ b/parquet/internal/encoding/byte_array_arrow_encoder_test.go
@@ -270,6 +270,123 @@ func benchmarkPlainByteArrayPutArrow[T 
arrowByteArrayOffset](b *testing.B) {
        }
 }
 
+// Exercise direct Arrow buffer writes with non-zero offsets, batches that
+// straddle a DELTA miniblock, and runs separated by non-empty null payloads.
+func TestDeltaLengthArrowContiguousRuns(t *testing.T) {
+       t.Run("int32", testDeltaLengthArrowContiguousRuns[int32])
+       t.Run("int64", testDeltaLengthArrowContiguousRuns[int64])
+}
+
+func testDeltaLengthArrowContiguousRuns[T arrowByteArrayOffset](t *testing.T) {
+       const n = 2*deltaByteArrayBatchSize + 17
+       data := []byte("discard before first value")
+       offsets := make([]T, n+1)
+       values := make([]parquet.ByteArray, n)
+       for i := range values {
+               offsets[i] = T(len(data))
+               values[i] = make([]byte, (i*17+i/13)%19)
+               for j := range values[i] {
+                       values[i][j] = byte(i*7 + j)
+               }
+               data = append(data, values[i]...)
+       }
+       offsets[n] = T(len(data))
+
+       for _, span := range []struct {
+               name       string
+               begin, end int
+       }{
+               {"whole", 0, n},
+               {"sliced", 11, n - 9},
+               {"single", 47, 48},
+               {"empty", 47, 47},
+       } {
+               for _, pattern := range []string{"all", "all-valid-bitmap", 
"none", "alternating", "long-runs"} {
+                       t.Run(span.name+"/"+pattern, func(t *testing.T) {
+                               var bits []byte
+                               if pattern != "all" {
+                                       bits = make([]byte, 
bitutil.BytesForBits(int64(span.end-span.begin+3)))
+                               }
+                               want := []parquet.ByteArray{[]byte("before")}
+                               for i := span.begin; i < span.end; i++ {
+                                       valid := pattern == "all" || pattern == 
"all-valid-bitmap" ||
+                                               (pattern == "alternating" && 
i%2 == 0) ||
+                                               (pattern == "long-runs" && 
(i/31)%2 == 0)
+                                       if valid {
+                                               want = append(want, values[i])
+                                               if bits != nil {
+                                                       bitutil.SetBit(bits, 
3+i-span.begin)
+                                               }
+                                       }
+                               }
+                               want = append(want, []byte("after"))
+
+                               mem := 
memory.NewCheckedAllocator(memory.DefaultAllocator)
+                               defer mem.AssertSize(t, 0)
+                               enc := NewEncoder(parquet.Types.ByteArray, 
parquet.Encodings.DeltaLengthByteArray,
+                                       false, nil, 
mem).(*DeltaLengthByteArrayEncoder)
+                               defer enc.Release()
+                               enc.Put([]parquet.ByteArray{[]byte("before")})
+                               middle := span.begin + (span.end-span.begin)/2
+                               for _, part := range [][2]int{{span.begin, 
middle}, {middle, span.end}} {
+                                       putArrowDeltaLength(enc, data, 
offsets[part[0]:part[1]+1], bits,
+                                               int64(3+part[0]-span.begin))
+                               }
+                               enc.Put([]parquet.ByteArray{[]byte("after")})
+                               got, err := enc.FlushValues()
+                               require.NoError(t, err)
+                               defer got.Release()
+                               require.Equal(t, encodedByteArrays(t, 
parquet.Encodings.DeltaLengthByteArray,
+                                       want, false, nil, 0), got.Bytes())
+                       })
+               }
+       }
+}
+
+func BenchmarkDeltaLengthArrowContiguousRuns(b *testing.B) {
+       const n = 64 * 1024
+       for _, width := range []int{0, 4, 16, 64} {
+               for _, pattern := range []string{"all", "long-runs", 
"alternating"} {
+                       b.Run(fmt.Sprintf("bytes=%d/%s", width, pattern), 
func(b *testing.B) {
+                               data := make([]byte, 7+n*width)
+                               offsets := make([]int32, n+1)
+                               for i := range offsets {
+                                       offsets[i] = int32(7 + i*width)
+                               }
+                               var bits []byte
+                               if pattern != "all" {
+                                       bits = make([]byte, 
bitutil.BytesForBits(n+3))
+                                       for i := 0; i < n; i++ {
+                                               valid := pattern == 
"alternating" && i%2 != 0 ||
+                                                       pattern == "long-runs" 
&& (i/128)%2 == 0
+                                               if valid {
+                                                       bitutil.SetBit(bits, 
i+3)
+                                               }
+                                       }
+                               }
+
+                               enc := NewEncoder(parquet.Types.ByteArray, 
parquet.Encodings.DeltaLengthByteArray,
+                                       false, nil, 
memory.DefaultAllocator).(*DeltaLengthByteArrayEncoder)
+                               defer enc.Release()
+                               b.SetBytes(int64(n * width))
+                               b.ReportAllocs()
+                               for b.Loop() {
+                                       if bits == nil {
+                                               enc.PutArrow(data, offsets)
+                                       } else {
+                                               enc.PutArrowSpaced(data, 
offsets, bits, 3)
+                                       }
+                                       buf, err := enc.FlushValues()
+                                       if err != nil {
+                                               b.Fatal(err)
+                                       }
+                                       buf.Release()
+                               }
+                       })
+               }
+       }
+}
+
 func TestDeltaByteArrayArrowEncoderPreservesStateAcrossBatches(t *testing.T) {
        values, data, offsets32, offsets64 := arrowByteArrayInput()
 

Reply via email to