Repository navigation
perf(exec): keep 4 x threads parts ahead of a sink that merges states - #218
Merged
Merged
Conversation
A sink that merges each part into partial states (global, grouped and two-level aggregates, the top-N) now lets 4 x threads parts run or wait ahead of its consumer instead of 2 x threads. Parts take as long as the bytes they read, and one part several times the mean fills a window of 2 x threads with finished parts while it runs, so the other workers idle until it is taken. A part union, whose parts hold their rows, and the join builds and probes keep 2 x threads. Tests: MergingSinksKeepAWiderWindow checks both windows; EachSinkOpensItsWindowAheadOfAFailedFirstPart checks, for every sink kind, that exactly its window's parts open ahead of a failed first part. Both fail with the window back at 2 x threads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes. The GROUP BY sink now passes the window it gives its lanes (window_) to its PartScheduler too, so EachSinkOpensItsWindowAheadOfAFailed FirstPart covers the lanes' bound: setting window_ back to PartWindow fails it (checked). The comments say which schedulers keep PartWindow (a part union, a join's build, a right join's own probe) and that a probe under a merging sink runs in MergeWindow, and no longer claim that a finished part holds no rows (a routed GROUP BY part holds its rows split by partition, a top-N part at most limit + offset). The test's expected scan set is min(window, parts). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Amends ADR 0013 (Decision, Memory, Load balance) with step 1's measurement on a 2-socket, 128-hardware-thread host: at 16 and 32 threads the threads idle on the window, because a part's cost follows the bytes it reads and one heavy part fills a window of 2 x threads. Records the paired A/B of MergeWindow against main on all 43 ClickBench queries and the 22 queries derived from TPC-H as a control, and what the change does not fix (the tail, the merge's 32 lanes, a right join's serial gather). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clang-tidy (pixi run tidy) rejected the test's shape table: positional aggregate initializers (modernize-use-designated-initializers), windows computed in int and widened (bugprone-implicit-widening-of-multiplication- result) and std::iota (modernize-use-ranges). No change in what it checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Claude finished @Hor911's task in 23s —— View job Claude review: no problems found
|
Coverage
Coverage gate: PASS (floors: Clang source-based coverage of the hermetic tests over |
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.
Summary
A sink that merges each part into partial states -- the global, grouped and two-level aggregates and the top-N --
now lets 4 × threads parts run or wait ahead of its consumer instead of 2 × threads (
MergeWindow); a GROUP BY'slanes take the same value as their bound. A part union, a join's build and the probe a right join schedules itself
keep 2 × threads (
PartWindow). Every part pipeline below a merging sink, a join's probe included, runs in the widerwindow.
Why (ADR 0013 load balance, step 1's measurement, on a 2-socket, 64-core, 128-hardware-thread host): at 16 and 32
threads antb1's threads idle where DuckDB's do not, and the idle is the window. A part takes as long as the bytes it
reads; on the full ClickBench data one row group of the most uneven column holds about 6 times the mean, and while
it runs the parts after it finish and fill the window, which only taking that part frees. Traced part by part, Q20
at 16 threads kept its workers 68% busy with 28% of the threads' time lost to the full window; DuckDB, handing out
the same row groups without a window, kept 95% busy. With 4 × threads the same trace is 94% busy.
ADR 0013 is amended with the measurement (Decision, Memory and Load balance), including what the change does not
fix: from 64 threads the idle is the last parts, started in part order (step 2), and the merge's 32 lanes; on the
queries derived from TPC-H the idle is the consumer's own work (a right join's serial gather of its emitted rows,
ADR 0026's C2b).
Type of change
Verification
Measured on the 128-thread host with two frozen builds (
pixi run releaseof main 20b5c53 and of this change onit,
cmp-different), one process per query, best of 3 tries, two rounds with the arm order alternating per queryand per round; the host is shared, so every run records the CPU other processes used and disturbed points were
re-run. Full data (100 files). The branch was then rebased onto main b9337b9 without conflicts: #215-#217 change only
src/plan/and other ADRs (none of the ClickBench queries has a join, so their plans are unchanged).ClickBench, all 43 queries, total of the best times, main → this change:
Largest moves: Q20 1.103 → 0.825 s at 16 threads and 0.527 → 0.368 s at 64; Q27 1.290 → 0.966 s and 0.649 →
0.511 s; Q28 4.303 → 3.602 s at 16. Slower beyond the noise: only Q23 at 64 threads (0.398 → 0.535 s; it gains at 16
and 32). Lane-heavy GROUP BYs (Q4, Q17, Q18) lean slower at 64 threads inside main's spread. At 128 threads the idle
is the last parts and the merge's 32 lanes, which this change does not touch.
The 22 queries derived from TPC-H (sf100), as a control, page cache verified resident: 16 threads 116.535 →
116.321 s, 64 threads 58.367 → 58.781 s; traced, Q1 and Q4 keep the same schedule (97% and 75% busy) with either
window. This workload is derived from the TPC-H Benchmark and is not comparable to published TPC-H Benchmark
results, as this implementation does not comply with all requirements of the TPC-H Benchmark.
Tests:
MergingSinksKeepAWiderWindowchecks both windows;EachSinkOpensItsWindowAheadOfAFailedFirstPartchecks,for every sink kind, that exactly its window's parts open ahead of a failed first part. Mutation-checked: with
MergeWindowat 2 × threads both fail; with only the GROUP BY'swindow_atPartWindowthe second fails.Checklist
pixi run checkpasses locally (lint + clang Debug -Werror + hermetic tests)docs/adr/0013-parallel-execution.mdis underCODEOWNERS and needs a maintainer's review
AI assistance
change, the tests and the ADR amendment; a read-only reviewer agent reviewed the diff (5 findings, all fixed).
🤖 Generated with Claude Code