derekperkins opened a new pull request, #1272:
URL: https://github.com/apache/arrow-go/pull/1272

   ### Rationale for this change
   
   `DictByteArrayEncoder.PutByteArray` inserts through the untyped 
`MemoTable.GetOrInsert(interface{})`, which boxes the `parquet.ByteArray` on 
every value written. Boxing a slice is a heap allocation 
(`runtime.convTslice`), and the memo table discards it immediately:
   
   ```go
   // parquet/internal/encoding/byte_array_encoder.go
   func (enc *DictByteArrayEncoder) PutByteArray(in parquet.ByteArray) {
        memoIdx, found, err := enc.memo.GetOrInsert(in)   // convTslice per 
value
   ```
   
   `hashing.BinaryMemoTable` already implements the allocation-free typed entry 
point, and `GetOrInsert` is a thin boxing wrapper over it:
   
   ```go
   func (b *BinaryMemoTable) GetOrInsert(val interface{}) (int, bool, error) {
        return b.InsertOrGet(b.valAsByteSlice(val))
   }
   ```
   
   The encoder can't reach it, because `encoding.BinaryMemoTable` doesn't list 
`InsertOrGet` among 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, against `main` at `7efe1c0`:
   
   |  | before | after | change |
   | --- | ---: | ---: | ---: |
   | sec/op | 3.266m ± 1% | 2.495m ± 2% | **−23.61%** (p=0.000) |
   | B/op | 3.551Mi ± 0% | 2.051Mi ± 0% | **−42.26%** (p=0.000) |
   | allocs/op | 131.12k ± 0% | 65.58k ± 0% | **−49.98%** (p=0.000) |
   
   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 via `pqarrow`. In a four-hour production CPU and heap profile of a 
single streaming writer, `DictByteArrayEncoder.PutByteArray` was 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].Put` 
was 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?
   
   - Add `InsertOrGet(val []byte)` to the `encoding.BinaryMemoTable` interface.
   - Call it from `DictByteArrayEncoder.PutByteArray` instead of `GetOrInsert`.
   - Add `InsertOrGet` to `binaryMemoTableImpl` so it still satisfies the 
interface.
   - Add `TestBinaryInsertOrGet`, covering both implementations.
   
   Notes:
   
   - `encoding.BinaryMemoTable` lives under `parquet/internal/`, so widening it 
is not a public API change.
   - The production implementation (`hashing.BinaryMemoTable`, via 
`NewBinaryDictionary`) already satisfied the wider interface with no changes.
   - The only other implementation is `binaryMemoTableImpl`, which the source 
marks deprecated and benchmark-only ("will be removed in a future release"); 
the method added there mirrors its existing `GetOrInsert`.
   - `DictFixedLenByteArrayEncoder` has 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. `TestBinaryInsertOrGet` runs against both `BinaryMemoTable` 
implementations and checks index assignment, the `found` flag on re-insertion, 
`nil` treated as the empty value, agreement with `GetOrInsert`, 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/...` — pass
   - `go test -race ./parquet/internal/encoding/...` — pass
   - `go test ./parquet/pqarrow/... ./parquet/file/...` — the only failures are 
pre-existing and byte-identical to unpatched `main` (the `parquet-testing` 
submodule data is not checked out locally); verified by stashing the patch and 
re-running
   - `gofmt -l` clean, `git diff --check` clean
   
   Benchmark command:
   
   ```
   go test ./parquet/internal/encoding -run '^$' -bench 
'^BenchmarkEncodeDictByteArray$' -benchmem -benchtime=500ms -count=10
   ```
   
   ### 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.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to