[SPARK-59653][SQL] Lazily materialize UnsafeMapData key/value arrays for escape analysis - #58921
Open
david-mollitor-db wants to merge 1 commit into
Open
david-mollitor-db wants to merge 1 commit into
david-mollitor-db wants to merge 1 commit into
Conversation
…for escape analysis UnsafeMapData's constructor eagerly allocated two nested UnsafeArrayData (keys, values). That nested object graph defeats JIT escape analysis, so a transient count-only map access -- size(map), cardinality, and the size(x) > 0 filter InferFiltersFromGenerate inserts below every non-outer explode/inline -- allocates a wrapper plus two sub-wrappers per row just to read an element count that is already stored inline in the layout. UnsafeArrayData is a flat object that the JIT already scalar-replaces (so size(array_col) allocates nothing); only maps carry this per-row garbage. Build the key/value array views lazily in keyArray()/valueArray() and read numElements() directly from the key-array header, making a freshly pointed-to UnsafeMapData a flat, scalar-replaceable object. There is no storage-format change and no call-site changes; serialization, copy(), and MapData.foreach are unaffected. On a representative size(map_col) workload (whole-stage codegen on, assertions disabled), per-row getMap wrapper allocation dropped from ~4022 to ~94 JFR allocation samples (~98%), results unchanged. Generated-by: Isaac Co-authored-by: Isaac <[email protected]>
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.
What changes were proposed in this pull request?
UnsafeRow.getMap/UnsafeArrayData.getMapcreate a freshUnsafeMapDataper access, and itsconstructor eagerly allocates two nested
UnsafeArrayDataobjects (keysandvalues). Thischanges
UnsafeMapDatato build those key/value array views lazily:keyArray()/valueArray()construct (and cache) theUnsafeArrayDataon first use instead ofin the constructor.
numElements()reads the key-array element count directly from the layout(
Platform.getLong(baseObject, baseOffset + 8)), which equalskeyArray().numElements()withoutmaterializing any view.
pointTostores the key-array byte size as a primitive field and no longer eagerly points thekey/value wrappers; the eager
keys.numElements() == values.numElements()debug assertion isdropped.
There is no change to the storage format and no call-site changes:
keyArray()/valueArray()keep their signatures, and serialization (
Externalizable/ Kryo),copy(), andMapData.foreachare unaffected (they use the accessors or operate on the raw bytes).Why are the changes needed?
For a transient, count-only map access --
size(map),cardinality, and thesize(x) > 0filterthat
InferFiltersFromGenerateinserts below every non-outerexplode/inline-- the nestedUnsafeMapData->keys/valuesobject graph defeats JIT escape analysis, so a wrapper plus twosub-wrappers are allocated per row purely to read an element count that is already stored inline in
the layout.
UnsafeArrayDatais a flat object, which the JIT already scalar-replaces, sosize(array_col)allocates nothing; only maps carry this per-row garbage.Making
UnsafeMapDataflat lets escape analysis scalar-replace the transient wrapper the same way.On a representative
size(map_col)workload (whole-stage codegen on, assertions disabled to matchproduction), the per-row
getMapwrapper allocation dropped from ~4022 to ~94 JFR allocationsamples (~98% eliminated), and
UnsafeMapDataleaves the top of the allocation-by-class profile.This benefits every short-lived count-only map access, not just
size.Does this PR introduce any user-facing change?
No. This is an allocation / GC-pressure reduction on a hot map-access path; results, ordering, and
nullability are unchanged. An audit of all
UnsafeMapDatacreation sites found none that reuse asingle instance across rows via
pointTowhile iterating keys/values, so consumers that iteratekeys/values are neutral (they allocate the same wrappers, only on demand).
How was this patch tested?
UnsafeMapSuite(extended with cases for count-onlynumElements, cached key/value views,copy,and an empty map),
UnsafeRowConverterSuite(nested maps), andCollectionExpressionsSuitepass, asdo
WholeStageCodegenSuiteandGeneratorFunctionSuite(explode exercises the map path). Theallocation reduction was verified with JFR (
jdk.ObjectAllocationSample) on asize(map_col)queryunder whole-stage codegen:
UnsafeRow.getMapwrapper allocation fell ~4022 -> ~94 samples with thechange, with identical results.
Was this patch authored or co-authored using generative AI tooling?
Generated-by: Isaac
This pull request and its description were written by Isaac.