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

   # Which issue does this PR close?
   
   - Closes #10881.
   
   # Rationale for this change
   
   `VariantMetadata::get_entry` resolves a field name to a field id by 
searching the
   dictionary, decoding and comparing dictionary strings as it goes (linear for 
an
   unsorted dictionary, logarithmic for a large sorted one).
   
   Several paths copy fields out of a variant object and back into a builder 
that
   shares that object's metadata dictionary. `shred_variant` writes every field 
the
   shredding schema does not cover into the leftover `value` column, and 
projection
   paths do the same. In all of them the field name handed to the builder came 
from
   `VariantObject::iter`, which produced it by looking up a field id in that 
very
   dictionary. Searching for it by name is a round trip: the id was already 
known,
   and the search spends string comparisons recovering it.
   
   `ReadOnlyMetadataBuilder` has a `known_field_names` cache intended to absorb 
this
   cost, but it cannot help here. `VariantValueArrayBuilder::builder_ext` 
constructs
   a fresh `ReadOnlyMetadataBuilder` per value, so in a per-row builder the 
cache is
   populated and dropped again on every row, never serving a lookup, and each 
row
   pays to hash names it will never see again.
   
   In a CPU profile of the `shred_variant_unmatched_object_8k_rows` benchmark 
added
   here, `ReadOnlyMetadataBuilder::try_upsert_field_name` accounted for about 
57% of
   `shred_variant`. After this change it accounts for about 20%, and 
`get_entry` no
   longer appears in the hot path.
   
   # What changes are included in this PR?
   
   - `VariantMetadata::borrowed_field_id` (crate-private). A field name that is 
a
     slice of the dictionary's own value region already encodes its field id: it
     belongs to the entry whose offset equals the name's distance from the 
start of
     that region. This finds the entry with a binary search over the offset 
array,
     comparing integers instead of decoding dictionary strings, and confirms 
the hit
     by comparing lengths rather than bytes.
   
     A candidate is accepted only when it starts at the name's address and has 
the
     name's length, which makes the entry's bytes and the name's bytes the same
     bytes. Anything else, including a name that borrows from elsewhere, a 
slice of
     an entry, or metadata with arbitrary offsets, falls back to the existing
     search. Addresses are only ever compared as integers, never dereferenced.
   
     Note that the offset array is monotonic regardless of whether the 
dictionary is
     sorted, so this binary search is available for unsorted dictionaries too, 
where
     `get_entry` itself can only search linearly.
   
   - `VariantMetadata::get_entry` tries the above first, so all callers benefit.
   
     Cost for callers this cannot help: `get_entry` is public, and a name that 
does
     not borrow from the dictionary now runs the address range check before the
     existing search. That check short circuits on a failed comparison, so such 
a
     caller pays a few integer operations and nothing else. The one case that 
pays
     more is a name pointing into the value region without starting an entry, 
for
     example a substring of one: that costs a binary search over the offset 
array
     before falling back. Both are bounded, but I would rather state them than 
have
     them found in review.
   
   - `ReadOnlyMetadataBuilder::try_upsert_field_name` tries it before consulting
     `known_field_names`, so the paths described above do no hashing at all. The
     cache still serves field names that do not borrow from the dictionary.
   
   - `shred_variant` reuses one scratch buffer to track which shredded fields a 
row
     supplied, instead of allocating a `HashSet` per row.
   
   - Two new benchmarks in `parquet-variant-compute/benches/variant_kernels.rs`
     covering objects that the shredding schema matches partially and not at 
all.
   
   ## One behavior note
   
   The spec requires dictionary keys to be unique, and validation enforces that 
for
   sorted dictionaries. It does not enforce it for unsorted ones, so a 
dictionary
   that validates can still contain the same key twice. For such a dictionary,
   `get_entry` previously returned the first matching id and now returns the id 
the
   borrowed name actually came from. Both ids name that same string, so a 
returned
   field id still always names the string the caller asked for, but the 
specific id
   can differ from before in that spec-violating case. I am happy to reject
   duplicates during validation of unsorted dictionaries instead, or to fold 
that
   into a follow-up, if maintainers prefer.
   
   # Are these changes tested?
   
   Yes. New unit tests cover sorted and unsorted dictionaries, agreement 
between the
   borrowed lookup, `get_entry`, and lookups by an owned (non-borrowed) copy of 
the
   same name, names borrowed from a different dictionary that must not be 
resolved
   against this one, a slice of an entry that shares its start offset without 
being
   equal to it, and empty field names. The existing `parquet-variant`,
   `parquet-variant-compute`, `parquet-variant-json`, proptest fuzz, and
   `variant_interop` suites pass unchanged.
   
   The two new benchmarks cover 8192 rows over a 300-entry dictionary with 
15-field
   objects. I am deliberately not posting timings yet. The machine available to 
me is
   heavily contended, and a paired interleaved probe there produced a 65% spread
   within a single invocation on identical work, so any speedup figure from it 
would
   be indistinguishable from noise. I will follow up with numbers from a quiet
   machine, measured with interleaved arms and with unaffected control 
benchmarks
   used to certify that the run is valid.
   
   # Are there any user-facing changes?
   
   No public API changes. `get_entry` keeps its signature and its contract that 
a
   returned field id names the requested string; see the behavior note above 
for the
   one case where the specific id it picks can differ.
   


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