This pr is huge again but I will try to introduce each improvement one
by one.
## Overview
### Remove Rc for Text and Blob.
In general copying is bad, that's why we hid it with `Rc`s. With the
introduction of `ImmutableRecord` we make it less relevant because now
we will copy only once anyways, no other place should copy it so we can
avoid using `Rc`. If we we were to copy it it most likely means where
are doing something wrong.
### Reuse `Text` and `Blob` OwnedValues.
Most of the queries spend time overwriting the same register over and
over. What about we don't allocate new `OwnedValue` and we just simply
reuse the `OwnedValue` and extend the internal buffer. That's what I did
and it worked quite nicely.
### Make `Register::Record` be `ImmutableRecord`
`ImmutableRecord` basically means "serialized record", that's why all
the data is contained in a single payload buffer. There is a list of
values to reference that payload to reduce time complexity of search --
there is an argument to make a record without this vec to reduce memory
footprint. This improvement I don't think it had a direct impact on
performance but it is a simpler way to lay the memory without any
complicated reference counted pointers, and instead we use a contiguous
piece of memory.
### Make `ImmutableRecord` reusable in `BTreeCursor`.
`BTreeCursor` allocated and deallocated records when it needed a new
one. This is obviously a big waste because we could be reusing the
internal buffer to avoid allocations. `ImmutableRecord` proved to be
useful here because now, we will only store a single `ImmutableRecord`
in the cursor that we will never deallocate -- we will just reallocate
when needed and replace the current one with the next one on demand.
## Return `Row` as a reference of Registers.
A `ResultRow` bytecode takes care of gathering all the columns of a row
and returning them to the user. Previously we could create a new
`Record` struct with all the cloned values which proved to be wasteful.
SQLite is smart about this so we must be as well. Basically a row now is
a wrapper for `struct Row { *const Register, count: usize }`, and we
basically include some QOL methods to avoid using pointers directly.
I know pointers are unsafe. That's why this row will be invalidate on
the next step of the VM and this row should be not used outside there.
### Inlining go brrr
`read_varint` and `read_value` are called in a tight loop making it easy
to see overhead of the call stack. That's why I sprinkled some
`#[inline(always)]` and saw something like a 15% speed boost.
## read_record with custom `SmallVec<T>`
We tend to overuse vectors for everything, this is quite bad because it
requires heap memory allocations. We can avoid this with a simple
`SmallVec` that simply fallsback to a vec with more complex scenarios.
## Benchmarks!
```
### before
fun/limbo » cargo bench -- limbo_execute 2>&1 | grep -B 1 "time: " | tee out.log
Execute `SELECT 1`/limbo_execute_select_1
time: [43.958 ns 44.056 ns 44.154 ns]
--
Execute `SELECT * FROM users LIMIT ?`/limbo_execute_select_rows/1
time: [407.82 ns 408.57 ns 409.41 ns]
--
Execute `SELECT * FROM users LIMIT ?`/limbo_execute_select_rows/10
time: [2.7335 µs 2.7386 µs 2.7443 µs]
--
Execute `SELECT * FROM users LIMIT ?`/limbo_execute_select_rows/50
time: [13.451 µs 13.485 µs 13.520 µs]
--
Execute `SELECT * FROM users LIMIT ?`/limbo_execute_select_rows/100
time: [26.967 µs 27.077 µs 27.201 µs]after:
```
### after
```
fun/limbo (more-register) » cargo bench -- limbo_execute 2>&1 | grep -B 1 "time: " | tee out.log 130 ↵
Execute `SELECT 1`/limbo_execute_select_1
time: [33.386 ns 33.440 ns 33.510 ns]
--
Execute `SELECT * FROM users LIMIT ?`/limbo_execute_select_rows/1
time: [326.79 ns 327.37 ns 328.03 ns]
--
Execute `SELECT * FROM users LIMIT ?`/limbo_execute_select_rows/10
time: [1.5817 µs 1.5849 µs 1.5889 µs]
--
Execute `SELECT * FROM users LIMIT ?`/limbo_execute_select_rows/50
time: [7.3295 µs 7.3531 µs 7.3829 µs]
--
Execute `SELECT * FROM users LIMIT ?`/limbo_execute_select_rows/100
time: [14.538 µs 14.570 µs 14.606 µs]
```
Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>
Closes#1197
We make read_record faster by not allocating Vec if not needed. This is
why I introduced a simple `SmallVec<T>` that will have a stack allocated
list for the simplest workloads, and a heap allocated if we were to
require more stuff.
Both read_varint and read_value, at least in my mac m4, were not
inlined. Since these functions are called so many times it made sense to
inline them to avoid call overhead. With this I saw something like 20%
improvement over previous commit in my m4.
Improve allocation usage from ImmutableRecords by reusing them.
ImmutableRecord is basically a contigous piece of memory that holds the
current record. If we move to some other record we usually deallocate
the previous one and allocate a new one -- obviously this is wasteful.
With this commit we will reuse the ImmutableRecord to allow payload to
be extended if needed or reused if we can, making it faster to iterate
records basically.
Closes#1206Closes#447Closes#1117
Issues:
1. Rust floating point math fucntions are non deterministic.
2. SQLite have complex floating point display rules.
I changed rust functions to libm for math ops and implemented Display
trait for OwnedValue:Float. A lot of float formatting SQLite probably
inherits from C have to be handcrafted.
Closes#1208
This PR has two parts:
## 1. Make reading cells faster
My benchmark was simple, running `test_sequential_write` test and see
how long it takes. It is a nice benchmark because it will write a new
row and then `select * from t` for every write to check the contents.
## From:
```bash
cargo test test_sequential_write --release -- --nocapture --test-threads=1 7.21s user 0.34s system 88% cpu 8.527 total
```
### To:
```bash
cargo test test_sequential_write --release -- --nocapture --test-threads=1 3.14s user 0.31s system 82% cpu 4.161 total
```
## 2. Fix reading overflow pages.
The code to read overflow pages was wrong, `read_payload` would try to
read as many overflow pages as possible but if there weren't in cache
they would return `IO` and not read more pages after that. This PR makes
every read record to check if it needs to read from overflow pages and
if not, it will simply read from the current page.
Closes#1141
### The problem:
I often need to copy the output of an `Explain` statement to my
clipboard. Currently this is not possible because it currently will only
write to stdout.
All other limbo output, I am able to run `.output file` in the CLI, then
enter my query and in another tmux pane I simply `cat file | xclip -in
-selection clipboard`.
### The solution:
Expose a `statement.explain()` method that returns the query explanation
as a string. If the user uses something like `execute` instead of
prepare, it will default to `stdout` as expected, but this allows the
user to access the query plan on the prepared statement and do with it
what they please.
Closes#1166