Repository navigation
Conversation
|
When running CI pipeline, got few UB. Switched to a less optimal way, which still provided boost in performance. By materializing
|
|
Could somebody potentially trigger the CI run? Thanks in advance |
|
Thanks for picking this up. The shape of the change is exactly what #4626 was asking for: keep the runtime alignment check as load-bearing, and replace the per-element null branch with A few things to look at, the first one is a correctness concern.
I'll approve the CI workflows so the matrix actually exercises the change. Out of curiosity, what was the UB you hit earlier? Worth knowing whether item 1 above was the cause or there's a separate hazard. Disclosure: this review was assisted by an AI tool (Anthropic Claude via Claude Code). I read the diff, the linked issue, and the prior closed PR's review feedback before forming the comments above, and I take responsibility for them. |
|
Thank you for the review @andygrove I have changed my approach and bechmarked it. Posting the benchmarking results, which I ran using
cc @mbutrovich, if you could take a quick look at this, please. Thanks! |
|
Thanks for the rework @sandugood, and for pulling the benchmark numbers and the One thing I want to call out as a nice catch: threading the timezone through and calling A few items before this is ready: 1. The boolean nullable path is UB, and this revision moved it the wrong way. This is item 1 from the earlier review. The new nullable branch does: let slice = unsafe { std::slice::from_raw_parts(self.element_offset as *const bool, num_elements) };
for (idx, &value) in slice.iter().enumerate() { ... }Rust requires every let values = unsafe { std::slice::from_raw_parts(self.element_offset as *const u8, num_elements) };
for &value in values { builder.append_value(value != 0); }Could you mirror that 2. Copy-paste leftover in 3. A couple of targeted tests would help. The bitmap flip and the partial-byte handling are easy to get subtly wrong. Would you be open to adding unit tests for nullable arrays where 4. Formatting. The 5. Minor. The byte reinterpretation of the null word assumes little-endian bit ordering. That matches Arrow and every platform we target, so it is fine, but a one-line comment next to the The boolean path is the only blocker. Everything else is small. Once that is a Disclosure: this review was assisted by an AI tool (Anthropic Claude via Claude Code). I read the diff, the linked issue, and the prior review thread before forming these comments, and I take responsibility for them. |
|
Thank you again for your review @andygrove |
|
Could somebody, please, retrigger the CI run? I think it was due to a .md mismatch that was fixed on the latest main |
|
Should I rebase it or just simply merge the curr |
Replacing the per-element Four things. Does The timezone is now threaded through The unaligned path is now untested There are two implementations per type that must produce identical results, and the new tests construct buffers through Could one test force the misaligned case, for example by allocating with one extra byte of padding and offsetting the pointer? Even one test per type asserting the two paths agree on the same logical data would be worth having, since a divergence between them would be a wrong answer rather than a crash. Trailing bits in the flipped mask
Benchmark numbers should be in the description The description says the results are "copied from the comment below", but the body itself has no table. Comments get buried and do not survive into the commit history. Could you inline the before and after medians for each type and shape, and state explicitly that the |
sunchao
left a comment
There was a problem hiding this comment.
Summary
- Prior state and problem: Nullable primitive arrays performed a null check and append for every element, limiting throughput for large arrays.
- Design approach: Convert Spark’s null bitmap into Arrow validity and append an intermediate
PrimitiveArrayfor aligned inputs. - Correctness / compatibility analysis: Spark’s relevant array-layout methods are identical across the supported versions examined. Focused validation passed for bitmap boundaries, forced misalignment, repeated appends, booleans, and timestamp timezones. The previously reported boolean concern is resolved.
- Key design decisions: Runtime alignment checks and scalar fallbacks remain. Timestamp metadata is preserved to satisfy
append_array’s type check. - Implementation sketch: The primitive macro, date helper, and timestamp helper construct validity and value buffers. Both timestamp dispatch sites forward the timezone.
- Behavioral changes worth calling out: Large arrays improve, but temporary allocations materially regress short-array conversion. One introduced P2 finding follows.
- Suggested improvements: Keep an allocation-free path for short arrays while retaining bulk conversion for larger inputs.
Reviewed the entire diff from 2fd3960dfee464b2da5a98c0e754c049d720002b to 9b4510590fd675710f251466f0fc24a00abb8c46. The PR is not a draft. Existing discussion was read before forming findings. No substantiated existing P1/P2 blockers remain.
Routed skills: review-comet-pr, review-comet-shuffle-pr, review-comet-ffi-pr, and review-comet-memory-pr. This checkout lacks .ai/skills/, so the repository copies in checkout 6161-2bf433338e77 were used.
Exact-head CI: 61 successful checks and 9 skipped, with no failures. Linux shuffle tests cover Spark 3.4–4.2, and macOS shuffle tests cover Spark 4.0. Native CI reports 673 passed and 5 skipped, including all five added tests. Its merge commit has the same tree as the reviewed head. The benchmark check was skipped.
Validation limits: The local full-crate test attempt stopped on uncached anyhow 1.0.103. A disposable release harness using the exact base/head append-method bodies and locked Arrow 58.3.0 passed nine correctness tests and reproduced the performance finding. This harness does not exercise JNI or complete shuffle queries. No local Miri run was performed. Project files remain unchanged.
| NullBuffer::new(BooleanBuffer::new(Buffer::from(flipped), 0, num_elements)); | ||
|
|
||
| let arr = PrimitiveArray::<$arrow_type>::new( | ||
| ScalarBuffer::from(Buffer::from_slice_ref(values)), |
There was a problem hiding this comment.
[P2] Could we retain an allocation-free path for short arrays? Every aligned nullable append now allocates temporary bitmap/value buffers, and append_array copies the values again into the builder. append_list_column_batch invokes this with <true> for every list, including lists without null elements. Using the exact base/head methods with Arrow 58.3.0, a release benchmark including ListBuilder construction, downcasting, and list appends measured singleton array<int> conversion at 19.34 ns/row before versus 212.48 ns/row after, about 11× slower. An allocation counter independently measured 0 versus 4,000 allocations for 1,000 singleton appends into a preallocated builder. This materially increases conversion CPU for JVM shuffle workloads containing many short arrays. Preserve the scalar loop below a measured size cutoff, or reuse storage to avoid these per-list allocations. The same approach applies to the date and timestamp helpers.
Evidence: Disposable harness: /tmp/comet-4672-append-check. The extracted append-method bodies were verified against both supplied SHAs. Run cargo run --release --offline --manifest-path /tmp/comet-4672-append-check/Cargo.toml --bin list_bench and the same command with --bin allocations. The list benchmark uses 8,192 rows per batch, 64 distinct aligned input arrays, initial values-builder capacity 32, and medians of nine alternating base/head measurements. Singleton arrays without nulls measured 19.34→212.48 ns/row, and singleton null arrays measured 20.86→199.24 ns/row. Shorter-path allocation overhead is confirmed by the deterministic 0→4 allocations per singleton append. A separate 10,000-element benchmark confirmed the intended large-array improvement, so the finding concerns the unconditional use of this path for short arrays.
Resolves the conflicts in native/shuffle/src/spark_unsafe/list.rs with apache#6604, which folded the timestamp and date appenders into impl_append_to_builder and added the MIN_BULK_APPEND_ELEMENTS / has_null() fast path for nullable arrays without nulls: - The timestamp and date appenders stay macro-generated, as on main. The macro takes this branch's Arrow type argument, and an optional timezone argument that only append_timestamps_to_builder uses, so append_array still sees the builder's type. - A nullable array with no nulls and at least MIN_BULK_APPEND_ELEMENTS elements keeps main's single append_slice. An array shorter than that stays on the per-element loop, as on main. The validity-bitmap path from this branch runs for aligned arrays of at least MIN_BULK_APPEND_ELEMENTS elements that hold a null. - The two test modules are combined into one.
The round-trip test built every builder with its type's default data type, so the timestamp case never had a timezone. append_array panics when the array's data type, timezone included, differs from the builder's. The test now builds the builder with the column's data type and adds a timestamp column with a timezone, which fails if the bitmap path drops the timezone.
|
@sandugood It looks like this PR is no longer being updated, so I plan on branching from this and creating a new PR to address the final issues. I hope that is ok. Thanks for the contribution! |
|
@sandugood thanks for all the work on this. It's been quiet for a few weeks, so I've picked it up in #6824 to get it over the line. I kept your commits on that branch, so you'll be credited as co-author when it merges. On top of your changes, it merges main, since #6604 reworked the same macro. It also takes the bitmap path only for arrays of 64 or more elements. Building the validity and value buffers costs four allocations per array, which made shorter arrays with nulls slower than the per-element loop (sunchao's review above has the details). Your review on #6824 would be very welcome. |
Which issue does this PR close?
Closes #4626
Rationale for this change
Currently, there is a bottleneck in performance during bulk append in the
SparkUnsafeArrayimplementation (inmacroand respectiveBuildertypes forbool,date32andtimestamp). If the array isNULLABLEthere is a hotspot:Self::is_null_in_bitset()which is suboptimal (see the benchmarking results below)What changes are included in this PR?
With this PR we change the flow of execution for nullable arrays:
SparkvsArrowlogicBooleanBufferfrom the bytes and then creating aNullBufferPrimitiveArray(with type specified in themacro_rules!and inside the according methods in theSparkUnsafeArrayfor several dtypes) from slice databuilderHow are these changes tested?
All of the basic tests pass.
Here is the benchmarking result (copied from the comment below).
Result from
with_nullsversion of benches (no_nullswasnt affected with this PR)