Repository navigation
Conversation
…t path Three contained removals, measured with an LD_PRELOAD allocation profiler on 120x cached-plan executions of LDBC Q1 (678 malloc+free pairs per execute before, 573 after; Q1 hot exec p50 224us to 210us): - PhysicalOperator::registerProfilingMetrics: bind thread-local dummy metrics when the profiler is disabled instead of registering into the profiler unordered_map (map nodes plus strings plus unique_ptr per operator per task, plus mutex traffic). - RelTable::initScanState: reuse a scan-state positions buffer across per-morsel inits, skip the bound-ID sort when input is already ordered, and replace the stdmap node-group partition with a linear run scan over the sorted offsets. - ResultSetPool: per-thread MRU cache of up to 8 descriptor slots instead of a single slot, which thrashed across pipelines and reallocated all vectors every execution. Steady-state pool hit rate 50pct to 100pct.
Checkout of the cached physical-plan template clones hundreds of small allocations per execution (operators, evaluators, print infos, schemas, descriptors). Keep up to 10 ready plans per thread, reset via prepareForReuse, and reuse them. Descriptors stay attached on pooled plans; nested same-statement execution falls back to fresh clones.
|
Update: added a second commit on this branch — per-thread physical-plan pooling. perf(processor): pool cloned physical plans per thread on the fast path Checkout of the cached physical-plan template clones hundreds of small allocations per execution (operators, evaluators, print infos, schemas, descriptors). The pool keeps up to 10 ready plans per thread, reset via the existing Ground-truth A/B with uprobes (bpftrace
Breakdown of the -733 (LD_PRELOAD stack attribution, directionally confirmed): profiler metric registration ~-60, ResultSet LRU ~-30, initScanState map/sort Note: the LD_PRELOAD shims undercount ~3.6x vs uprobes (internal libc call paths bypass interposition) — absolute shim numbers in earlier comments should be read as relative only. Validation so far: Q1 result identical (Thomas Brown); optimizer suite 4/4; targeted e2e subsets (match/subquery/join/transaction/delete/ldbc, 211 tests) green on the pre-pool state. Requesting full e2e on CI for the final state (local full-suite runs exceed this dev box). |
…hunk functors Reused-operator hardening plus allocator fixes. No behavior change for fresh operators (all cleared vectors start empty; all SFO lambdas evaluate identically): operator initLocalState fills now clear member vectors first so re-execution starts clean (hash join build and probe, intersect, cross product, order by and top-k, hash and simple aggregates, scan tables, hash-aggregate local state), and ColumnChunkData keeps the compression algorithm on the chunk with capture-this stdfunction members that fit small-function optimization instead of heap-allocating 24-byte functors per chunk.
|
Update: added a third commit (operator-init idempotency + SFO-bound chunk functors). Deliberately not included: per-task pipeline-replica pooling. I prototyped pooling Current per-execute malloc tally (uprobe ground truth on Q1): main 3,021 -> 2,288 with plan pooling included. Remaining big-ticket items for a follow-up: per-task clone elimination (needs the state-lifecycle audit above), ValueVector pooling beyond ResultSet reuse, scan-state teardown allocs. |
|
Validation update (perf-only tree, no lookup join): Q1-Q10 shell sweep vs base Note: I investigated exact single-hop fanout for INLJ root selection on top of this branch (probing secondary ART indexes + per-offset CSR degrees at plan time) and reverted it: |
|
This turned up while testing an overlapping optimization on top of this branch: a CSR scan change (sparse CSR header decode in clusters plus a bounded neighbour-cache refill) built on the bound-node order this PR's On ad3f67a merged onto main a7e923e (same tree as the PR head), a prepared statement over a node table of more than one node group loses rows after two executions with a selective parameter: the next one is still right, every later one returns only the last node group. import tempfile
import ladybug as lb
d = tempfile.mkdtemp()
n = 140_000 # more than one node group (131,072 nodes)
open(f"{d}/n.csv", "w").writelines(f"{i}\n" for i in range(n))
conn = lb.Connection(lb.Database(f"{d}/db"), num_threads=1)
conn.execute("CREATE NODE TABLE N(id INT64, PRIMARY KEY(id))")
conn.execute(f'COPY N FROM "{d}/n.csv"')
query = "MATCH (a:N) WHERE a.id < $k RETURN count(*)"
stmt = conn.prepare(query)
print([conn.execute(stmt, {"k": k}).get_all()[0][0] for k in (3000, 3000, n, n)])main prints |
|
@zahariash bug likely caused by pooled plan leakage. Need a more comprehensive audit of all operators for properly resetting state when the pooled plan is reused. |
…r resets - Clear physicalPlanPool when the physical-plan cache is invalidated on rebind (parameter type change / catalog version bump). Pooled trees cloned from the previous template reference stale evaluators and sink descriptors; the second execution after a type change reused them and returned wrong results (ApiTest.PrepareWithoutParamsReusesPlan). - Reset per-execution vectors missed by the idempotency pass so pooled operator objects start clean: ResultCollector payload vectors, QueryPrimaryKeyLookup outVectors, PackedFilteredCount multiplicity states/local counts, UnionAllScan vectors, GroupedReachableCount key vectors/scan states, CopyTo input vectors. - RelTable::initScanState: on empty batches drop the stale multi-group plan and mark the scan exhausted instead of rescanning the previous batch. - ColumnChunkData: call flush/metadata impls directly instead of via this-capturing std::functions (use-after-move when chunks relocate; also removes the per-chunk allocation entirely). - ResultSetPool: revert MRU-8 to the single per-thread slot. With unique descriptor ids per uncached query the 8-slot cache accumulates garbage until the cap, so buffer-manager memory keeps growing across identical queries (BufferManagerTest.TestBMUsageForIdenticalQueries).
e8d018c to
3b181e6
Compare
What
Three contained removals of per-execution allocations on the cached-plan path, found with an
LD_PRELOADallocation profiler (stack sampling) driving 120 cached-plan executions of LDBC Q1. Measured: 678 malloc+free pairs/execute before, 573 after (-15%); Q1 hot exec p50 224us to 210us.physical_operator.cpp): when profiling is disabled, bind thread-local dummy metrics instead of registering into the profilerunordered_map(map nodes + strings +unique_ptrper operator per task, plus mutex traffic). ~60 alloc pairs/execute removed.RelTable::initScanState(rel_table.cpp): reuse a scan-state positions buffer across per-morsel inits, skip the bound-ID sort when the input is already ordered, replace thestd::mapnode-group partition with a linear run scan over sorted offsets (common single-group case now allocates nothing).ResultSetPool(processor.cpp): per-thread MRU cache of up to 8 descriptor slots instead of a single slot, which thrashed across pipelines (each pipeline sink has its own descriptor id) and reallocated all vectors every execution. Steady-state pool hit rate ~50% to ~100% (verified with temporary hit/miss instrumentation, since removed).Validation
AnonymousParquetDeleteReloadfailure, which fails identically on basemain.Notes
main, no shared files.