Repository navigation
Conversation
…ing them per batch The row path read each array, map and struct of an unsafe row through Spark's getters, which allocate a view per value. ArrayWriter, StructWriter and MapWriter now keep one UnsafeArrayData, UnsafeRow or UnsafeMapData each and point it at every value, as an earlier revision of apache#6730 did. A view holds the last row it read, and that revision was dropped because nothing ever let go of it. Detaching after every row turned out to cost what the reuse saves, one pointTo per nested writer per row, so ArrowWriter collects the views at construction (UnsafeViews, by type so each detach is a direct call) and points them at an empty layout in finish and reset. Between rows a writer holds at most one row per nested writer, for one batch. Every production caller builds a writer per batch as a local and finishes it, so a batch that throws drops its views with its frame. Throwaway harness, one column of 8192 unsafe rows per shape, all shapes warmed together, best of 30, median of three JVMs, JDK 21, assertions off, ns per row before -> after: array<string> 28.5 -> 26.0, map<string,string> 50.3 -> 45.6, array<struct<int,string>> 55.4 -> 51.5, array<array<int>> 44.0 -> 42.3, map<int,long> 29.5 -> 26.2, struct<int,string> 21.9 -> 19.5, a row of eight mixed fields 119.6 -> 113.4. Flat columns are unchanged. The reachability test now allows one held row per nested writer before finish or reset and none after, for unsafe rows and generic rows holding unsafe values, with and without nulls. The strict-warnings profile gets a filter for the unused parameter of the default collectViews hook, as it has for the other extension hooks that leave a parameter for overrides. Part of apache#6765. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
acruise
force-pushed
the
perf/arrow-writer-reuse-nested-views
branch
from
November 2, 2026 19:04
29df2f3 to
28bec46
Compare
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.
Which issue does this PR close?
Part of #6765. The issue asks for the views to be detached once per row; this does it once per
batch instead, for the reason below, and leaves the issue open for the maintainers to decide
whether that bound is acceptable.
Rationale for this change
ArrowWriter's row path reads each array, map and struct of anUnsafeRowthrough Spark'sgetters, which allocate a view of the value for every value: an
UnsafeArrayData, anUnsafeMapDatawith its two arrays, or anUnsafeRow. An earlier revision of #6730 pointed onereused view per writer at each value instead, and was 2 to 4 ns per row faster on nested types.
It was dropped in review because a reused view keeps the last row it read reachable, along with
the row's memory, until the writer is dropped.
#6765 proposed keeping the reuse and detaching the views once per row. I implemented that first
and measured it: a
pointToper nested writer per row costs 1 to 3 ns, which is what the reusesaves, so against
mainthe net was nil on JDK 21 and within noise on JDK 17. The full table isin #6765 (comment).
This PR detaches once per batch instead, in
finishandreset. The cost is then nothing perrow, and the retention is bounded: between rows a writer holds at most one row per nested writer,
and every production caller (
RowArrowReader,SparkColumnarArrowReader, both paths inCometArrowConverters) builds the writer as a local inside its per-batch method and finishes itthere, so a batch that throws drops its views with its frame. Whether one row per nested writer
for the life of one batch is an acceptable bound is the question for review; it is the same
retention #6730's review rejected, now with the alternative measured.
What changes are included in this PR?
spark/src/main/scala/org/apache/spark/sql/comet/execution/arrow/ArrowWriters.scala:ArrayWriter,StructWriterandMapWritereach keep oneUnsafeArrayData,UnsafeRoworUnsafeMapDataand point it at every unsafe value they write, inwriteUnsafeRowFieldandwriteArrayElements, as the earlier revision of perf: convert Spark's unsafe rows to Arrow straight from their memory #6730 did. The generic path throughsetValueis unchanged.
UnsafeViewsholds the views of all the writers under anArrowWriter, collected once atconstruction through
ArrowFieldWriter.collectViews, by type so that each detach is a directcall.
detachpoints each at an empty layout: along[]{0}for an array,long[]{8, 0, 0}fora map (a null base would not do for either,
pointToreads a header), and(null, 0, 0)for arow.
ArrowWriter.finishandArrowWriter.resetcalldetach.writedoes not. A flat schemacollects no views and is untouched.
pom.xml: thestrict-warningsprofile gets a-Wconffilter for the unused parameter of thedefault
ArrowFieldWriter.collectViewshook, next to the filters for the other extension hooks thatleave a parameter for overrides. Without it the strict job reports that parameter, and then the
@nowarnannotations on the shuffle managers'getReader/getWriteras unused, because thecompile stops before the phase whose warning they suppress.
spark/src/test/scala/org/apache/spark/sql/comet/execution/arrow/CometArrowWriterSuite.scala:the row path keeps no reference to an unsafe row once it is writtenbecomes... once its batch is finished. It writes 20 rows of nested arrays, maps and structs, each as an unsafe row and asa generic row holding the unsafe values (which reach the same views through
setValue), withand without nulls, and checks that at most five rows (one per nested writer) are reachable from
the writer before
finishorresetand none after. The test fails if the detach is removed.How are these changes tested?
All 27 tests of
CometArrowWriterSuitepass, including the existing checks that the unsafe-rowpath matches the generic row path for every type, with collections past 64 elements and strings
of every length.
Throwaway harness, not included: one column of 8192 unsafe rows per shape, every tenth row and
element null, collections of
i % 6elements, all ten shapes warmed together in one JVM, best of30 rounds after 10 warmup rounds, median of three JVMs. Apple M3 Max, assertions off. ns per row,
main-> this PR:Nested shapes are 3 to 11% faster; the flat columns move within the run-to-run spread of about 1
ns. On JDK 17 the reuse without a per-row detach, which is what this PR measures as, was 2 to 4 ns
per row under
mainon the same shapes (table in the issue comment linked above).The
array<map<string,int>>figure is not a result of this PR and should not be read as one. Thatshape is bimodal on
mainas well, 92 to 95 ns in some JVMs and 104 to 111 in others, which is#6757. With
-XX:+PrintInliningon five JVMs of the harness, the two modes differ in exactly oneinlining decision: in the final C2 compile of
MapWriter.writeArrayElements, the loop over anarray's maps, the call to
writeUnsafe(and through itwriteEntries) isinline (hot)in everyslow JVM and
already compiled into a big methodin every fast one. The inline trees below thatpoint are identical. The decision depends on whether
writeUnsafe's own C2 nmethod, which exceedsInlineSmallCode, has been installed by the time the loop's compile reaches the call site; bothare hot at once on concurrent compiler threads, so it is a race, which is why #6757 saw it only
with many shapes warmed together. This PR neither causes nor fixes it; the same two methods race
on
main.🤖 Generated with Claude Code