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 85610f71 perf(parquet/internal/utils): pack narrow RLE literal levels
eight at a time (#1419)
85610f71 is described below
commit 85610f718a241fa3f77e77650e889f0846fe4c58
Author: Minh Vu <[email protected]>
AuthorDate: Tue Oct 27 17:19:07 2026 +0100
perf(parquet/internal/utils): pack narrow RLE literal levels eight at a
time (#1419)
## What
Pack narrow Parquet RLE literal levels eight at a time.
## Why
The encoder currently handles each literal level separately.
## Implementation
- Pack full groups directly for widths 1, 2, 4, and 8.
- Keep the existing path for repeated runs, partial groups, and wider
levels.
- Add byte parity, boundary, and fallback coverage with a paired
benchmark.
## Benchmark
**2.61x faster on random levels and 2.25x on alternating levels** in an
isolated encoder model. Other patterns vary.
---------
Co-authored-by: Matt Topol <[email protected]>
---
parquet/internal/utils/rle.go | 50 +++++++
parquet/internal/utils/rle_internal_test.go | 217 ++++++++++++++++++++++++++++
2 files changed, 267 insertions(+)
diff --git a/parquet/internal/utils/rle.go b/parquet/internal/utils/rle.go
index 9b34b350..a1655b12 100644
--- a/parquet/internal/utils/rle.go
+++ b/parquet/internal/utils/rle.go
@@ -609,6 +609,32 @@ func (r *RleEncoder) PutBatchLevels(values []int16) (int,
error) {
}
}
+ // Literal levels usually have a width of only one or two bits.
Pack a
+ // complete group at once instead of feeding eight values
through Put,
+ // then unpacking the uint64 buffer again in WriteValues.
+ if r.repCount == 0 && len(r.buffer) == 0 && len(values)-encoded
>= 8 &&
+ r.BitWidth > 0 && r.BitWidth <= 8 {
+ group := values[encoded : encoded+8]
+ var packed uint64
+ fits := true
+ maxValue := uint64(1<<r.BitWidth) - 1
+ for i, level := range group {
+ if level < 0 || uint64(level) > maxValue {
+ fits = false
+ break
+ }
+ packed |= uint64(level) << (uint(i) *
uint(r.BitWidth))
+ }
+ if fits {
+ if err := r.putPackedLevelGroup(packed); err !=
nil {
+ return encoded + 7, err
+ }
+ r.curVal = uint64(group[7])
+ encoded += 8
+ continue
+ }
+ }
+
batchEnd := min(len(values), encoded+8-len(r.buffer))
for encoded < batchEnd {
if err := r.Put(uint64(values[encoded])); err != nil {
@@ -620,6 +646,30 @@ func (r *RleEncoder) PutBatchLevels(values []int16) (int,
error) {
return encoded, nil
}
+// putPackedLevelGroup writes eight literal levels to the current RLE literal
+// run. PutBatchLevels calls this only when there is no buffered/repeated run,
+// and each level fits in BitWidth bits.
+func (r *RleEncoder) putPackedLevelGroup(packed uint64) error {
+ if r.literalIndicatorOffset == -1 {
+ var err error
+ r.literalIndicatorOffset, err = r.w.SkipBytes(1)
+ if err != nil {
+ return err
+ }
+ }
+
+ if err := r.w.WriteValue(packed, uint(r.BitWidth*8)); err != nil {
+ return err
+ }
+
+ r.litCount += 8
+ // One byte is reserved for the literal header: at most 63 groups.
+ if r.litCount/8+1 >= 1<<6 {
+ return r.flushLiteral(true)
+ }
+ return nil
+}
+
// PutBatchIndices encodes a batch of dictionary indices.
func (r *RleEncoder) PutBatchIndices(values []int32) (int, error) {
encoded := 0
diff --git a/parquet/internal/utils/rle_internal_test.go
b/parquet/internal/utils/rle_internal_test.go
index db02f9cf..a8ad7e52 100644
--- a/parquet/internal/utils/rle_internal_test.go
+++ b/parquet/internal/utils/rle_internal_test.go
@@ -16,6 +16,8 @@
package utils
import (
+ "bytes"
+ "fmt"
"math"
"testing"
)
@@ -74,3 +76,218 @@ func TestRleEncoderBatchReportsValuesBeforeFlushError(t
*testing.T) {
t.Fatalf("encoded %d values, want 8", n)
}
}
+
+func makePackedLevelValues(width, n int, pattern string) []int16 {
+ levels := make([]int16, n)
+ mask := uint32((1 << min(width, 15)) - 1)
+ seed := uint32(0x12345678)
+ for i := range levels {
+ seed ^= seed << 13
+ seed ^= seed >> 17
+ seed ^= seed << 5
+ switch pattern {
+ case "random":
+ levels[i] = int16(seed & mask)
+ case "alternating":
+ levels[i] = int16(uint32(i%2) & mask)
+ case "mostly_defined":
+ if i%20 != 0 {
+ levels[i] = int16(mask)
+ }
+ case "repeated":
+ if (i/13)%2 != 0 {
+ levels[i] = int16(mask)
+ }
+ case "boundary":
+ levels[i] = int16(uint32(i*17+i/3) & mask)
+ }
+ }
+ return levels
+}
+
+func TestRleBatchLevelsPackedMatchesScalar(t *testing.T) {
+ for _, width := range []int{0, 1, 2, 3, 4, 8, 9, 15, 16} {
+ for _, pattern := range []string{"random", "alternating",
"mostly_defined", "repeated", "boundary"} {
+ values := makePackedLevelValues(width, 63*8+17, pattern)
+ t.Run(fmt.Sprintf("width=%d/%s", width, pattern),
func(t *testing.T) {
+ size := MaxRLEBufferSize(width, len(values)) +
MinRLEBufferSize(width) + 32
+ expected := make([]byte, size)
+ scalar :=
NewRleEncoder(NewWriterAtBuffer(expected), width)
+ for _, value := range values {
+ if err := scalar.Put(uint64(value));
err != nil {
+ t.Fatal(err)
+ }
+ }
+ expected = expected[:scalar.Flush()]
+
+ for _, mode := range []string{"whole",
"chunked", "interleaved"} {
+ t.Run(mode, func(t *testing.T) {
+ got := make([]byte, size)
+ enc :=
NewRleEncoder(NewWriterAtBuffer(got), width)
+ chunkSizes := []int{1, 7, 8, 9,
31, 65}
+ for offset, chunk := 0, 0;
offset < len(values); chunk++ {
+ end := len(values)
+ if mode != "whole" {
+ end = min(end,
offset+chunkSizes[chunk%len(chunkSizes)])
+ }
+ if mode ==
"interleaved" && chunk%3 == 0 {
+ for _, value :=
range values[offset:end] {
+ if err
:= enc.Put(uint64(value)); err != nil {
+
t.Fatal(err)
+ }
+ }
+ } else {
+ n, err :=
enc.PutBatchLevels(values[offset:end])
+ if err != nil {
+
t.Fatal(err)
+ }
+ if n !=
end-offset {
+
t.Fatalf("encoded %d, want %d", n, end-offset)
+ }
+ }
+ offset = end
+ }
+ got = got[:enc.Flush()]
+ if !bytes.Equal(got, expected) {
+ t.Fatalf("encoded bytes
differ from scalar at width %d, %s (%d vs %d bytes)", width, pattern, len(got),
len(expected))
+ }
+ decoded := make([]uint64,
len(values))
+ r :=
NewRleDecoder(bytes.NewReader(got), width)
+ n, err := r.GetBatch(decoded)
+ if err != nil || n !=
len(values) {
+ t.Fatalf("decoded %d
values, err %v", n, err)
+ }
+ for i, value := range values {
+ if decoded[i] !=
uint64(value) {
+
t.Fatalf("decoded[%d] = %d, want %d", i, decoded[i], value)
+ }
+ }
+ })
+ }
+ })
+ }
+ }
+}
+
+func TestRleBatchLevelsPackedWriteErrors(t *testing.T) {
+ for _, width := range []int{1, 2, 4, 8} {
+ values := makePackedLevelValues(width, 520, "alternating")
+ for _, capacity := range []int{0, 1, 2, 8, 16, 63, 64, 128,
503} {
+ t.Run(fmt.Sprintf("width=%d/capacity=%d", width,
capacity), func(t *testing.T) {
+ expected, got := make([]byte, capacity),
make([]byte, capacity)
+ scalar :=
NewRleEncoder(NewWriterAtBuffer(expected), width)
+ scalarCount := 0
+ var scalarErr error
+ for _, value := range values {
+ if scalarErr =
scalar.Put(uint64(value)); scalarErr != nil {
+ break
+ }
+ scalarCount++
+ }
+ enc := NewRleEncoder(NewWriterAtBuffer(got),
width)
+ n, err := enc.PutBatchLevels(values)
+ if (err != nil) != (scalarErr != nil) || n !=
scalarCount {
+ t.Fatalf("count/error: got (%d, %v),
want (%d, %v)", n, err, scalarCount, scalarErr)
+ }
+ if !bytes.Equal(got, expected) {
+ t.Fatal("partial encoded output
differs")
+ }
+ })
+ }
+ }
+}
+
+// Compare partial output and errors against encoding each level separately.
+func TestRleBatchLevelsPackedBatchWriteErrors(t *testing.T) {
+ // When a repeated run follows literals, the batch API flushes the
literal
+ // header before consuming the next eight values. Put has already
accepted
+ // seven of those values when it encounters the same error. Keep
explicit
+ // batch counts for these mostly_defined cases (width -> capacity ->
count).
+ flushErrorCounts := map[int]map[int]int{
+ 1: {0: 8, 8: 48, 16: 88, 64: 328},
+ 2: {0: 8},
+ 4: {0: 8, 63: 188},
+ }
+ for _, width := range []int{1, 2, 4, 8} {
+ for _, pattern := range []string{"random", "mostly_defined",
"repeated", "alternating"} {
+ values := makePackedLevelValues(width, 520, pattern)
+ for _, capacity := range []int{0, 1, 2, 8, 16, 63, 64,
128, 503} {
+ t.Run(fmt.Sprintf("width=%d/%s/capacity=%d",
width, pattern, capacity), func(t *testing.T) {
+ expected, got := make([]byte,
capacity), make([]byte, capacity)
+ scalar :=
NewRleEncoder(NewWriterAtBuffer(expected), width)
+ scalarCount := 0
+ var scalarErr error
+ for _, value := range values {
+ if scalarErr =
scalar.Put(uint64(value)); scalarErr != nil {
+ break
+ }
+ scalarCount++
+ }
+ current :=
NewRleEncoder(NewWriterAtBuffer(got), width)
+ n, err := current.PutBatchLevels(values)
+ wantCount := scalarCount
+ if count, ok :=
flushErrorCounts[width][capacity]; pattern == "mostly_defined" && ok {
+ wantCount = count
+ if err == nil {
+ t.Fatal("expected a
literal header write error")
+ }
+ }
+ if n != wantCount || (err != nil) !=
(scalarErr != nil) || !bytes.Equal(expected, got) {
+ t.Fatalf("batch (%d, %v), want
count %d, scalar (%d, %v), bytes equal: %t", n, err, wantCount, scalarCount,
scalarErr, bytes.Equal(expected, got))
+ }
+ })
+ }
+ }
+ }
+}
+
+func TestRleBatchLevelsPackedInvalidValues(t *testing.T) {
+ // Invalid levels preserve the per-value writer's behavior, including
+ // negative input cast directly to uint64.
+ for _, width := range []int{1, 2, 4, 8} {
+ values := []int16{0, 1, 0, 1, 0, 1, 0, -1, 0, 1, 0, 1, 0, 1, 0,
1024}
+ size := MaxRLEBufferSize(width, len(values)) +
MinRLEBufferSize(width) + 32
+ expected, got := make([]byte, size), make([]byte, size)
+ scalar := NewRleEncoder(NewWriterAtBuffer(expected), width)
+ scalarCount := 0
+ var scalarErr error
+ for _, value := range values {
+ if scalarErr = scalar.Put(uint64(value)); scalarErr !=
nil {
+ break
+ }
+ scalarCount++
+ }
+ current := NewRleEncoder(NewWriterAtBuffer(got), width)
+ n, err := current.PutBatchLevels(values)
+ if scalarCount != n || (scalarErr != nil) != (err != nil) ||
+ !bytes.Equal(expected[:scalar.Flush()],
got[:current.Flush()]) {
+ t.Fatalf("width %d: invalid-level output changed",
width)
+ }
+ }
+}
+
+func BenchmarkRleBatchLevelsPacked(b *testing.B) {
+ for _, width := range []int{1, 2, 4, 8, 16} {
+ for _, pattern := range []string{"random", "alternating",
"mostly_defined", "repeated"} {
+ for _, size := range []int{1024, 65536} {
+ values := makePackedLevelValues(width, size,
pattern)
+ b.Run(fmt.Sprintf("width=%d/%s/n=%d", width,
pattern, size), func(b *testing.B) {
+ output := make([]byte,
MaxRLEBufferSize(width, len(values))+MinRLEBufferSize(width)+32)
+ enc :=
NewRleEncoder(NewWriterAtBuffer(output), width)
+ b.SetBytes(int64(len(values) * 2))
+ b.ReportAllocs()
+ for b.Loop() {
+ enc.Clear()
+ n, err :=
enc.PutBatchLevels(values)
+ if err != nil || n !=
len(values) {
+ b.Fatalf("encoded %d
levels: %v", n, err)
+ }
+ if got := enc.Flush(); got >
len(output) {
+ b.Fatalf("encoded %d
bytes into %d byte output", got, len(output))
+ }
+ }
+ })
+ }
+ }
+ }
+}