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 77d27ce5 perf(parquet): reuse dictionary decoder scratch (#1222)
77d27ce5 is described below
commit 77d27ce5ecf819dd4350f59c3ecdf187a1b01e80
Author: Minh Vu <[email protected]>
AuthorDate: Fri Aug 28 21:51:28 2026 +0200
perf(parquet): reuse dictionary decoder scratch (#1222)
## Summary
- Reuse the `[]int` conversion scratch in dictionary index decoding.
- Reuse the `[]bool` validity scratch in spaced dictionary decoding.
- Clear reused validity entries before applying the next bitmap.
- Add repeated dense and nullable decode tests.
- Add a benchmark for 1, 64, 4096, and 65536 values.
## Benchmark
**Command**
```text
go test ./parquet/internal/encoding -run '^$' -bench
'^BenchmarkDictByteArrayDecoderDecodeIndices$' -benchmem -benchtime=100ms
-count=3
```
**Machine:** Apple M1 Pro, arm64
**Go:** 1.26.3
The benchmark warms up one decode, then reuses the same decoder and
dictionary builder for repeated batches. The spaced cases use 25% null
values.
| Case | Before | After |
| --- | ---: | ---: |
| Dense, 1 value | 4,944 B/op, 3 allocs/op | 4,944 B/op, 3 allocs/op |
| Dense, 64 values | 5,712 B/op, 5 allocs/op | 5,200 B/op, 4 allocs/op |
| Dense, 4096 values | 54,096 B/op, 5 allocs/op | 21,328 B/op, 4
allocs/op |
| Dense, 65536 values | 791,378 B/op, 5 allocs/op | 267,089 B/op, 4
allocs/op |
| Spaced, 64 values | 9,936 B/op, 8 allocs/op | 9,360 B/op, 6 allocs/op
|
| Spaced, 4096 values | 58,192 B/op, 6 allocs/op | 21,328 B/op, 4
allocs/op |
| Spaced, 65536 values | 856,912 B/op, 6 allocs/op | 267,088 B/op, 4
allocs/op |
## Tests
- `go test ./parquet/internal/encoding`
- `go test ./parquet/file`
---
parquet/internal/encoding/decoder.go | 37 ++--
.../internal/encoding/decoder_benchmark_test.go | 198 +++++++++++++++++++++
2 files changed, 224 insertions(+), 11 deletions(-)
diff --git a/parquet/internal/encoding/decoder.go
b/parquet/internal/encoding/decoder.go
index 9c124da2..90ab6cbd 100644
--- a/parquet/internal/encoding/decoder.go
+++ b/parquet/internal/encoding/decoder.go
@@ -108,7 +108,9 @@ type dictDecoder[T parquet.ColumnTypes] struct {
dictValueDecoder utils.DictionaryConverter[T]
idxDecoder *utils.TypedRleDecoder[T]
- idxScratchSpace []uint64
+ idxScratchSpace []uint64
+ idxAppendScratch []int
+ validScratch []bool
}
// SetDict sets a decoder that can be used to decode the dictionary that is
@@ -169,11 +171,15 @@ func (d *dictDecoder[T]) DecodeIndices(numValues int,
bldr array.Builder) (int,
n, err := d.idxDecoder.GetBatch(d.idxScratchSpace)
- toAppend := make([]int, n)
- for i, v := range d.idxScratchSpace {
- toAppend[i] = int(v)
+ if cap(d.idxAppendScratch) < n {
+ d.idxAppendScratch = make([]int, n, bitutil.NextPowerOf2(n))
+ } else {
+ d.idxAppendScratch = d.idxAppendScratch[:n]
+ }
+ for i, v := range d.idxScratchSpace[:n] {
+ d.idxAppendScratch[i] = int(v)
}
- bldr.(*array.BinaryDictionaryBuilder).AppendIndices(toAppend, nil)
+ bldr.(*array.BinaryDictionaryBuilder).AppendIndices(d.idxAppendScratch,
nil)
d.nvals -= n
return n, err
}
@@ -190,15 +196,24 @@ func (d *dictDecoder[T]) DecodeIndicesSpaced(numValues,
nullCount int, validBits
return n, err
}
- valid := make([]bool, n)
+ if cap(d.validScratch) < n {
+ d.validScratch = make([]bool, n, bitutil.NextPowerOf2(n))
+ } else {
+ d.validScratch = d.validScratch[:n]
+ }
+ clear(d.validScratch)
bitutils.VisitBitBlocks(validBits, offset, int64(n),
- func(pos int64) { valid[pos] = true }, func() {})
+ func(pos int64) { d.validScratch[pos] = true }, func() {})
- toAppend := make([]int, n)
- for i, v := range d.idxScratchSpace {
- toAppend[i] = int(v)
+ if cap(d.idxAppendScratch) < n {
+ d.idxAppendScratch = make([]int, n, bitutil.NextPowerOf2(n))
+ } else {
+ d.idxAppendScratch = d.idxAppendScratch[:n]
+ }
+ for i, v := range d.idxScratchSpace[:n] {
+ d.idxAppendScratch[i] = int(v)
}
- bldr.(*array.BinaryDictionaryBuilder).AppendIndices(toAppend, valid)
+ bldr.(*array.BinaryDictionaryBuilder).AppendIndices(d.idxAppendScratch,
d.validScratch)
d.nvals -= n - nullCount
return n, nil
}
diff --git a/parquet/internal/encoding/decoder_benchmark_test.go
b/parquet/internal/encoding/decoder_benchmark_test.go
new file mode 100644
index 00000000..6b9e0a14
--- /dev/null
+++ b/parquet/internal/encoding/decoder_benchmark_test.go
@@ -0,0 +1,198 @@
+// 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/array"
+ "github.com/apache/arrow-go/v18/arrow/bitutil"
+ "github.com/apache/arrow-go/v18/arrow/memory"
+ "github.com/apache/arrow-go/v18/parquet"
+ "github.com/apache/arrow-go/v18/parquet/internal/encoding"
+ "github.com/stretchr/testify/require"
+)
+
+func encodeDictIndices(t testing.TB, values []parquet.ByteArray)
encoding.Buffer {
+ t.Helper()
+
+ enc := encoding.NewEncoder(parquet.Types.ByteArray,
parquet.Encodings.PlainDict, true, nil,
memory.DefaultAllocator).(*encoding.DictByteArrayEncoder)
+ defer enc.Release()
+
+ enc.Put(values)
+ buf, err := enc.FlushValues()
+ if err != nil {
+ t.Fatalf("could not encode dictionary indices: %v", err)
+ }
+ return buf
+}
+
+func newBinaryDictionaryBuilder(mem memory.Allocator)
*array.BinaryDictionaryBuilder {
+ dictBuilder := array.NewStringBuilder(mem)
+ dictBuilder.Append("one")
+ dictBuilder.Append("two")
+ dictBuilder.Append("three")
+ dict := dictBuilder.NewArray()
+ dictBuilder.Release()
+
+ dictType := &arrow.DictionaryType{
+ IndexType: arrow.PrimitiveTypes.Int32,
+ ValueType: arrow.BinaryTypes.String,
+ }
+ bldr := array.NewDictionaryBuilderWithDict(mem, dictType,
dict).(*array.BinaryDictionaryBuilder)
+ dict.Release()
+ return bldr
+}
+
+func TestDictByteArrayDecoderDecodeIndices(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+ defer mem.AssertSize(t, 0)
+
+ values := []parquet.ByteArray{
+ []byte("one"), []byte("two"), []byte("one"), []byte("three"),
+ []byte("two"), []byte("one"),
+ }
+ indices := encodeDictIndices(t, values)
+ defer indices.Release()
+
+ decoder := encoding.NewDictDecoder(parquet.Types.ByteArray, nil,
mem).(*encoding.DictByteArrayDecoder)
+ bldr := newBinaryDictionaryBuilder(mem)
+ defer bldr.Release()
+
+ require.NoError(t, decoder.SetData(len(values), indices.Bytes()))
+ n, err := decoder.DecodeIndices(len(values), bldr)
+ require.NoError(t, err)
+ require.Equal(t, len(values), n)
+
+ arr := bldr.NewDictionaryArray()
+ for i, want := range []int{0, 1, 0, 2, 1, 0} {
+ require.False(t, arr.IsNull(i))
+ require.Equal(t, want, arr.GetValueIndex(i))
+ }
+ arr.Release()
+
+ require.NoError(t, decoder.SetData(len(values), indices.Bytes()))
+ n, err = decoder.DecodeIndices(len(values), bldr)
+ require.NoError(t, err)
+ require.Equal(t, len(values), n)
+}
+
+func TestDictByteArrayDecoderDecodeIndicesSpaced(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+ defer mem.AssertSize(t, 0)
+
+ values := []parquet.ByteArray{
+ []byte("one"), []byte("two"), []byte("three"), []byte("one"),
+ }
+ indices := encodeDictIndices(t, values)
+ defer indices.Release()
+
+ decoder := encoding.NewDictDecoder(parquet.Types.ByteArray, nil,
mem).(*encoding.DictByteArrayDecoder)
+ bldr := newBinaryDictionaryBuilder(mem)
+ defer bldr.Release()
+
+ validBits := []byte{0x0f}
+ require.NoError(t, decoder.SetData(len(values), indices.Bytes()))
+ n, err := decoder.DecodeIndicesSpaced(8, 4, validBits, 0, bldr)
+ require.NoError(t, err)
+ require.Equal(t, 8, n)
+
+ arr := bldr.NewDictionaryArray()
+ for i := 0; i < 8; i++ {
+ require.Equal(t, i < 4, !arr.IsNull(i))
+ }
+ arr.Release()
+
+ validBits = []byte{0xf0}
+ require.NoError(t, decoder.SetData(len(values), indices.Bytes()))
+ n, err = decoder.DecodeIndicesSpaced(8, 4, validBits, 0, bldr)
+ require.NoError(t, err)
+ require.Equal(t, 8, n)
+
+ arr = bldr.NewDictionaryArray()
+ defer arr.Release()
+ for i := 0; i < 8; i++ {
+ require.Equal(t, i >= 4, !arr.IsNull(i))
+ }
+}
+
+func BenchmarkDictByteArrayDecoderDecodeIndices(b *testing.B) {
+ for _, nvalues := range []int{1, 64, 4096, 65536} {
+ b.Run(fmt.Sprintf("dense/%d", nvalues), func(b *testing.B) {
+ benchmarkDictByteArrayDecoderDecodeIndices(b, nvalues,
0)
+ })
+ }
+ for _, nvalues := range []int{64, 4096, 65536} {
+ b.Run(fmt.Sprintf("spaced/%d/25pct-null", nvalues), func(b
*testing.B) {
+ benchmarkDictByteArrayDecoderDecodeIndices(b, nvalues,
nvalues/4)
+ })
+ }
+}
+
+func benchmarkDictByteArrayDecoderDecodeIndices(b *testing.B, nvalues,
nullCount int) {
+ values := make([]parquet.ByteArray, nvalues-nullCount)
+ for i := range values {
+ values[i] = []byte("one")
+ }
+ indices := encodeDictIndices(b, values)
+ defer indices.Release()
+
+ decoder := encoding.NewDictDecoder(parquet.Types.ByteArray, nil,
memory.DefaultAllocator).(*encoding.DictByteArrayDecoder)
+ bldr := newBinaryDictionaryBuilder(memory.DefaultAllocator)
+ defer bldr.Release()
+
+ var validBits []byte
+ if nullCount > 0 {
+ validBits = make([]byte, bitutil.BytesForBits(int64(nvalues)))
+ for i := nullCount; i < nvalues; i++ {
+ bitutil.SetBit(validBits, i)
+ }
+ }
+
+ decode := func() {
+ bldr.Resize(0)
+ if err := decoder.SetData(len(values), indices.Bytes()); err !=
nil {
+ b.Fatal(err)
+ }
+ var (
+ n int
+ err error
+ )
+ if nullCount == 0 {
+ n, err = decoder.DecodeIndices(nvalues, bldr)
+ } else {
+ n, err = decoder.DecodeIndicesSpaced(nvalues,
nullCount, validBits, 0, bldr)
+ }
+ if err != nil {
+ b.Fatal(err)
+ }
+ if n != nvalues {
+ b.Fatalf("decoded %d values, want %d", n, nvalues)
+ }
+ }
+
+ decode()
+ b.ReportAllocs()
+ b.SetBytes(int64(nvalues))
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ decode()
+ }
+ b.StopTimer()
+}