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 a8af7331 perf(parquet): reuse boolean Bloom filter hashes (#1258)
a8af7331 is described below

commit a8af7331d9fe6addf0d271b7cca005a47edb877c
Author: Minh Vu <[email protected]>
AuthorDate: Mon Aug 31 22:38:51 2026 +0200

    perf(parquet): reuse boolean Bloom filter hashes (#1258)
    
    ## What
    
    - Precompute the xxHash values for `false` (`[]byte{0}`) and `true`
    (`[]byte{1}`).
    - Reuse them in dense and spaced boolean bitmap Bloom-filter paths.
    - Keep custom `Hasher` implementations on the existing path.
    
    This removes a `Sum64` call for every boolean while keeping the same
    hash values.
    
    ## Benchmark
    
    Apple M1 Pro, Go 1.26.3, 7 samples, one CPU:
    
    `go test -vet=off ./parquet/metadata -run '^$' -bench
    '^BenchmarkBloomFilter(HashingFromBitmap|BooleanBitmap)' -benchmem
    -benchtime=150ms -count=7 -cpu=1`
    
    The benchmark matrix covers 100K and 1M booleans, all false, all true,
    alternating, random, and spaced values with 10% and 50% nulls.
    
    Median results for alternating values:
    
    | Case | Before | After |
    | --- | --- | --- |
    | Hash, 100K | 636.0 us, 802,819 B/op, 2 allocs | 181.6 us, 802,816
    B/op, 1 alloc |
    | Insert, 100K | 1.840 ms, 8,193 B/op, 2 allocs | 943.3 us, 8,193 B/op,
    2 allocs |
    | Hash, 1M | 7.206 ms, 8,003,596 B/op, 2 allocs | 1.510 ms, 8,003,584
    B/op, 1 alloc |
    | Insert, 1M | 18.832 ms, 8,193 B/op, 2 allocs | 9.767 ms, 8,193 B/op, 2
    allocs |
    
    For alternating data, hashing is 3.5x to 4.8x faster and insertion is
    about 1.9x faster.
    
    ## Tests
    
    - `go test ./parquet/metadata`
    - `PARQUET_TEST_DATA="$PWD/parquet-testing/data" go test ./...`
    - `go vet ./parquet/metadata`
---
 parquet/metadata/bitmap_benchmark_test.go   | 123 ++++++++++++++++++++++++++++
 parquet/metadata/bloom_filter.go            |  84 ++++++++++++++++++-
 parquet/metadata/bloom_filter_batch_test.go |  43 ++++++++++
 3 files changed, 248 insertions(+), 2 deletions(-)

diff --git a/parquet/metadata/bitmap_benchmark_test.go 
b/parquet/metadata/bitmap_benchmark_test.go
index 773954e7..79339b8d 100644
--- a/parquet/metadata/bitmap_benchmark_test.go
+++ b/parquet/metadata/bitmap_benchmark_test.go
@@ -17,6 +17,7 @@
 package metadata
 
 import (
+       "fmt"
        "testing"
 
        "github.com/apache/arrow-go/v18/arrow/bitutil"
@@ -69,6 +70,128 @@ func BenchmarkBloomFilterHashingFromBitmap(b *testing.B) {
        }
 }
 
+// BenchmarkBloomFilterBooleanBitmap benchmarks dense and spaced boolean bloom 
filter paths
+// with the built-in xxhash implementation.
+func BenchmarkBloomFilterBooleanBitmap(b *testing.B) {
+       for _, numValues := range []int{100_000, 1_000_000} {
+               numValues := numValues
+               for _, tc := range benchmarkBooleanBitmapCases(numValues) {
+                       tc := tc
+                       b.Run(fmt.Sprintf("hash/%d/%s", numValues, tc.name), 
func(b *testing.B) {
+                               bloom := NewBloomFilter(1024, 1024, 
memory.DefaultAllocator)
+                               b.ReportAllocs()
+                               b.ResetTimer()
+
+                               for i := 0; i < b.N; i++ {
+                                       _ = GetHashesFromBitmap(bloom.Hasher(), 
tc.bitmap, 0, int64(numValues))
+                               }
+                       })
+
+                       b.Run(fmt.Sprintf("insert/%d/%s", numValues, tc.name), 
func(b *testing.B) {
+                               bloom := NewBloomFilter(1024, 1024, 
memory.DefaultAllocator)
+                               b.ReportAllocs()
+                               b.ResetTimer()
+
+                               for i := 0; i < b.N; i++ {
+                                       InsertHashesFromBitmap(bloom, 
tc.bitmap, 0, int64(numValues))
+                               }
+                       })
+               }
+
+               for _, tc := range benchmarkSpacedBooleanBitmapCases(numValues) 
{
+                       tc := tc
+                       b.Run(fmt.Sprintf("spaced-hash/%d/%s", numValues, 
tc.name), func(b *testing.B) {
+                               bloom := NewBloomFilter(1024, 1024, 
memory.DefaultAllocator)
+                               b.ReportAllocs()
+                               b.ResetTimer()
+
+                               for i := 0; i < b.N; i++ {
+                                       _ = 
GetSpacedHashesFromBitmap(bloom.Hasher(), tc.numValid, tc.bitmap, 0, 
int64(numValues), tc.validBits, 0)
+                               }
+                       })
+
+                       b.Run(fmt.Sprintf("spaced-insert/%d/%s", numValues, 
tc.name), func(b *testing.B) {
+                               bloom := NewBloomFilter(1024, 1024, 
memory.DefaultAllocator)
+                               b.ReportAllocs()
+                               b.ResetTimer()
+
+                               for i := 0; i < b.N; i++ {
+                                       InsertSpacedHashesFromBitmap(bloom, 
tc.numValid, tc.bitmap, 0, int64(numValues), tc.validBits, 0)
+                               }
+                       })
+               }
+       }
+}
+
+type benchmarkBooleanBitmapCase struct {
+       name      string
+       bitmap    []byte
+       validBits []byte
+       numValid  int64
+}
+
+func benchmarkBooleanBitmapCases(numValues int) []benchmarkBooleanBitmapCase {
+       patterns := []string{"all-false", "all-true", "alternating", "random"}
+       cases := make([]benchmarkBooleanBitmapCase, 0, len(patterns))
+       for _, pattern := range patterns {
+               cases = append(cases, benchmarkBooleanBitmapCase{
+                       name:   pattern,
+                       bitmap: makeBenchmarkBooleanBitmap(numValues, pattern),
+               })
+       }
+       return cases
+}
+
+func benchmarkSpacedBooleanBitmapCases(numValues int) 
[]benchmarkBooleanBitmapCase {
+       bitmap := makeBenchmarkBooleanBitmap(numValues, "alternating")
+       cases := make([]benchmarkBooleanBitmapCase, 0, 2)
+       for _, tc := range []struct {
+               name      string
+               nullEvery int
+       }{
+               {name: "10pct-null", nullEvery: 10},
+               {name: "50pct-null", nullEvery: 2},
+       } {
+               validBits := make([]byte, 
bitutil.BytesForBits(int64(numValues)))
+               var numValid int64
+               for i := 0; i < numValues; i++ {
+                       if i%tc.nullEvery != 0 {
+                               bitutil.SetBit(validBits, i)
+                               numValid++
+                       }
+               }
+               cases = append(cases, benchmarkBooleanBitmapCase{
+                       name:      tc.name,
+                       bitmap:    bitmap,
+                       validBits: validBits,
+                       numValid:  numValid,
+               })
+       }
+       return cases
+}
+
+func makeBenchmarkBooleanBitmap(numValues int, pattern string) []byte {
+       bitmap := make([]byte, bitutil.BytesForBits(int64(numValues)))
+       var state uint64 = 0x9e3779b97f4a7c15
+       for i := 0; i < numValues; i++ {
+               set := false
+               switch pattern {
+               case "all-true":
+                       set = true
+               case "alternating":
+                       set = i%2 == 0
+               case "random":
+                       state ^= state << 7
+                       state ^= state >> 9
+                       set = state&1 != 0
+               }
+               if set {
+                       bitutil.SetBit(bitmap, i)
+               }
+       }
+       return bitmap
+}
+
 // BenchmarkBloomFilterHashBatching compares the materialized and streaming
 // paths used to update a bloom filter from a large fixed-width batch.
 func BenchmarkBloomFilterHashBatching(b *testing.B) {
diff --git a/parquet/metadata/bloom_filter.go b/parquet/metadata/bloom_filter.go
index 842f8016..0dfbde9b 100644
--- a/parquet/metadata/bloom_filter.go
+++ b/parquet/metadata/bloom_filter.go
@@ -54,6 +54,9 @@ var (
                0x47b6137b, 0x44974d91, 0x8824ad5b, 0xa2b7289d,
                0x705495c7, 0x2df1424b, 0x9efc4947, 0x5c6bfb31}
 
+       booleanFalseHash = xxhash.Sum64([]byte{0})
+       booleanTrueHash  = xxhash.Sum64([]byte{1})
+
        defaultHashStrategy = format.BloomFilterHash{XXHASH: &format.XxHash{}}
        defaultAlgorithm    = format.BloomFilterAlgorithm{BLOCK: 
&format.SplitBlockAlgorithm{}}
        defaultCompression  = format.BloomFilterCompression{UNCOMPRESSED: 
&format.Uncompressed{}}
@@ -115,6 +118,13 @@ func (xxhasher) Sum64sInto(b [][]byte, vals []uint64) {
        }
 }
 
+func precomputedBooleanHashes(h Hasher) (falseHash, trueHash uint64, ok bool) {
+       if _, ok := h.(xxhasher); !ok {
+               return 0, 0, false
+       }
+       return booleanFalseHash, booleanTrueHash, true
+}
+
 type sum64sIntoHasher interface {
        Sum64sInto([][]byte, []uint64)
 }
@@ -240,9 +250,20 @@ func GetHashesFromBitmap(h Hasher, bitmap []byte, 
bitmapOffset int64, numValues
                return []uint64{}
        }
 
+       out := make([]uint64, numValues)
+       if falseHash, trueHash, ok := precomputedBooleanHashes(h); ok {
+               for i := range numValues {
+                       if bitutil.BitIsSet(bitmap, int(bitmapOffset+i)) {
+                               out[i] = trueHash
+                       } else {
+                               out[i] = falseHash
+                       }
+               }
+               return out
+       }
+
        // Convert each bool bit to []byte for hashing
        // Reuse a single-byte slice to avoid allocating per value
-       out := make([]uint64, numValues)
        b := []byte{0}
        for i := range numValues {
                val := bitutil.BitIsSet(bitmap, int(bitmapOffset+i))
@@ -266,11 +287,30 @@ func GetSpacedHashesFromBitmap(h Hasher, numValid int64, 
bitmap []byte, bitmapOf
        }
 
        out := make([]uint64, 0, numValid)
+       falseHash, trueHash, usePrecomputedHashes := precomputedBooleanHashes(h)
 
        // Use SetBitRunReader to efficiently iterate over valid values
+       setReader := bitutils.NewSetBitRunReader(validBits, validBitsOffset, 
numValues)
+       if usePrecomputedHashes {
+               for {
+                       run := setReader.NextRun()
+                       if run.Length == 0 {
+                               break
+                       }
+
+                       for i := range run.Length {
+                               if bitutil.BitIsSet(bitmap, 
int(bitmapOffset+run.Pos+i)) {
+                                       out = append(out, trueHash)
+                               } else {
+                                       out = append(out, falseHash)
+                               }
+                       }
+               }
+               return out
+       }
+
        // Reuse a single-byte slice to avoid allocating per value
        b := []byte{0}
-       setReader := bitutils.NewSetBitRunReader(validBits, validBitsOffset, 
numValues)
        for {
                run := setReader.NextRun()
                if run.Length == 0 {
@@ -298,11 +338,27 @@ func InsertHashesFromBitmap(b BloomFilterBuilder, bitmap 
[]byte, bitmapOffset in
        }
 
        h := b.Hasher()
+       falseHash, trueHash, usePrecomputedHashes := precomputedBooleanHashes(h)
        var (
                hashBatch [bloomFilterHashBatchSize]uint64
                value     [1]byte
        )
 
+       if usePrecomputedHashes {
+               for offset := int64(0); offset < numValues; offset += 
bloomFilterHashBatchSize {
+                       end := min(offset+int64(bloomFilterHashBatchSize), 
numValues)
+                       for i := offset; i < end; i++ {
+                               if bitutil.BitIsSet(bitmap, 
int(bitmapOffset+i)) {
+                                       hashBatch[i-offset] = trueHash
+                               } else {
+                                       hashBatch[i-offset] = falseHash
+                               }
+                       }
+                       b.InsertBulk(hashBatch[:end-offset])
+               }
+               return
+       }
+
        for offset := int64(0); offset < numValues; offset += 
bloomFilterHashBatchSize {
                end := min(offset+int64(bloomFilterHashBatchSize), numValues)
                for i := offset; i < end; i++ {
@@ -324,12 +380,36 @@ func InsertSpacedHashesFromBitmap(b BloomFilterBuilder, 
numValid int64, bitmap [
        }
 
        h := b.Hasher()
+       falseHash, trueHash, usePrecomputedHashes := precomputedBooleanHashes(h)
        var (
                hashBatch [bloomFilterHashBatchSize]uint64
                value     [1]byte
        )
 
        setReader := bitutils.NewSetBitRunReader(validBits, validBitsOffset, 
numValues)
+       if usePrecomputedHashes {
+               for {
+                       run := setReader.NextRun()
+                       if run.Length == 0 {
+                               break
+                       }
+
+                       runEnd := run.Pos + run.Length
+                       for pos := run.Pos; pos < runEnd; pos += 
bloomFilterHashBatchSize {
+                               end := min(pos+int64(bloomFilterHashBatchSize), 
runEnd)
+                               for i := pos; i < end; i++ {
+                                       if bitutil.BitIsSet(bitmap, 
int(bitmapOffset+i)) {
+                                               hashBatch[i-pos] = trueHash
+                                       } else {
+                                               hashBatch[i-pos] = falseHash
+                                       }
+                               }
+                               b.InsertBulk(hashBatch[:end-pos])
+                       }
+               }
+               return
+       }
+
        for {
                run := setReader.NextRun()
                if run.Length == 0 {
diff --git a/parquet/metadata/bloom_filter_batch_test.go 
b/parquet/metadata/bloom_filter_batch_test.go
index 932470ca..eee3078d 100644
--- a/parquet/metadata/bloom_filter_batch_test.go
+++ b/parquet/metadata/bloom_filter_batch_test.go
@@ -177,6 +177,49 @@ func TestInsertHashesFromBitmapBatchesValues(t *testing.T) 
{
        assert.Empty(t, empty.batches)
 }
 
+func TestBitmapBloomHashingPreservesCustomHasher(t *testing.T) {
+       const (
+               numValues    = bloomFilterHashBatchSize + 7
+               bitmapOffset = int64(3)
+               validOffset  = int64(5)
+       )
+
+       bitmap := make([]byte, bitutil.BytesForBits(bitmapOffset+numValues))
+       validBits := make([]byte, bitutil.BytesForBits(validOffset+numValues))
+       var numValid int64
+       for i := 0; i < numValues; i++ {
+               if i%3 == 0 {
+                       bitutil.SetBit(bitmap, int(bitmapOffset)+i)
+               }
+               if i%4 != 0 {
+                       bitutil.SetBit(validBits, int(validOffset)+i)
+                       numValid++
+               }
+       }
+
+       expectedDense := GetHashesFromBitmap(xxhasher{}, bitmap, bitmapOffset, 
numValues)
+       hasher := &recordingHasher{}
+       assert.Equal(t, expectedDense, GetHashesFromBitmap(hasher, bitmap, 
bitmapOffset, numValues))
+       assert.Len(t, hasher.inputs, numValues)
+
+       expectedSpaced := GetSpacedHashesFromBitmap(xxhasher{}, numValid, 
bitmap, bitmapOffset, numValues, validBits, validOffset)
+       hasher = &recordingHasher{}
+       assert.Equal(t, expectedSpaced, GetSpacedHashesFromBitmap(hasher, 
numValid, bitmap, bitmapOffset, numValues, validBits, validOffset))
+       assert.Len(t, hasher.inputs, int(numValid))
+
+       hasher = &recordingHasher{}
+       bloom := newBatchRecordingBloomFilter(hasher)
+       InsertHashesFromBitmap(bloom, bitmap, bitmapOffset, numValues)
+       assert.Equal(t, expectedDense, flattenHashBatches(bloom.batches))
+       assert.Len(t, hasher.inputs, numValues)
+
+       hasher = &recordingHasher{}
+       bloom = newBatchRecordingBloomFilter(hasher)
+       InsertSpacedHashesFromBitmap(bloom, numValid, bitmap, bitmapOffset, 
numValues, validBits, validOffset)
+       assert.Equal(t, expectedSpaced, flattenHashBatches(bloom.batches))
+       assert.Len(t, hasher.inputs, int(numValid))
+}
+
 func TestInsertSpacedHashesFromBitmapBatchesValidValues(t *testing.T) {
        const (
                numValues    = 2*bloomFilterHashBatchSize + 19

Reply via email to