Skip to content

perf(processor): run one-morsel node scans on the calling thread, allocate TopK scratch on use - #1499

Merged
adsharma merged 2 commits into
LadybugDB:mainfrom
zahariash:perf/narrow-small-node-scans
Nov 4, 2026
Merged

adsharma merged 2 commits into
LadybugDB:mainfrom
zahariash:perf/narrow-small-node-scans

Conversation

@zahariash

Copy link
Copy Markdown
Contributor

This work was produced with the help of language models.

Part of #1432.

On the issue's 50-row table, a prepared MATCH (t:T) RETURN t.id, t.age took 94 µs with the default 16 threads and 11 µs with CALL threads=1. The table is one morsel, but its scan ran as a parallel task, so every execution woke the worker pool; ORDER BY woke it twice. TopK also allocated and zeroed two 2048-value vectors per payload column in every buffer, which glibc faulted back in on each execution (36 page faults for LIMIT 10 on one thread).

Fix. A node table scan with at most one morsel, counting uncommitted node groups, and an ORDER BY merge with at most one sorted run become one-thread tasks, which run on the calling thread unless a query timeout is set (#1297). Only one thread had work there, so no parallelism is lost. The scan now sets up its shared state, where it counts morsels, just before scheduling. TopKBuffer allocates its scratch vectors on first use in reduce() or merge().

Median µs, Ryzen 9 6900HS, 16 threads, C++ API:

query on 50 rows text, main → PR prepared, main → PR allocations per prepared execution
scan 229 → 134 94 → 12 349 → 103
WHERE t.age > 45 301 → 184 102 → 12 503 → 137
+ ORDER BY t.age DESC 506 → 253 244 → 40 1,204 → 387
+ LIMIT 10 472 → 296 196 → 75 1,478 → 479

Most of the remaining text-query time is compiling; the comment on #1432 covers it.

Tests. SingleMorselTaskTest records each scanned row's thread with a UDF: a one-morsel scan runs on the caller, while three morsels, an uncommitted row, or two tables under MATCH (n) run on workers. A merge task accepts one thread for one sorted run and two for two. top_k_reduce.test reaches TopK's reduce(), which no test did. Each check fails when its condition is removed. Arrow and ice-disk tables with several row groups are not covered. Full ctest: no failures.

Cost. Scans of more than one morsel are unchanged. A 2M-node table (sum, sort, TopK) and a 1,000-node source with 2M edges stay within 0.4% of main's instructions, TopK measured on one thread. On LDBC SNB SF1 (the 30-query suite and complex IC1–IC14, 4 and 16 cores), no query is slower in every round; geomeans are 0.97–1.02 of main.

zahariash and others added 2 commits November 2, 2026 20:00
…n the caller

A node table scan whose source hands out one morsel, and an ORDER BY merge
with one sorted run, have no work for a second thread. Narrow those tasks to
one thread so they run inline instead of waking the worker pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TopKBuffer allocated and zeroed two 2048-value vectors per payload column plus
the boundary vectors in every local and shared buffer. Only reduce() and merge()
use them, so a buffer under the reduce threshold never needed them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adsharma

adsharma commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

I was concerned about sub-morsel parallelism. But it's not an issue. In the cases where the node groups are split, this optimization is bypassed because getNumMorsels() > 1.

@adsharma
adsharma merged commit a973d7f into LadybugDB:main Nov 4, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants