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 2a18bcee perf(parquet): encode RLE levels in batches (#1177)
2a18bcee is described below

commit 2a18bcee01b9194f0820523ae62bb90e0603a5f2
Author: Minh Vu <[email protected]>
AuthorDate: Wed Aug 26 20:11:02 2026 +0200

    perf(parquet): encode RLE levels in batches (#1177)
    
    ### What changed
    
    - add a batch path for repetition and definition level encoding
    - scan repeated runs once instead of calling the scalar encoder for
    every level
    - keep the scalar path around literal boundaries
    - preserve the existing encoded bytes across split streaming calls
    - add level encoder and scalar-versus-batch benchmarks
    
    ### Benchmarks
    
    Apple M1 Pro:
    
    | RLE workload | Before | After | Change |
    | --- | ---: | ---: | ---: |
    | 65,536 all defined | 151,051 ns/op | 31,143 ns/op | -79% |
    | 65,536 mostly defined | 324,228 ns/op | 252,108 ns/op | -22% |
    | 65,536 alternating | 339,128 ns/op | 341,149 ns/op | +0.6% |
    
    The alternating case is the fallback path. Allocations remain at zero.
---
 parquet/internal/encoding/levels.go                |  14 +--
 .../encoding/levels_encode_benchmark_test.go       | 138 +++++++++++++++++++++
 parquet/internal/utils/bit_reader_test.go          |  59 +++++++++
 parquet/internal/utils/rle.go                      |  55 +++++++-
 parquet/internal/utils/rle_internal_test.go        |  76 ++++++++++++
 5 files changed, 329 insertions(+), 13 deletions(-)

diff --git a/parquet/internal/encoding/levels.go 
b/parquet/internal/encoding/levels.go
index e88a18ef..3da9a575 100644
--- a/parquet/internal/encoding/levels.go
+++ b/parquet/internal/encoding/levels.go
@@ -98,12 +98,7 @@ func (l *LevelEncoder) EncodeNoFlush(lvls []int16) (nencoded 
int, err error) {
 
        switch l.encoding {
        case format.Encoding_RLE:
-               for _, level := range lvls {
-                       if err = l.rle.Put(uint64(level)); err != nil {
-                               return
-                       }
-                       nencoded++
-               }
+               nencoded, err = l.rle.PutBatchLevels(lvls)
        default:
                for _, level := range lvls {
                        if err = l.bit.WriteValue(uint64(level), 
uint(l.bitWidth)); err != nil {
@@ -140,12 +135,7 @@ func (l *LevelEncoder) Encode(lvls []int16) (nencoded int, 
err error) {
        switch l.encoding {
        case format.Encoding_RLE:
                defer func() { l.rleLen = l.rle.Flush() }()
-               for _, level := range lvls {
-                       if err = l.rle.Put(uint64(level)); err != nil {
-                               return
-                       }
-                       nencoded++
-               }
+               nencoded, err = l.rle.PutBatchLevels(lvls)
 
        default:
                defer l.bit.Flush(false)
diff --git a/parquet/internal/encoding/levels_encode_benchmark_test.go 
b/parquet/internal/encoding/levels_encode_benchmark_test.go
new file mode 100644
index 00000000..3b03b503
--- /dev/null
+++ b/parquet/internal/encoding/levels_encode_benchmark_test.go
@@ -0,0 +1,138 @@
+// 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_test
+
+import (
+       "fmt"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/arrow-go/v18/parquet"
+       "github.com/apache/arrow-go/v18/parquet/internal/encoding"
+       parquetutils "github.com/apache/arrow-go/v18/parquet/internal/utils"
+)
+
+func BenchmarkLevelEncoder(b *testing.B) {
+       patterns := []struct {
+               name string
+               fill func([]int16, int16)
+       }{
+               {"all_defined", func(levels []int16, maxLevel int16) {
+                       for i := range levels {
+                               levels[i] = maxLevel
+                       }
+               }},
+               {"mostly_defined", func(levels []int16, maxLevel int16) {
+                       for i := range levels {
+                               if i%20 != 0 {
+                                       levels[i] = maxLevel
+                               }
+                       }
+               }},
+               {"alternating", func(levels []int16, maxLevel int16) {
+                       for i := range levels {
+                               if i%2 != 0 {
+                                       levels[i] = maxLevel
+                               }
+                       }
+               }},
+       }
+
+       for _, size := range []int{1024, 64 * 1024} {
+               for _, maxLevel := range []int16{1, 3} {
+                       for _, pattern := range patterns {
+                               b.Run(fmt.Sprintf("%s/max_level=%d/levels=%d", 
pattern.name, maxLevel, size), func(b *testing.B) {
+                                       levels := make([]int16, size)
+                                       pattern.fill(levels, maxLevel)
+                                       output := 
encoding.NewBufferWriter(encoding.LevelEncodingMaxBufferSize(parquet.Encodings.RLE,
 maxLevel, size), memory.DefaultAllocator)
+                                       defer output.Release()
+
+                                       var encoder encoding.LevelEncoder
+                                       encoder.Init(parquet.Encodings.RLE, 
maxLevel, output)
+                                       b.ReportAllocs()
+                                       b.SetBytes(int64(len(levels) * 
arrow.Int16SizeBytes))
+                                       b.ResetTimer()
+                                       for b.Loop() {
+                                               encoder.Reset(maxLevel)
+                                               encoded, err := 
encoder.Encode(levels)
+                                               if err != nil {
+                                                       b.Fatal(err)
+                                               }
+                                               if encoded != size {
+                                                       b.Fatalf("encoded %d 
levels, want %d", encoded, size)
+                                               }
+                                       }
+                               })
+                       }
+               }
+       }
+}
+
+func BenchmarkRleLevelEncoder(b *testing.B) {
+       patterns := []struct {
+               name string
+               fill func([]int16)
+       }{
+               {"all_defined", func(levels []int16) {
+                       for i := range levels {
+                               levels[i] = 1
+                       }
+               }},
+               {"mostly_defined", func(levels []int16) {
+                       for i := range levels {
+                               if i%20 != 0 {
+                                       levels[i] = 1
+                               }
+                       }
+               }},
+               {"alternating", func(levels []int16) {
+                       for i := range levels {
+                               levels[i] = int16(i % 2)
+                       }
+               }},
+       }
+
+       for _, pattern := range patterns {
+               b.Run(pattern.name, func(b *testing.B) {
+                       levels := make([]int16, 64*1024)
+                       pattern.fill(levels)
+                       for _, mode := range []string{"scalar", "batch"} {
+                               b.Run(mode, func(b *testing.B) {
+                                       output := make([]byte, 
parquetutils.MaxRLEBufferSize(1, len(levels)))
+                                       encoder := 
parquetutils.NewRleEncoder(parquetutils.NewWriterAtBuffer(output), 1)
+
+                                       b.ReportAllocs()
+                                       b.SetBytes(int64(len(levels) * 
arrow.Int16SizeBytes))
+                                       for b.Loop() {
+                                               encoder.Clear()
+                                               if mode == "scalar" {
+                                                       for _, level := range 
levels {
+                                                               if err := 
encoder.Put(uint64(level)); err != nil {
+                                                                       
b.Fatal(err)
+                                                               }
+                                                       }
+                                               } else if _, err := 
encoder.PutBatchLevels(levels); err != nil {
+                                                       b.Fatal(err)
+                                               }
+                                               encoder.Flush()
+                                       }
+                               })
+                       }
+               })
+       }
+}
diff --git a/parquet/internal/utils/bit_reader_test.go 
b/parquet/internal/utils/bit_reader_test.go
index 0468ff13..44804c24 100644
--- a/parquet/internal/utils/bit_reader_test.go
+++ b/parquet/internal/utils/bit_reader_test.go
@@ -661,6 +661,65 @@ func TestRleRandom(t *testing.T) {
        suite.Run(t, new(RLERandomSuite))
 }
 
+func TestRleBatchLevelsMatchesScalar(t *testing.T) {
+       patterns := map[string][]int16{
+               "all defined":      make([]int16, 257),
+               "alternating":      make([]int16, 257),
+               "long alternating": make([]int16, 63*8+17),
+               "literal boundary": make([]int16, 63*8+17),
+               "mixed runs":       {},
+       }
+       for i := range patterns["all defined"] {
+               patterns["all defined"][i] = 3
+               patterns["alternating"][i] = int16(i % 2)
+       }
+       for i := range patterns["long alternating"] {
+               patterns["long alternating"][i] = int16(i % 2)
+       }
+       for i := range patterns["literal boundary"] {
+               if i < 63*8-3 {
+                       patterns["literal boundary"][i] = int16(i % 2)
+               } else {
+                       patterns["literal boundary"][i] = 3
+               }
+       }
+       for runLength := 1; runLength <= 32; runLength++ {
+               for range runLength {
+                       patterns["mixed runs"] = append(patterns["mixed runs"], 
int16(runLength%4))
+               }
+       }
+
+       for name, levels := range patterns {
+               t.Run(name, func(t *testing.T) {
+                       bufSize := utils.MaxRLEBufferSize(2, len(levels))
+                       scalarBuf := make([]byte, bufSize)
+                       scalar := 
utils.NewRleEncoder(utils.NewWriterAtBuffer(scalarBuf), 2)
+                       for _, level := range levels {
+                               if err := scalar.Put(uint64(level)); err != nil 
{
+                                       t.Fatal(err)
+                               }
+                       }
+                       scalarLen := scalar.Flush()
+
+                       batchBuf := make([]byte, bufSize)
+                       batch := 
utils.NewRleEncoder(utils.NewWriterAtBuffer(batchBuf), 2)
+                       chunkSizes := []int{1, 7, 8, 9, 31}
+                       for offset, chunk := 0, 0; offset < len(levels); 
chunk++ {
+                               end := 
min(offset+chunkSizes[chunk%len(chunkSizes)], len(levels))
+                               n, err := 
batch.PutBatchLevels(levels[offset:end])
+                               if err != nil {
+                                       t.Fatal(err)
+                               }
+                               assert.Equal(t, end-offset, n)
+                               offset = end
+                       }
+                       batchLen := batch.Flush()
+
+                       assert.Equal(t, scalarBuf[:scalarLen], 
batchBuf[:batchLen])
+               })
+       }
+}
+
 func (r *RLETestSuite) ValidateRle(vals []uint64, width int, expected []byte, 
explen int) {
        const buflen = 64 * 1024
        buf := make([]byte, buflen)
diff --git a/parquet/internal/utils/rle.go b/parquet/internal/utils/rle.go
index 596f2018..29013c53 100644
--- a/parquet/internal/utils/rle.go
+++ b/parquet/internal/utils/rle.go
@@ -407,7 +407,7 @@ func (r *RleEncoder) flushLiteral(updateIndicator bool) 
(err error) {
 }
 
 func (r *RleEncoder) flushRepeated() (ret bool) {
-       indicator := r.repCount << 1
+       indicator := uint64(r.repCount) << 1
 
        ret = r.w.WriteVlqInt(uint64(indicator))
        ret = ret && r.w.WriteAligned(r.curVal, 
int(bitutil.BytesForBits(int64(r.BitWidth))))
@@ -444,6 +444,59 @@ func (r *RleEncoder) Put(value uint64) error {
        return nil
 }
 
+// PutBatchLevels encodes a batch of repetition or definition levels.
+func (r *RleEncoder) PutBatchLevels(values []int16) (int, error) {
+       encoded := 0
+       for encoded < len(values) {
+               value := values[encoded]
+               if r.repCount >= 8 && r.curVal == uint64(value) {
+                       runEnd := encoded + 1
+                       for runEnd < len(values) && values[runEnd] == value {
+                               runEnd++
+                       }
+
+                       runLength := min(runEnd-encoded, 
int(math.MaxInt32-r.repCount))
+                       r.repCount += int32(runLength)
+                       encoded += runLength
+                       if r.repCount == math.MaxInt32 && encoded < len(values) 
&& values[encoded] == value {
+                               if !r.flushRepeated() {
+                                       return encoded, errors.New("failed to 
flush repeated value")
+                               }
+                       }
+                       continue
+               }
+               if r.repCount == 0 && len(r.buffer) == 0 && len(values)-encoded 
>= 8 && values[encoded+7] == value {
+                       runEnd := encoded + 1
+                       for runEnd < len(values) && values[runEnd] == value {
+                               runEnd++
+                       }
+                       if runEnd-encoded >= 8 {
+                               r.curVal = uint64(value)
+                               if r.litCount != 0 {
+                                       r.repCount = 8
+                                       if err := r.flushLiteral(true); err != 
nil {
+                                               return encoded, err
+                                       }
+                                       encoded += 8
+                               }
+                               runLength := min(runEnd-encoded, 
int(math.MaxInt32-r.repCount))
+                               r.repCount += int32(runLength)
+                               encoded += runLength
+                               continue
+                       }
+               }
+
+               batchEnd := min(len(values), encoded+8-len(r.buffer))
+               for encoded < batchEnd {
+                       if err := r.Put(uint64(values[encoded])); err != nil {
+                               return encoded, err
+                       }
+                       encoded++
+               }
+       }
+       return encoded, nil
+}
+
 func (r *RleEncoder) Clear() {
        r.curVal = 0
        r.repCount = 0
diff --git a/parquet/internal/utils/rle_internal_test.go 
b/parquet/internal/utils/rle_internal_test.go
new file mode 100644
index 00000000..db02f9cf
--- /dev/null
+++ b/parquet/internal/utils/rle_internal_test.go
@@ -0,0 +1,76 @@
+// 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 utils
+
+import (
+       "math"
+       "testing"
+)
+
+func TestRleEncoderMaximumRepeatedRun(t *testing.T) {
+       buf := make([]byte, 16)
+       enc := NewRleEncoder(NewWriterAtBuffer(buf), 1)
+       enc.curVal = 1
+       enc.repCount = math.MaxInt32
+
+       if n := enc.Flush(); n != 6 {
+               t.Fatalf("encoded %d bytes, want 6", n)
+       }
+       want := []byte{0xfe, 0xff, 0xff, 0xff, 0x0f, 1}
+       for i, value := range want {
+               if buf[i] != value {
+                       t.Fatalf("byte %d = %#x, want %#x", i, buf[i], value)
+               }
+       }
+}
+
+func TestRleEncoderBatchSplitsMaximumRepeatedRun(t *testing.T) {
+       buf := make([]byte, 16)
+       enc := NewRleEncoder(NewWriterAtBuffer(buf), 1)
+       enc.curVal = 1
+       enc.repCount = math.MaxInt32 - 4
+
+       n, err := enc.PutBatchLevels([]int16{1, 1, 1, 1, 1, 1})
+       if err != nil {
+               t.Fatal(err)
+       }
+       if n != 6 {
+               t.Fatalf("encoded %d values, want 6", n)
+       }
+       if n := enc.Flush(); n != 8 {
+               t.Fatalf("encoded %d bytes, want 8", n)
+       }
+
+       want := []byte{0xfe, 0xff, 0xff, 0xff, 0x0f, 1, 4, 1}
+       for i, value := range want {
+               if buf[i] != value {
+                       t.Fatalf("byte %d = %#x, want %#x", i, buf[i], value)
+               }
+       }
+}
+
+func TestRleEncoderBatchReportsValuesBeforeFlushError(t *testing.T) {
+       enc := NewRleEncoder(NewWriterAtBuffer(nil), 1)
+       values := []int16{0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1}
+
+       n, err := enc.PutBatchLevels(values)
+       if err == nil {
+               t.Fatal("expected a write error")
+       }
+       if n != 8 {
+               t.Fatalf("encoded %d values, want 8", n)
+       }
+}

Reply via email to