perf(parquet): use typed memo insertion for byte-array dictionaries - #1272
Open
derekperkins wants to merge 1 commit into
Open
perf(parquet): use typed memo insertion for byte-array dictionaries#1272derekperkins wants to merge 1 commit into
derekperkins wants to merge 1 commit into
Conversation
DictByteArrayEncoder.PutByteArray inserted through the untyped MemoTable.GetOrInsert, boxing the parquet.ByteArray on every value written. Boxing a slice is a heap allocation that the memo table discards immediately. hashing.BinaryMemoTable already implements the allocation-free InsertOrGet, and GetOrInsert is a boxing wrapper over it. Expose InsertOrGet on the encoding.BinaryMemoTable interface so the encoder can call it directly. This applies the treatment from apache#1178 and apache#1251 to the byte-array path that apache#1178 explicitly left unchanged. BenchmarkEncodeDictByteArray, benchstat n=10: -23.61% sec/op, -42.26% B/op, -49.98% allocs/op. The allocation delta is exactly one per value. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
|
I think those are flaky tests, they're not related to this PR. I don't have the rights to restart the failed jobs |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rationale for this change
DictByteArrayEncoder.PutByteArrayinserts through the untypedMemoTable.GetOrInsert(interface{}), which boxes theparquet.ByteArrayon every value written. Boxing a slice is a heap allocation (runtime.convTslice), and the memo table discards it immediately:hashing.BinaryMemoTablealready implements the allocation-free typed entry point, andGetOrInsertis a thin boxing wrapper over it:The encoder can't reach it, because
encoding.BinaryMemoTabledoesn't listInsertOrGetamong its methods.This is the same change already made for the numeric paths in #1178 and #1251. #1178 notes that it "keep[s] the existing byte-array and fixed-length byte-array paths unchanged", so this is the remaining half of that work rather than a new direction.
Benchmark —
BenchmarkEncodeDictByteArray, already in the tree (65,535 values, 100 unique, 8–32 byte strings).benchstat, n=10, Apple M3 Max, Go 1.27, againstmainat7efe1c0:The allocation delta is exactly 65,536 — one per value, plus one.
The benchmark understates the production effect, because its 100 distinct values keep the memo table small. In a low-cardinality column the boxing dominates: every value allocates, and every value is then found to be a duplicate. We hit this writing Iceberg tables through
iceberg-go, which writes viapqarrow. In a four-hour production CPU and heap profile of a single streaming writer,DictByteArrayEncoder.PutByteArraywas the fifth-largest allocation site in the whole process — 25.5M objects, 7.7% of everything allocated, all of it this one site.typedDictEncoder[int64].Putwas number one in the same profile at 15.1% before #1178 landed; together the two accounted for roughly a quarter of the process's allocations, which showed up as ~12% of CPU in GC mark.What changes are included in this PR?
InsertOrGet(val []byte)to theencoding.BinaryMemoTableinterface.DictByteArrayEncoder.PutByteArrayinstead ofGetOrInsert.InsertOrGettobinaryMemoTableImplso it still satisfies the interface.TestBinaryInsertOrGet, covering both implementations.Notes:
encoding.BinaryMemoTablelives underparquet/internal/, so widening it is not a public API change.hashing.BinaryMemoTable, viaNewBinaryDictionary) already satisfied the wider interface with no changes.binaryMemoTableImpl, which the source marks deprecated and benchmark-only ("will be removed in a future release"); the method added there mirrors its existingGetOrInsert.DictFixedLenByteArrayEncoderhas the same pattern. I left it out to keep this focused and because I have no production numbers for it — happy to follow up.Are these changes tested?
Yes.
TestBinaryInsertOrGetruns against bothBinaryMemoTableimplementations and checks index assignment, thefoundflag on re-insertion,niltreated as the empty value, agreement withGetOrInsert, and that stored values do not alias the caller's buffer. I confirmed the test fails when the implementation is deliberately broken.go build ./parquet/...go vet ./parquet/internal/encoding/go test ./parquet/internal/encoding/...— passgo test -race ./parquet/internal/encoding/...— passgo test ./parquet/pqarrow/... ./parquet/file/...— the only failures are pre-existing and byte-identical to unpatchedmain(theparquet-testingsubmodule data is not checked out locally); verified by stashing the patch and re-runninggofmt -lclean,git diff --checkcleanBenchmark command:
Are there any user-facing changes?
No. The modified interface is in an internal package, and encoded output is unchanged — only the insertion path differs.