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 547733d3 perf(parquet): stop spaced expansion once aligned (#1220)
547733d3 is described below
commit 547733d3252da9f4b027db71e8442b4c3b210358
Author: Minh Vu <[email protected]>
AuthorDate: Fri Aug 28 23:00:22 2026 +0200
perf(parquet): stop spaced expansion once aligned (#1220)
## Summary
- Stop `spacedExpand` as soon as the remaining decoded prefix is already
in its final positions.
- Avoid the final no-op `copy` for aligned valid prefixes.
- Add focused coverage for empty, all-null, leading, middle, trailing,
clustered, alternating, and offset bitmaps.
- Add a 65,536-value benchmark for `int32` and `BYTE_ARRAY`.
## Benchmark
- **Machine:** Apple M1 Pro, arm64
- **Go:** 1.26.3
- **Command:** `go test ./parquet/internal/encoding -run '^$' -bench
'^BenchmarkSpacedExpand$' -benchmem -benchtime=50ms -count=3`
- **Workloads:** trailing, late, clustered, random 10% nulls, and
alternating validity layouts
- **Result:** `0 B/op` and `0 allocs/op` for every case. CPU impact is
small and layout-dependent, with the clearest benefit when the reverse
walk reaches an already-aligned prefix.
## Tests
- `go test ./parquet/internal/encoding`
- `go test ./parquet/internal/... ./parquet/file ./parquet/metadata
./parquet/schema ./parquet/variant ./parquet/compress ./parquet/cmd/...`
---
parquet/internal/encoding/decoder.go | 3 +
.../encoding/decoder_spaced_benchmark_test.go | 225 +++++++++++++++++++++
2 files changed, 228 insertions(+)
diff --git a/parquet/internal/encoding/decoder.go
b/parquet/internal/encoding/decoder.go
index 90ab6cbd..3955bf31 100644
--- a/parquet/internal/encoding/decoder.go
+++ b/parquet/internal/encoding/decoder.go
@@ -243,6 +243,9 @@ func spacedExpand[T parquet.ColumnTypes](buffer []T,
nullCount int, validBits []
// overwrite any existing data with the correctly spaced data.
Any data that happens to be left in the null
// slots is fine since it shouldn't matter and saves us work.
idxDecode -= run.Length
+ if idxDecode == run.Pos {
+ return numValues
+ }
n := copy(buffer[run.Pos:],
buffer[idxDecode:int64(idxDecode)+run.Length])
debug.Assert(n == int(run.Length), "copy copied incorrect
number of elements in spacedExpand")
}
diff --git a/parquet/internal/encoding/decoder_spaced_benchmark_test.go
b/parquet/internal/encoding/decoder_spaced_benchmark_test.go
new file mode 100644
index 00000000..1becad5f
--- /dev/null
+++ b/parquet/internal/encoding/decoder_spaced_benchmark_test.go
@@ -0,0 +1,225 @@
+// 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 (
+ "testing"
+
+ "github.com/apache/arrow-go/v18/arrow/bitutil"
+ "github.com/apache/arrow-go/v18/parquet"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSpacedExpand(t *testing.T) {
+ tests := []struct {
+ name string
+ valid []bool
+ values []int32
+ offset int64
+ }{
+ {name: "empty"},
+ {
+ name: "all valid",
+ valid: []bool{true, true, true, true},
+ values: []int32{10, 11, 12, 13},
+ },
+ {
+ name: "all null",
+ valid: []bool{false, false, false, false},
+ values: nil,
+ },
+ {
+ name: "trailing null",
+ valid: []bool{true, true, true, false},
+ values: []int32{10, 11, 12},
+ },
+ {
+ name: "leading null",
+ valid: []bool{false, true, true, true},
+ values: []int32{10, 11, 12},
+ },
+ {
+ name: "middle null",
+ valid: []bool{true, false, true, true},
+ values: []int32{10, 11, 12},
+ },
+ {
+ name: "clustered nulls",
+ valid: []bool{true, true, false, false, true, true},
+ values: []int32{10, 11, 12, 13},
+ },
+ {
+ name: "alternating",
+ valid: []bool{true, false, true, false, true, false},
+ values: []int32{10, 11, 12},
+ },
+ {
+ name: "offset",
+ valid: []bool{false, true, true, false, true},
+ values: []int32{10, 11, 12},
+ offset: 3,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ require.Equal(t, countValid(tt.valid), len(tt.values))
+
+ validBits := make([]byte,
bitutil.BytesForBits(tt.offset+int64(len(tt.valid))))
+ for i, valid := range tt.valid {
+ if valid {
+ bitutil.SetBit(validBits,
int(tt.offset)+i)
+ }
+ }
+
+ buffer := make([]int32, len(tt.valid))
+ copy(buffer, tt.values)
+ n := spacedExpand(buffer, len(tt.valid)-len(tt.values),
validBits, tt.offset)
+ require.Equal(t, len(tt.valid), n)
+
+ valueIndex := 0
+ for i, valid := range tt.valid {
+ if valid {
+ assert.Equal(t, tt.values[valueIndex],
buffer[i], "value at position %d", i)
+ valueIndex++
+ }
+ }
+ })
+ }
+}
+
+func countValid(values []bool) int {
+ var count int
+ for _, valid := range values {
+ if valid {
+ count++
+ }
+ }
+ return count
+}
+
+type spacedExpandBenchmarkCase struct {
+ name string
+ validBits []byte
+ nullCount int
+}
+
+func newSpacedExpandBenchmarkCase(name string, valid []bool)
spacedExpandBenchmarkCase {
+ validBits := make([]byte, bitutil.BytesForBits(int64(len(valid))))
+ for i, isValid := range valid {
+ if isValid {
+ bitutil.SetBit(validBits, i)
+ }
+ }
+
+ return spacedExpandBenchmarkCase{
+ name: name,
+ validBits: validBits,
+ nullCount: len(valid) - countValid(valid),
+ }
+}
+
+func benchmarkValidityCases(numValues int) []spacedExpandBenchmarkCase {
+ valid := make([]bool, numValues)
+ for i := range valid {
+ valid[i] = i != numValues-1
+ }
+
+ cases := []spacedExpandBenchmarkCase{
+ newSpacedExpandBenchmarkCase("TrailingNull", valid),
+ }
+
+ for i := range valid {
+ valid[i] = i != numValues*9/10
+ }
+ cases = append(cases, newSpacedExpandBenchmarkCase("LateNull", valid))
+
+ clusterLen := numValues / 100
+ clusterStart := numValues - clusterLen - 1
+ for i := range valid {
+ valid[i] = i < clusterStart || i >= clusterStart+clusterLen
+ }
+ cases = append(cases, newSpacedExpandBenchmarkCase("ClusteredNulls",
valid))
+
+ state := uint32(1)
+ for i := range valid {
+ state = state*1664525 + 1013904223
+ valid[i] = state%10 != 0
+ }
+ cases = append(cases, newSpacedExpandBenchmarkCase("Random10PctNulls",
valid))
+
+ for i := range valid {
+ valid[i] = i%2 == 0
+ }
+ cases = append(cases, newSpacedExpandBenchmarkCase("Alternating",
valid))
+
+ return cases
+}
+
+func benchmarkSpacedExpand[T parquet.ColumnTypes](b *testing.B, input []T,
validBits []byte, nullCount int) {
+ b.Helper()
+ const batchSize = 32
+ buffers := make([][]T, batchSize)
+ for i := range buffers {
+ buffers[i] = make([]T, len(input))
+ }
+
+ b.ReportAllocs()
+ b.SetBytes(int64(len(input)))
+ b.ResetTimer()
+
+ for i := 0; i < b.N; {
+ batch := min(batchSize, b.N-i)
+ b.StopTimer()
+ for j := 0; j < batch; j++ {
+ copy(buffers[j], input)
+ }
+ b.StartTimer()
+ for j := 0; j < batch; j++ {
+ if n := spacedExpand(buffers[j], nullCount, validBits,
0); n != len(input) {
+ b.Fatalf("spacedExpand returned %d, want %d",
n, len(input))
+ }
+ }
+ i += batch
+ }
+}
+
+func BenchmarkSpacedExpand(b *testing.B) {
+ const numValues = 1 << 16
+
+ for _, tc := range benchmarkValidityCases(numValues) {
+ b.Run(tc.name, func(b *testing.B) {
+ b.Run("Int32", func(b *testing.B) {
+ input := make([]int32, numValues)
+ for i := range input[:numValues-tc.nullCount] {
+ input[i] = int32(i)
+ }
+ benchmarkSpacedExpand(b, input, tc.validBits,
tc.nullCount)
+ })
+
+ b.Run("ByteArray", func(b *testing.B) {
+ input := make([]parquet.ByteArray, numValues)
+ value := parquet.ByteArray([]byte("value"))
+ for i := range input[:numValues-tc.nullCount] {
+ input[i] = value
+ }
+ benchmarkSpacedExpand(b, input, tc.validBits,
tc.nullCount)
+ })
+ })
+ }
+}