Repository navigation
perf: borrow LMDB strings without copying and reuse execution buffers - #399
Merged
Merged
Conversation
A reused executor, such as a Program's, resets its tree before every execution, and MarkApply kept its materialized RHS through that reset, so an IN/ANY subquery answered with the rows of an earlier execution, possibly from another transaction. Executor::start now marks the reset as a restart, and MarkApply keeps its cache only when a parent rescans it within one execution.
…ctions DataValue::Utf8 and DataValue::Binary now hold Utf8Str and BinaryBytes, which are owned or point into the reads of a MappedScope. An LMDB read-only transaction holds one, so the Char, Varchar and Varbinary columns it decodes point into its memory map instead of being copied. Mapped values never leave a transaction as is: cloning one copies it, and result rows (ResultRow) read without copying but hand values out copied, or lent for 'txn. TransactionIter and ProgramIter borrow their transaction, so their next_tuple lends values until it ends; other iterators lend them only within the callback. The ORM decodes through FromTxnRow and FromTxnValue on those iterators (txn_orm, Transaction::get and Transaction::fetch), and the Model and Projection derives accept one lifetime, so Cow<'txn, str> fields borrow from the transaction. Breaking: the value types of Utf8 and Binary, next_tuple callbacks and FromQueryRow take a ResultRow, and Transaction::fetch returns a TxnOrmIter.
- StorageTransaction::range/range_rev and remove_range take a BytesPool; LMDB and memory iterators copy their bounds into pooled buffers and InnerIter::recycle hands them back. - TableCodec range-bound helpers hand their BytesPool to the callback. - IndexIter takes its encode buffers from the pool lazily, and finished executions recycle every storage iterator's buffers. - DataValue implements clone_from to reuse string, binary and tuple buffers; ParamArena::bind overwrites the previous values with it.
- IndexScan keeps its ranges in ScanRanges: the plan's static range, or an owned range that parameters bind into with Range::bind_from, so reruns reuse its buffers. IndexIter no longer holds ranges; each call lends them with the cover mapping through IndexScanView. - The cover mapping is computed once per executor (TupleMapping::cover) instead of on every read_by_index. - Bound keys encode into ExecBuffer::bound_buffers, and StorageIter holds IndexIter unboxed (416 -> 200 bytes on LMDB). - DataValue/RangeValue/Range bind_from replace bind_parameters, writing a bound template over the previous value in place; ParamArena::bind matches the slot directly. - Range::as_slice; next_scan_tuple and next_tuple_for_test take a closure.
- Executions reuse the allocation of their storage iterator slots: an emptied slot Vec goes back to ExecBuffer type-erased (SpareIters) and comes back when the slot layout matches. New slots reserve exactly. - A ProgramIter dropped before its rows run out still hands its iterator buffers and slots back to the program. - Program::execute takes `(usize, Param)` pairs. Param::Str and Param::Bytes borrow, and binding copies them into the buffers of the previous execution, so string parameters bind without allocating. tpcc's DbParam follows.
- Fixed-size integers decode straight from the row slice. - SkipFixed and SkipVariable carry `null`: with it the column's bytes are skipped but it keeps its slot as a NULL. SkipResidualFilterColumns uses it for table-row reads (looked-up index rows, primary key and sequential scans), so a column only the eliminated index predicate read is no longer decoded. Covered reads keep using the cover mapping. - ProjectOperator::exprs holds `(Option<usize>, ExprRef)`. The last optimizer rule, MapProjectColumns, sets the input position of each plain column no other projected expression reads, and rewrite moves that value out instead of evaluating and cloning it. - ProjectOperator::new takes an iterator of expressions.
…ons read-only Strings decoded from stored rows are no longer validated as UTF-8 outside debug builds: every stored string was encoded from a `str`. Order-Status and Stock-Level now run in read-only transactions, as TPC-C allows; SQLite starts them with a deferred BEGIN.
Operators that keep values while they run cloned them, and cloning a mapped string copies it out of the read transaction. They now copy with `DataValue::clone_in_txn`, which leaves a mapped value pointing into its read: grouping, distinct, sort, window and join keys, joined rows of hash and nested-loop joins, MIN/MAX and DISTINCT aggregate state, IN-subquery keys, index probes and set-operation rows. Each drops what it kept when `reset` runs before the next execution. `Clone` still copies, so values that leave the engine stay owned.
alloc_trace runs one prepared, index-driven query on KiteSQL (LMDB) and on SQLite, with and without mmap, for notes from 64 B to 256 KiB. It reports latency, malloc and lookaside counts, live heap, and where the returned note pointers point. A multi-op query passes the same rows through grouping, a window and a sort. borrowed_parse keeps a whole result set after the query, as raw rows and parsed from JSON and CSV, borrowing every string on KiteSQL and owning them on SQLite. Both count allocations by interposing glibc malloc, so they run on Linux with glibc only.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #399 +/- ##
==========================================
- Coverage 93.07% 93.03% -0.04%
==========================================
Files 273 274 +1
Lines 39175 39998 +823
==========================================
+ Hits 36461 37214 +753
- Misses 2714 2784 +70
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
IndexScan borrows the plan's static range or owns one that parameters bind into, so the ScanRanges enum and its range() accessor go away.
Fixes clippy's missing_safety_doc and drops the doc comment of Tuple::deserialize_from_into.
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.
What problem does this PR solve?
LMDB reads copied every string and binary out of the memory map, and reused executors reallocated their buffers on every execution.
Issue link:
What is changed and how it works?
reset.Breaking:
Utf8/Binaryvalue types,ResultRowinnext_tuple/FromQueryRow,Transaction::fetchreturnsTxnOrmIter,Program::executetakes(usize, Param).Code changes
Check List
Tests
Side effects
Note for reviewer