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 68c7223d perf(parquet): reuse boolean spaced scratch (#1231)
68c7223d is described below
commit 68c7223dcff880f81e27a8c2a9954de50f46dff2
Author: Minh Vu <[email protected]>
AuthorDate: Fri Aug 28 19:24:08 2026 +0200
perf(parquet): reuse boolean spaced scratch (#1231)
## What
- Reuse a `[]bool` scratch buffer in `PutSpaced` for Plain and RLE
boolean encoders.
- Reuse the same scratch for RLE `PutSpacedBitmap` writes.
- Keep the scratch capacity across repeated page writes.
## Benchmark
Apple M1 Pro, Go 1.26.3. The same encoder is reused, and scratch growth
happens before the timed loop.
Command: `go test -vet=off ./parquet/internal/encoding -run "^$" -bench
"^Benchmark(Plain|Rle)BooleanEncoderPutSpaced$" -benchmem
-benchtime=300ms -count=3 -cpu=1`
All-valid input, before vs after:
| Encoder | Values | Before | After |
| --- | ---: | ---: | ---: |
| Plain | 1,024 | 1,024 B/op, 1 alloc | 0 B/op, 0 allocs |
| Plain | 65,536 | 65,539 B/op, 1 alloc | 0 B/op, 0 allocs |
| Plain | 1,048,576 | 1,048,620 B/op, 1 alloc | 0 B/op, 0 allocs |
| RLE | 1,024 | 1,232 B/op, 4 allocs | 208 B/op, 3 allocs |
| RLE | 65,536 | 65,747 B/op, 4 allocs | 208 B/op, 3 allocs |
## Tests
- `go test ./parquet/internal/encoding -count=1`
- `PARQUET_TEST_DATA=parquet-testing/data go test ./... -count=1`
---
parquet/internal/encoding/boolean_encoder.go | 37 ++++++---
.../boolean_encoder_spaced_benchmark_test.go | 96 ++++++++++++++++++++++
2 files changed, 122 insertions(+), 11 deletions(-)
diff --git a/parquet/internal/encoding/boolean_encoder.go
b/parquet/internal/encoding/boolean_encoder.go
index a4111804..0f7fcc8b 100644
--- a/parquet/internal/encoding/boolean_encoder.go
+++ b/parquet/internal/encoding/boolean_encoder.go
@@ -35,8 +35,9 @@ const (
// PlainBooleanEncoder encodes bools as a bitmap as per the Plain Encoding
type PlainBooleanEncoder struct {
encoder
- bitsBuffer []byte
- wr utils.BitmapWriter
+ bitsBuffer []byte
+ spacedScratch []bool
+ wr utils.BitmapWriter
}
// Type for the PlainBooleanEncoder is parquet.Types.Boolean
@@ -113,9 +114,13 @@ func (enc *PlainBooleanEncoder) putBitmapScalar(bitmap
[]byte, offset, length in
// PutSpaced will use the validBits bitmap to determine which values are nulls
// and can be left out from the slice, and the encoded without those nulls.
func (enc *PlainBooleanEncoder) PutSpaced(in []bool, validBits []byte,
validBitsOffset int64) {
- bufferOut := make([]bool, len(in))
- nvalid := spacedCompress(in, bufferOut, validBits, validBitsOffset)
- enc.Put(bufferOut[:nvalid])
+ if cap(enc.spacedScratch) < len(in) {
+ enc.spacedScratch = make([]bool, len(in))
+ } else {
+ enc.spacedScratch = enc.spacedScratch[:len(in)]
+ }
+ nvalid := spacedCompress(in, enc.spacedScratch, validBits,
validBitsOffset)
+ enc.Put(enc.spacedScratch[:nvalid])
}
// PutSpacedBitmap encodes boolean values directly from a bitmap with validity
information,
@@ -186,6 +191,7 @@ type RleBooleanEncoder struct {
encoder
bufferedValues []bool
+ spacedScratch []bool
}
func (RleBooleanEncoder) Type() parquet.Type {
@@ -197,9 +203,13 @@ func (enc *RleBooleanEncoder) Put(in []bool) {
}
func (enc *RleBooleanEncoder) PutSpaced(in []bool, validBits []byte,
validBitsOffset int64) {
- bufferOut := make([]bool, len(in))
- nvalid := spacedCompress(in, bufferOut, validBits, validBitsOffset)
- enc.Put(bufferOut[:nvalid])
+ if cap(enc.spacedScratch) < len(in) {
+ enc.spacedScratch = make([]bool, len(in))
+ } else {
+ enc.spacedScratch = enc.spacedScratch[:len(in)]
+ }
+ nvalid := spacedCompress(in, enc.spacedScratch, validBits,
validBitsOffset)
+ enc.Put(enc.spacedScratch[:nvalid])
}
// PutSpacedBitmap encodes boolean values from a bitmap with validity
information.
@@ -219,7 +229,12 @@ func (enc *RleBooleanEncoder) PutSpacedBitmap(bitmap
[]byte, bitmapOffset int64,
// Extract valid bits to []bool for buffering
// Use SetBitRunReader to efficiently iterate over valid values
- bufferOut := make([]bool, numValid)
+ nvalid := int(numValid)
+ if cap(enc.spacedScratch) < nvalid {
+ enc.spacedScratch = make([]bool, nvalid)
+ } else {
+ enc.spacedScratch = enc.spacedScratch[:nvalid]
+ }
idx := 0
reader := bitutils.NewSetBitRunReader(validBits, validBitsOffset,
numValues)
@@ -231,12 +246,12 @@ func (enc *RleBooleanEncoder) PutSpacedBitmap(bitmap
[]byte, bitmapOffset int64,
// Convert this run of bits to bools
for i := int64(0); i < run.Length; i++ {
- bufferOut[idx] = bitutil.BitIsSet(bitmap,
int(bitmapOffset+run.Pos+i))
+ enc.spacedScratch[idx] = bitutil.BitIsSet(bitmap,
int(bitmapOffset+run.Pos+i))
idx++
}
}
- enc.Put(bufferOut)
+ enc.Put(enc.spacedScratch[:idx])
return numValid
}
diff --git a/parquet/internal/encoding/boolean_encoder_spaced_benchmark_test.go
b/parquet/internal/encoding/boolean_encoder_spaced_benchmark_test.go
new file mode 100644
index 00000000..092b0c09
--- /dev/null
+++ b/parquet/internal/encoding/boolean_encoder_spaced_benchmark_test.go
@@ -0,0 +1,96 @@
+// 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 (
+ "fmt"
+ "testing"
+
+ "github.com/apache/arrow-go/v18/arrow/bitutil"
+ "github.com/apache/arrow-go/v18/arrow/memory"
+ "github.com/apache/arrow-go/v18/parquet"
+)
+
+type booleanSpacedEncoder interface {
+ TypedEncoder
+ PutSpaced([]bool, []byte, int64)
+}
+
+func BenchmarkPlainBooleanEncoderPutSpaced(b *testing.B) {
+ benchmarkBooleanEncoderPutSpaced(b, parquet.Encodings.Plain)
+}
+
+func BenchmarkRleBooleanEncoderPutSpaced(b *testing.B) {
+ benchmarkBooleanEncoderPutSpaced(b, parquet.Encodings.RLE)
+}
+
+func benchmarkBooleanEncoderPutSpaced(b *testing.B, enc parquet.Encoding) {
+ patterns := []struct {
+ name string
+ valid func(int) bool
+ }{
+ {name: "all_valid", valid: func(int) bool { return true }},
+ {name: "ten_percent_null", valid: func(i int) bool { return
i%10 != 0 }},
+ {name: "fifty_percent_null", valid: func(i int) bool { return
i%2 != 0 }},
+ {name: "ninety_percent_null", valid: func(i int) bool { return
i%10 == 0 }},
+ }
+
+ for _, length := range []int{1024, 64 * 1024, 1024 * 1024} {
+ for _, pattern := range patterns {
+ b.Run(fmt.Sprintf("length_%d/%s", length,
pattern.name), func(b *testing.B) {
+ values := make([]bool, length)
+ for i := range values {
+ values[i] = i%3 == 0
+ }
+ validBits := make([]byte,
bitutil.BytesForBits(int64(length)))
+ for i := range length {
+ if pattern.valid(i) {
+ bitutil.SetBit(validBits, i)
+ }
+ }
+
+ encoder := NewEncoder(
+ parquet.Types.Boolean, enc, false, nil,
memory.DefaultAllocator,
+ ).(booleanSpacedEncoder)
+ defer encoder.Release()
+
+ encoder.PutSpaced(values, validBits, 0)
+ flushBooleanEncoder(b, encoder)
+
+ b.ReportAllocs()
+ b.SetBytes(int64(length))
+ b.ResetTimer()
+ for b.Loop() {
+ encoder.PutSpaced(values, validBits, 0)
+ flushBooleanEncoder(b, encoder)
+ }
+ })
+ }
+ }
+}
+
+func flushBooleanEncoder(b *testing.B, encoder booleanSpacedEncoder) {
+ buf, err := encoder.FlushValues()
+ if err != nil {
+ b.Fatal(err)
+ }
+ buf.Release()
+
+ if enc, ok := encoder.(*RleBooleanEncoder); ok {
+ enc.bufferedValues = enc.bufferedValues[:0]
+ }
+}