You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
perf: reuse the nested unsafe views in ArrowWriter's row path, detaching them once per row #6765
ArrowWriter's row path reads each array, map and struct of an UnsafeRow through Spark's getters, which allocate a view of the value for each value: an UnsafeArrayData, an UnsafeMapData with its two arrays, or an UnsafeRow. An earlier revision of #6730 pointed one reused view per writer at every value instead. In a harness that warms every type up together, that was 1 to 4 ns per row faster on nested types:
ns per row
main
reused views
a view per value
array
59.1
26.8
30.1
map<string,string>
116.9
49.7
53.5
array<struct<int,string>>
86.8
57.8
61.1
array<array>
80.1
45.6
47.8
map<int,long>
49.5
30.2
31.9
struct<int,string>
32.0
22.2
23.0
#6730 dropped the reuse because a reused view keeps the last row it read reachable after the row's values are copied, along with the row's memory, until the writer is dropped. That is at most one row per nested writer for the life of a batch's writer, but with large rows it is heap that Spark does not account for.
Describe the potential solution
Reuse the views and detach them once per row, rather than after every value. Detaching after every value, by pointing each view at an empty layout, costs as much as it saves. It was 13% slower than a view per value on array<array>, because an array of nested values detaches once per element. Instead, ArrowWriter could keep the nested writers that hold a view and detach just those after each write, so flat rows pay nothing. An UnsafeArrayData can point at long[] {0} with a size of 8, an UnsafeMapData at long[] {8, 0, 0} with a size of 24, and an UnsafeRow at (null, 0, 0). A null base would crash the first two, because their pointTo reads a header.
CometArrowWriterSuite's the row path keeps no reference to an unsafe row once it is written checks that no row stays reachable from the writers, and it has to keep passing.
Additional context
Measured on an M3 Max with JDK 17: one column of 8192 rows per case, every tenth row and element null, the median of three alternating JVMs. Part of #6565. See the review of #6730.
I implemented this as described: ArrayWriter, StructWriter and MapWriter each keep one UnsafeArrayData / UnsafeRow / UnsafeMapData and point it at each value, and ArrowWriter
collects those views at construction (by type, so each is detached through a direct call, not a
virtual one) and points them at an empty layout after every write and in finish. A flat schema
collects none. The reachability test passes with the detach and fails without it, for unsafe rows
and for generic rows holding unsafe values, with and without nulls.
The reuse saves what this issue measured, but the per-row detach costs about the same, so against main the net is nil, and array<map<...>> loses to the inlining quirk in #6757.
Throwaway harness: one column of 8192 unsafe rows per shape, every tenth row and element null,
collections of i % 6 elements, all ten shapes warmed together in one JVM, best of 30 rounds after
10 warmup rounds, median of three JVMs. Apple M3 Max, assertions off (-ea removed from the test
argLine). ns per row:
Detaching once per row gives all of that back. One pointTo per nested writer per row is 1 to 3
ns, the same order as the saving, and with four nested writers in the wide row it is a wash. On
JDK 21 every nested shape is at or slightly above main; on JDK 17 three are 2 to 3 ns under,
two are 1 to 2 ns over, the rest are equal.
The first point is the only version that pays, and it keeps up to one row reachable per nested
writer until the batch's finish, which is what the #6730 review rejected. Since a per-row detach
costs what the reuse saves, the next thing to try is detaching once per batch instead: at finish
and reset, with the same views. That bounds the retention to one row per nested writer for the
lifetime of one batch, at no per-row cost, and gives the "reuse, no detach" column above. I'll
measure that and the array<map> case before opening anything.
Activity
acruise commentedon Nov 2, 2026
I implemented this as described:
ArrayWriter,StructWriterandMapWritereach keep oneUnsafeArrayData/UnsafeRow/UnsafeMapDataand point it at each value, andArrowWritercollects those views at construction (by type, so each is detached through a direct call, not a
virtual one) and points them at an empty layout after every
writeand infinish. A flat schemacollects none. The reachability test passes with the detach and fails without it, for unsafe rows
and for generic rows holding unsafe values, with and without nulls.
The reuse saves what this issue measured, but the per-row detach costs about the same, so against
mainthe net is nil, andarray<map<...>>loses to the inlining quirk in #6757.Throwaway harness: 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 of 30 rounds after10 warmup rounds, median of three JVMs. Apple M3 Max, assertions off (
-earemoved from the testargLine). ns per row:
(21) is JDK 21.0.12, (17) is JDK 17.0.19. Run-to-run spread is about 2 ns on the nested shapes.
Reading it:
than
mainon the nested shapes and 5 to 8 on the wide row, on both JDKs. That matches the tablein this issue.
pointToper nested writer per row is 1 to 3ns, the same order as the saving, and with four nested writers in the wide row it is a wash. On
JDK 21 every nested shape is at or slightly above
main; on JDK 17 three are 2 to 3 ns under,two are 1 to 2 ns over, the rest are equal.
array<map<string,int>>is 15 to 20% slower with the reuse on both JDKs, with or without thedetach. That is the C2 inlining quirk in perf: converting unsafe arrays of maps to Arrow is up to 1.7x slower when C2 inlines MapWriter.writeEntries #6757, which the view-per-value code on
mainhappensto avoid.
The first point is the only version that pays, and it keeps up to one row reachable per nested
writer until the batch's
finish, which is what the #6730 review rejected. Since a per-row detachcosts what the reuse saves, the next thing to try is detaching once per batch instead: at
finishand
reset, with the same views. That bounds the retention to one row per nested writer for thelifetime of one batch, at no per-row cost, and gives the "reuse, no detach" column above. I'll
measure that and the
array<map>case before opening anything.