Repository navigation
Conversation
…SMALL Read the needed order statistics with select_nth_unstable_by(total_cmp) instead of a full sort. Results are bit-identical; a test compares them against the previous full-sort code on randomised data. Fixes storytold#2679 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Select statistical ranks and interpolate percentiles crates/functions/src/stat/descriptive.rs |
median, kth, and percentile interpolation use in-place selection or direct access for monotone inputs. Existing validation and result rules remain. |
Check selection against full-sort references crates/functions/src/stat/tests.rs |
Tests compare results bit-for-bit and errors exactly across varied input lengths, special floating-point values, ties, and boundary parameters. |
Priority:
Estimated code review effort: 3 (Moderate) | ~20 minutes
Change: Refactor
Suggested reviewers: aiongg
Merge Risk: 
Minimal · up to 8b330
-
The statistical calculations use selection instead of full sorting. No merge-blocking issue was identified from the available evidence.
Architecture risk: 
Medium · up to 89087
-
The change affects 1 system.
Changed systems:
cratesArchitecture concerns
No architecture-level concerns identified.Review details
-
Systems and components
- observed — crates (service) was modified; 1 changed file maps to changed impact.
Before / after behavior
- observed — Modified behavior in crates/functions/src/stat.rs:
mediannow selects the middle order statistic in place instead of sorting the vector; for even-length inputs it takes the maximum of the lower partition and returns its midpoint with the selected upper value. Empty-input handling is unchanged. - observed — Modified behavior in crates/functions/src/stat.rs:
kthnow selects the requested order statistic in place rather than indexing a sorted copy. It retains the existing ceiling ofk, invalid-rank#NUM!result, and largest-versus-smallest index mapping. - observed — Modified behavior in crates/functions/src/stat.rs:
interpnow selects the lower ranked value and finds the next value as the minimum of the upper partition when interpolation is needed, replacing indexing into sorted data. The inclusive percentile path passes its vector directly to selection after the existing empty-input andkchecks. - observed — Modified behavior in crates/functions/src/stat.rs: The exclusive percentile path now passes its vector directly to
interp, replacing the full sort; its existingkand position validation remains unchanged.
Reliability and maintainability
- inferred — Risk-relevant change factors for crates: blast_radius_1; direct_dependents_1
-
Pre-merge checks: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passed-
Full details: Linked Issues check
-
Explanation
Issue
#2679requires selection for MEDIAN, PERCENTILE.INC, PERCENTILE.EXC, QUARTILE.INC, QUARTILE.EXC, LARGE, and SMALL. The reviewed changes useselect_nth_unstable_bywithf64::total_cmpfor unordered inputs and handle monotone inputs directly. The implementation uses the lower partition maximum for even-count MEDIAN and the upper partition minimum for percentile interpolation. QUARTILE functions use the percentile path. The added oracle test compares results with the former full-sort behavior across ties, signed zero, NaN, infinities, invalid parameters, and sorted data.
Full details: Out of Scope Changes check
-
Explanation
The changes stay within issue
#2679. They modify order-statistic implementations and add focused oracle tests. Thepub(super)visibility change supports those tests. PERCENTRANK and other full-order callers remain unchanged.
-
Full details: Docstring Coverage
-
Explanation
Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 13 functions across 2 files.
Resolution
Write docstrings for the functions missing them to satisfy the coverage threshold.
-
Finishing Touches
-
🧪 Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Comment @coderabbitai help to get the list of available commands.
|
This PR is one of five speed-ups from an audit of the
Results are identical in every case. #2686 compares against the old full-sort code bit for bit (lengths 0 to 4096, ties, -0.0, NaN, inf). #2691 compares against a plain fold, bit for bit, over full recalculations and 30 edit rounds and against the unmodified build on random workbooks with errors, text, spills and RANDBETWEEN. #2694 and #2696 compare byte-identical dumps of all values, Open PRs that touch the same code and will need a small merge: #2319 and #2252 ( 🤖 Generated with Claude Code |
FredApps
left a comment
There was a problem hiding this comment.
Reviewed the selection changes and surrounding callers at this head. I found no new concrete correctness defect: total_cmp defines the same ordering for selection and sorting, the even median uses the lower partition maximum, and interpolation uses the upper partition minimum only for a nonzero fractional rank. Existing input guards prevent invalid selection indices; QUARTILE delegates to the changed percentile helpers.
Local validation: a dependency-free Rust harness extracted the production helper bodies and the committed old-sort oracle test unchanged (only the error type/import scaffolding was supplied). selection_matches_full_sort_bit_for_bit passed: 1 passed, 0 failed. This tests the algorithms, not the complete function-dispatch path.
CI evidence: inspected the successful gate job log, including this oracle test; 2,525 tests passed, one timing-sensitive calc scaling test skipped, and all ten gate steps passed (including fmt, Clippy and wasm). These are CI results, not a local Cargo pass.
The reported instruction counts remain author measurements. They demonstrate the large synthetic order-statistic hot path; sorted/reverse-sorted inputs, small inputs, and whole-formula range collection can change the end-to-end speedup. I did not reproduce those benchmark numbers.
Local Cargo validation was blocked before compilation: the pinned egui source was unavailable offline, and the isolated crate build could not download missing crates because the TLS client credentials were unavailable. No security settings were changed. This is a COMMENT because local integration validation and independent performance reproduction remain incomplete.
stat.rs was split into stat/; re-applied the selection changes in stat/descriptive.rs and moved the oracle test to stat/tests.rs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Merged current main into this branch. stat.rs was split into stat/ on main, so the merge conflicted. I re-applied the selection changes (median, kth, interp, percentile/quartile) by hand in stat/descriptive.rs and moved the old-vs-new oracle test to stat/tests.rs. The diff against main is again only those two files. fmt, clippy -D warnings and the gridcraft-functions stat tests pass, including the bit-for-bit selection_matches_full_sort test. |
|
Compared this branch with upstream
The selection versus full-sort bitwise oracle |
A full sort recognises ascending and descending runs in O(n), which a selection cannot beat, so sorted, reverse-sorted and tie-block input was up to 1.8x slower than before. Check for a monotone run first (one pass that stops at the first out-of-order pair) and index into it. Extend the bit-for-bit oracle test with ascending, descending and nearly ascending data. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Thank you for the detailed measurements, they were very useful. I reproduced your findings on Linux (one pinned core, release build, instruction counts as the main metric). The cause was sorted input: the standard sort recognises an ascending or descending run in one pass, while the selection still does several passes. So on sorted, reverse-sorted and tie-block input (values repeated in sorted blocks) the branch needed 1.2 to 1.8 times the instructions of main. This also covers the formula-level slowdown with array-lifted LARGE (k = 1, 2, 3, 10), since each k repeats that work. Your small-input and 1,000,000 results were confirmed as faster. I could not reproduce a slowdown for randomly ordered duplicate-heavy data, which is 0.5 to 0.7 times main here; the tie-block shape (sorted runs of equal values) did regress and is fixed now. Change (head 8b330ac): before selecting, one cheap pass checks whether the data is already ascending or descending, and if so the value is read straight from it. The result is still bit-identical to a full sort, and the oracle test Instructions per call, branch relative to main (1.00 = same):
One caveat: a "pipe organ" shape (values rising then falling) is still about 1.5 times main for MEDIAN in instruction count, although wall time at 10,000 values was equal or lower. I left that alone because it is rare and fixing it would need a more complex algorithm. The PR description has the updated table. |
FredApps
left a comment
There was a problem hiding this comment.
Re-review at 8b330ac (previously commented at 890870c).
Since then: two merges of main and two commits (4523b81, 8b330ac) that read already-sorted input directly instead of selecting. I read the whole current diff of stat/descriptive.rs and stat/tests.rs.
Checked:
sorted_if_monotonechecks ascending, then descending, intotal_cmporder, each stopping at the first out-of-order pair, and reverses a descending slice. Elements that compare equal undertotal_cmpare bit-identical (it separates-0.0from0.0and orders NaNs by bits), so a reversed descending slice equals what the full sort produced.median: monotone input takes the old middle read; otherwiseselect_nth_unstable_by(n/2), and for an even count the lower neighbour is the maximum of the lower partition, which is the element atn/2 - 1in sorted order (the partition is non-empty becausen >= 2there).kth:idxis always in range after the existingkchecks (NaN, below 1 and abovelenstill return#NUM!), and selection at that index returns the same element assorted(v)[idx].interp: still#NUM!when the floor index is out of range; the next order statistic is the minimum of the upper partition (or none, givinglo), which is whats.get(i + 1)returned, and the interpolation arithmetic is unchanged, so the result is bit-identical.percentile_incandpercentile_exccomputeposbefore moving the vector and keep their range checks.selection_matches_full_sort_bit_for_bitcompares median,kthand both percentiles against kept copies of the old implementations on random data with ties, signed zeros, infinities, NaNs and both signs, at lengths 0 to 4,096, including already ascending, already descending and nearly ascending inputs (the monotone shortcut and its fallback).
No findings. I did not run the tests or the timings. Approval still needs a run on the current head and target.
Fixes #2679
What. MEDIAN, PERCENTILE(.INC/.EXC), QUARTILE(.INC/.EXC), LARGE and SMALL now find the one or two order statistics they need with
select_nth_unstable_by(f64::total_cmp)(O(n)) instead of sorting all the data (O(n log n)). Input that is already sorted (ascending or descending, e.g. a sorted column) is detected by one cheap pass and read directly, because the std sort handles such runs in O(n) and selection is slower there. MEDIAN with an even count takes the maximum of the lower partition for the lower middle value; PERCENTILE's interpolation takes the minimum of the upper partition as the next value (only computed when the fractional part is non-zero). PERCENTRANK and othersorted()callers are untouched.Why. The sort was the dominant cost, and
LARGE(range, SEQUENCE(300))repeated it once per spilled cell.Numbers (release,
taskseton one core,perf stat -e instructions:u, setup subtracted, 4 identical runs each):Adversarial inputs (same method, instructions per call, upstream/main vs this branch; core =
aggregate_values(_k), formula = call with a range argument,LARGE/SMALLwith k = {1,2,3,10} lifted):Before the sorted-input check, sorted/reverse/block-duplicate 10,000-value inputs were 1.2-1.8x main. The organ-pipe shape (instructions only; wall time at 10,000 was equal or lower) is the one remaining case where MEDIAN costs more than a full sort.
How verified. The new test
selection_matches_full_sort_bit_for_bitkeeps the old full-sort implementations as an oracle and compares results bit for bit (or same error) on randomised data: lengths 0 to 4096, many ties, -0.0/+0.0, NaN, infinities, k and percentile values out of range, fractional k, and both LARGE and SMALL. The test also covers already ascending, descending and nearly ascending data.cargo test -p gridcraft-functionspasses (148 tests),cargo fmt --all --checkandcargo clippy -p gridcraft-functions --all-targets -- -D warningsare clean.🤖 Generated with Claude Code
Summary by CodeRabbit