adriangb opened a new pull request, #10880:
URL: https://github.com/apache/arrow-rs/pull/10880

   # Which issue does this PR close?
   
   - Contributes to #9722.
   - Builds on #10878, which adds the `repeated_batches` benchmark cases used 
below.
   
   # Rationale for this change
   
   `WriterProperties` keeps per-column overrides in a
   `HashMap<ColumnPath, ColumnProperties>`. Every accessor that takes a
   `&ColumnPath` — `compression`, `encoding`, `dictionary_enabled`,
   `statistics_enabled`, `write_page_header_statistics`,
   `column_data_page_size_limit`, `column_dictionary_page_size_limit`,
   `column_data_page_v2_compression_ratio_threshold` and
   `bloom_filter_properties` — hashes that path and searches the map. 
`ColumnPath`
   is a `Vec<String>`, so each search hashes every part of the path and then
   compares strings on a hit.
   
   The column writers call those accessors repeatedly for the same leaf column:
   
   - 6 times while a column writer is constructed (`GenericColumnWriter::new` 
plus
     `ColumnValueEncoder::try_new`);
   - 2 more on every `write`, in `ByteBudgetChunker::new`;
   - 1 per mini-batch in `should_add_data_page`, and another in
     `should_dict_fallback` when a dictionary encoder is active;
   - 1 per page in `add_data_page`, plus one more for Data Page v2.
   
   `WriterProperties` is immutable once built, so all of that recomputes an 
answer
   that was already available when the column writer was created.
   
   Counting the searches directly (temporary instrumentation on the map access) 
over
   the `writer_overhead` shapes:
   
   | benchmark case | searches before | searches after |
   | --- | --- | --- |
   | `writer_overhead/1000_cols` | 10,000 | 1,000 |
   | `writer_overhead/5000_cols` | 50,000 | 5,000 |
   | `writer_overhead/10000_cols` | 100,000 | 10,000 |
   | `writer_overhead/1000_cols/repeated_batches` | 103,000 | 1,000 |
   | `writer_overhead/5000_cols/repeated_batches` | 515,000 | 5,000 |
   
   After the change it is exactly one search per leaf column per row group.
   
   # What changes are included in this PR?
   
   - Adds `WriterProperties::resolve_column_properties`, which searches the
     per-column map once and returns a `ResolvedColumnProperties` holding every
     per-column setting already resolved against the file-wide defaults. The 
struct
     is `pub(crate)`; no public API is added.
   - `GenericColumnWriter::new` calls it once and stores the result. The 
per-batch
     and per-page paths, `ByteBudgetChunker::new`, and both `ColumnValueEncoder`
     implementations read from it instead of searching the map again.
     `ColumnValueEncoder::try_new` and `create_bloom_filter` now take the 
resolved
     settings; that trait is not nameable outside the crate.
   - The existing per-column accessors keep their behaviour and are 
reimplemented on
     top of the same resolution helpers, so the "column override, else file 
default,
     else constant" rule has one definition rather than one per accessor.
   
   # Are these changes tested?
   
   Yes.
   
   - The existing suite passes unchanged, with and without `encryption`:
     `cargo test -p parquet --features "arrow async encryption test_common 
experimental"`.
   - New `test_resolve_column_properties_matches_individual_accessors` asserts 
the
     one-pass resolution agrees with every individual accessor, for a column 
with
     overrides, a column that inherits the file defaults, and properties left
     entirely at their defaults.
   - To check the writer is byte-for-byte unchanged I wrote the same file from a
     build before and after this change, with a workload covering the settings 
this
     PR touches: per-column compression, encoding, dictionary on and off, page 
and
     dictionary page size limits, page-header statistics, a bloom filter, 
dictionary
     fallback on multi-KB values, and both writer versions. The two files are
     identical.
   
   Benchmark:
   
       cargo bench -p parquet --bench writer_overhead
   
   I did not have an idle machine, and criterion's wall-clock intervals were too
   wide there to be usable. I measured CPU time (user + sys) per iteration 
instead,
   which is far less sensitive to competing load: the minimum over 40 
alternating
   runs of the two builds, with a separately measured ~13.3 ms of fixed 
benchmark
   setup subtracted.
   
   | benchmark case | before | after | |
   | --- | --- | --- | --- |
   | `writer_overhead/1000_cols` | 2.7 ms | 2.1 ms | see note |
   | `writer_overhead/5000_cols` | 20.2 ms | 18.5 ms | −8 % |
   | `writer_overhead/10000_cols` | 38.8 ms | 34.2 ms | −12 % |
   | `writer_overhead/1000_cols/repeated_batches` | 12.9 ms | 8.6 ms | −34 % |
   | `writer_overhead/5000_cols/repeated_batches` | 88.0 ms | 70.1 ms | −20 % |
   
   Note: at 1,000 columns with a single batch the remaining figure is close 
enough
   to the spread of the setup subtraction that I would not read a percentage 
into
   it. The other four imply about 40 ns per map search, consistently across all 
four
   shapes, which matches the search counts above. A run on an idle machine 
would be
   worth having, and the benchmark from #10878 makes that easy.
   
   `ColumnWriterImpl<Int32Type>` grows from 1312 to 1376 bytes to hold the 
resolved
   settings. Peak RSS on the 10,000 column benchmark was unchanged.
   
   # Are there any user-facing changes?
   
   No. No public API is added, removed or changed, and the bytes written are
   unchanged.
   
   # AI usage
   
   This PR was written with Claude Code and reviewed by a human. The 
byte-for-byte
   output comparison and the map-search counts above are the checks run to 
confirm
   the behaviour is unchanged.
   


-- 
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