Skip to content

perf(processor): keep Intersect's per-tuple scratch buffers on the operator - #1372

Merged
adsharma merged 1 commit into
LadybugDB:mainfrom
zahariash:perf/intersect-probe-buffers
Oct 24, 2026
Merged

adsharma merged 1 commit into
LadybugDB:mainfrom
zahariash:perf/intersect-probe-buffers

Conversation

@zahariash

Copy link
Copy Markdown
Contributor

This work was produced with the help of language models.

Fixes #1371.

Intersect allocated while probing: an unused vector for every probed row and five buffers for every intersected tuple. That is most of the allocation calls of LDBC SNB SF1 q30 and of the forum-member query below.

Fix. The unused vector is gone. The five buffers are members sized once in initLocalStateInternal; the selection vector holds DEFAULT_VECTOR_CAPACITY, so each task clone allocates one 16 KB buffer instead of up to one per tuple. Init also resets probedFlatTuples, tupleIdxPerBuildSide and carryBuildSideIdx, which only the constructor set.

LDBC SNB SF1, per execution instructions, 1 thread allocation calls wall, 4 threads
q30 3,262.3M → 2,712.4M (−16.9%) 3,648,821 → 868,840 187.7 → 170.9 ms
forum-member likes¹ 7,969.4M → 5,599.7M (−29.7%) 12,588,305 → 417,206 442.3 → 352.7 ms

¹ MATCH (f:Forum)-[:hasMember]->(p:Person)-[:likePost]->(:Post)<-[:containerOf]-(f) RETURN count(*). Wheel-like builds against main b9db312, same rows; wall is the median of 3 interleaved rounds. q1 and a two-hop knows count, without an INTERSECT, stay within run-to-run noise.

Tests. cyclic/large_lists.test gives a node 3,000 out-neighbours, so its list spans two build tuples, and runs a triangle and a three-build-side 4-clique over it, both pinned with HINT; expected values come from main. Full ctest: 3165/3165.

Related to #1291; these allocations were per row, not per execution.

Intersect allocated on every probed row: probeHTs built a vector of
vectors it never read. Each intersected tuple allocated five more
buffers: the tuple pointers, the lists, the list order, the left
selection vector (up to 16 KB) and the list of selection vectors to
slice. These are now members sized once in initLocalStateInternal, so
probing does not allocate.

initLocalStateInternal also resets the carry state (probedFlatTuples,
tupleIdxPerBuildSide, carryBuildSideIdx), which only the constructor
set, so a reused operator starts clean as its comment says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sliceSelVectors(selVectorsForIntersectedLists, lSelVector);
lSelVector.setToUnfiltered();
sliceSelVectors(selVectorsForIntersectedLists, leftSelVector);
leftSelVector.setToUnfiltered();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new code reuses one leftSelVector across intersect rounds:

  twoWayIntersect(..., leftSelVector, ...); // shrinks leftSelVector to the survivors
  sliceSelVectors(..., leftSelVector);
  leftSelVector.setToUnfiltered(); // <- keep the shrunken size, just unfilter it for next round

The delta between the two variants of setToUnfiltered() is subtle and easy to miss.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the overload hides it. This merged before I got to it, so #1382 passes the size explicitly, leftSelVector.setToUnfiltered(leftSelVector.getSelSize()), with a comment that twoWayIntersect leaves the surviving left IDs at the front of outKeyVector. Resetting the size to the first list's length instead fails acc_hj, cyclic/single_label, cyclic/multi_label and cyclic/large_lists.

@adsharma
adsharma merged commit 339bd6d into LadybugDB:main Oct 24, 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.

Intersect allocates scratch buffers for every probed row, 3.7M per LDBC SF1 q30 run

2 participants