Skip to content

perf(storage): decode sparse CSR headers in clusters and bound the neighbour refill - #1294

Draft
zahariash wants to merge 1 commit into
LadybugDB:mainfrom
zahariash:perf/csr-header-clusters
Draft

zahariash wants to merge 1 commit into
LadybugDB:mainfrom
zahariash:perf/csr-header-clusters

Conversation

@zahariash

Copy link
Copy Markdown
Contributor

This work was produced with the help of language models.

Follows up #1268.

Draft, opened early to coordinate with #1286 and #1291 on scan-state setup and per-execution allocations, not meant to merge before the freeze ends; it applies cleanly on top of #1286 with the same gains.

For bound nodes spread thinly over a node group the span from #1268 was still most of the CSR header: LDBC SNB SF1 q22 decoded 7.2M entries for 909 bound nodes, 43% of its instructions. Each neighbour-cache refill copied 2048 rows whether needed or not, 10% of q13.

Fix. A thinly spread batch decodes only clusters of the header entries its bound nodes need and records the ranges, so a later batch re-decodes only on a miss; a refill stops at a gap of 256 or more rows between lists. Clusters rely on RelTable::initScanState sorting bound nodes by offset, which #1286 keeps; data-column scan states are set up once per node group.

Performance. LDBC SNB SF1, 1 thread, user instructions against main a7e923e, rows identical: the 30 benchmark queries 6,055M → 5,631M (−7.0%); engine-only IC12 −40%, IC7 −28%, IC4 −25%, IC8 −18%, the other measured IC queries within −2.6%…+0.2%; heavy whole-table queries −0.3%, filtered extends −1.4%, none outside −3.1%…+0.9%; malloc calls per execution +1 to +12.

query main this PR
q13 186.0M 29.3M
q22 125.0M 34.7M
q27 73.2M 32.2M
q26 19.4M 8.7M
q19 88.8M 49.9M
q7 96.7M 75.9M
q30 3,630M 3,574M
Method

Both builds are wheel-like release builds of the Python module from the same worktree, on the prrao87/graph-benchmark-ldbc SF1 database. Each count is perf stat -e instructions:u of a process running the query 4 times minus one running it once, divided by 3, at 1 thread; the rows of the last execution are compared between builds. Queries of 4–6M move ±5–13% between builds with no logic change; the largest increase is q29 3.9M → 4.4M. IC1, IC4, IC7–IC9 and IC11–IC14 are perf samples of the engine library, since the IC harness's polars part is too noisy for whole-process counts. Heavy queries are 1.2G–15G, filtered extends 10M–1.6G. Malloc calls are from an LD_PRELOAD counter over the whole process, driver included, so only the delta between builds is meaningful; the extra calls are the first growth of each new scan state's decoded-range vector. On top of #1286 (ad3f67a) the 30 queries go 5,804M → 5,379M (−7.3%) with the same per-query gains, heavy −0.3%, filtered extends −1.6%, allocations +1 to +14.

Tests. scan/csr_header_clusters.test covers clustered and packed neighbour sets, node 0 and the last header node, an insert and a delete inside a cluster, bound nodes past the persistent header and a batch covered by an earlier one's clusters, across two CHECKPOINTs. Performance-only, so it passes on main too; runtime checks fail it on reading an undecoded header entry, though it does not assert which decode path ran. Full suite with ENABLE_RUNTIME_CHECKS: 3,101 tests pass; on top of #1286 only #1286's own two failures.

…ighbour refill

A rel scan decoded the CSR header span from the entry before a batch's first
bound node to its last one. For bound nodes spread thinly over a node group
that span is most of the header: LDBC SF1 q22 decoded 7.2M entries for 909
bound nodes. Every refill of the neighbour cache also copied the next 2048
CSR rows, whether the batch needed them or not.

A batch whose span is at least 4x its bound nodes now decodes only clusters
of the entries those nodes need, merging runs under 256 entries apart, in
place in the full-size header buffer: at most 256 clusters, counted as one of
the group's 16 partial decodes. The header records the decoded ranges and a
whole-header flag, so a later batch re-decodes only when one of its nodes
falls outside them, and nodes past the on-disk header read as empty lists.
A batch out of offset order gets no clusters. The cluster list reuses the
header's vector, and data-column scan states are initialized once per node
group.

A refill checks at most the next 4 bound lists, stops before the first that
starts 256 or more rows past the lists before it, and otherwise fills the
2048-row window up to the batch's last list. It runs out of line from the
loop that serves cached rows. With runtime checks the header buffer is
poisoned on group entry and every header read asserts coverage.

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

Copy link
Copy Markdown
Contributor

Freeze is targeted for Oct 19. main is still open.

This branch has not been deployed

No deployments
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