Smart2Raw
Home Applications How it works Performance Get started

Commercial

Premium Licensing Investment

More

Technical scope Cite About
Get in touch

Measured, not estimated.

Performance

Every number on this page came out of a program in the repository, and every one of them can be reproduced by a command you can run. Where a number could be checked against a naive loop, the program checks it and aborts on a disagreement — a benchmark that prints a pretty number it cannot defend is worse than no benchmark.

Seven column shapes, 4 million elements each, the query timings, and the command behind every table. Including the rows where Smart2Raw does not win — they are here for the same reason as the rest.

4231×range counting on a u8 column with the cumulative index — two 2 KB reads
0.000034 mscount_gt(220) on a column that stops at 200: the zone summary answers without reading payload
31test suites, 0 failures, clean under ASan and UBSan
100,950differential fuzz checks against a naive reference, with fixed seeds

The same column in every format

Seven column shapes, 4 million elements each, sizes in MB. S2R is the smallest form the library can produce for that shape.

The same column in seven formats 4 million elements · the rule marks the int64 input 4 million elements · the rule marks the int64 input Smart2Raw best classical alternative 0 10 20 30 40 megabytes int64 input · 30.52 MB A · uniform 0..200 3.81 dictionary A · uniform 0..200 — Smart2Raw 3.81 MB, dictionary 3.82 MB B · 12 distinct values dictionary 1.91 B · 12 distinct values — Smart2Raw 3.82 MB, dictionary 1.91 MB C · the same, sorted RLE 0.00 C · the same, sorted — Smart2Raw 0.06 MB, RLE 0.00 MB D · boolean 0/1 bitmap 0.48 D · boolean 0/1 — Smart2Raw 3.81 MB, bitmap 0.48 MB E · timestamps every 60 s 4.11 dictionary 41.01 E · timestamps every 60 s — Smart2Raw 4.11 MB, dictionary 41.01 MB F · random u64 30.52 dictionary 41.01 F · random u64 — Smart2Raw 30.52 MB, dictionary 41.01 MB G · ids in 0..1e6 15.26 dictionary G · ids in 0..1e6 — Smart2Raw 15.26 MB, dictionary 17.03 MB

The two red bars crossed the input: on a high-cardinality column the dictionary stores a dictionary the size of the data, and hands back 41.01 MB for the 30.52 MB it was given. Smart2Raw has no such bar — the widest class it has is the input. Look at rows B, C and D too, where the classical format wins: they are in the chart for the same reason as the rest. And note what this chart does not measure: it counts bytes, and bytes are only one axis — the other one is just below.

columnint64S2RdictionaryRLEbitmapS2R form
A · uniform 0..200 (telemetry)30.523.813.8230.37flat pool
B · 12 distinct values in 500..1150030.523.821.9127.97block-wise
C · the same, ORDERED30.520.061.910.00block-wise
D · boolean 0/130.523.810.4815.260.48flat pool
E · timestamps every 60 s30.524.1141.0130.52block-wise
F · random u64 (maximum entropy)30.5230.5241.0130.52flat pool
G · ids in 0..1e6 (high cardinality)30.5215.2617.0330.52flat pool
cc -O2 -std=c11 -I include benchmarks/format_matrix.c -o format_matrix
./format_matrix 4000000

The dictionary column is the theoretical floor of that format — codes of ceil(log2(k)) bits plus a dictionary of k values at 8 bytes, with no implementation overhead added. It is the most generous number the format can possibly produce.

Read row E and row F. On a high-cardinality column the dictionary is the size of the data, and the total lands at 41.01 MB against a 30.52 MB baseline — the format made the column larger than it was. RLE does the same on anything unordered, one run per value. A bitmap only exists at all when there are exactly two distinct values.

Smart2Raw has no such row, and not by tuning: it classifies by range, and its widest class is the int64 input. Row F is the proof — maximum entropy, and the result ties the baseline exactly. assert(s <= raw) runs inside the loop before each line is printed.

Bytes are only one axis

The table above answers "how much space". It does not answer the question that comes next, and that one decides what it costs to run: what you can ask of the bytes without first turning them into something else.

This chart isolates exactly that. The column is the same one, and both formats take practically the same space — 11.44 MB against 11.45 MB. With the bytes tied, what is left in the drawing is processing alone.

12 million elements · both formats take the same space: 11.44 against 11.45 MB 12 million elements · both formats take the same space: 11.44 against 11.45 MB 12 million elements · both formats take the same space: 11.44 against 11.45 MB Smart2Raw dictionary, implemented at its best 0 2 4 6 8 milliseconds, single core COUNT(x > 100) 0.60 0.63 parity COUNT(x > 100) — Smart2Raw 0.60 ms, dictionary, implemented at its best 0.63 ms SUM(x) 0.44 7.52 SUM(x) — Smart2Raw 0.44 ms, dictionary, implemented at its best 7.52 ms reach a non-SQL kernel * 0.00 · already contiguous 7.90 reach a non-SQL kernel — Smart2Raw 0.00 ms, dictionary, implemented at its best 7.90 ms

* Reaching a non-SQL kernel: a quantized dot product, a convolution, an int8 matmul, a DSP filter — each needs a contiguous native-width buffer. The warehouse format has to produce that buffer before it can start. Our pool already is that buffer.

The peer here is not a straw man. The dictionary is implemented at its best, over sorted distinct values — which makes the predicate a comparison on the codes themselves, with nothing to decode. That is why COUNT comes out level, and saying so is what earns the other two rows their credit.

And where we lose is measured alongside. On a column of 12 distinct values spread across a wide range, the peer with 4-bit codes wins: 11.44 MB against 5.72 MB and 0.468 ms against 0.326 ms — 2x larger and 1.44x slower on our side. The floor is arithmetic: 12 distinct values need log₂(12) = 3.58 bits, the peer uses 4, and the smallest native class here is 8. That absence is the decision that buys the 7.9 ms in the row above, not an oversight.

The full report — including what was retracted from an earlier version of it — is in benchmarks/warehouse/WAREHOUSE_FORMAT_BENCH.md. It measures 3.4.0, when the same loss was 4x and 3.2x; the affine factoring in 3.5.0 is what reduced it.

Query timings

whatbeforeafterwhere it comes from
count_gt on 8M ordered elements0.371 msbelow the clockorder is maintained, so binary search applies
count_gt(220) on a column ending at 2000.1435 ms0.000034 msthe zone summary answers without reading payload
range count on a u8 column4231×the cumulative index: two reads from 2 KB that do not grow with the data
12M elements with a stride22.89 MB / 1.033 ms11.44 MB / 0.468 msthe common step is divided out, exactly
4M timestamps, wrong shape vs right shape15.26 MB / 0.73 ms4.11 MB / 0.04 mss2r_recommend() picks the block-wise form

The cumulative index pays for itself in 11 queries and then costs nothing, because 2 KB does not grow with the column. It also refuses to answer when it is stale: the pool carries an epoch, every write bumps it, and the index records the epoch it was built at.

Against a real database: SQLite

The tables above compare against formats. A format is an abstraction, and almost nobody runs one in production — almost everybody runs a SQLite. benchmarks/maestro/ compares against it: the same input CSV, a real .db and a set of real .s2r files written to disk, both openable afterwards.

whatSQLite / int64Smart2Rawgainkind
SUM over the 12 columns that compact2635 µs16.3 µs161×measured
COUNT with a filter3900 µs69.5 µs56×measured
size on disk412.0 KB275.7 KB1.49× smallerexact
resident memory934.8 KB342.2 KB2.73×exact
data moved per scan868.1 KB275.4 KB3.15×exact
SUM throughput in SIMD2131 Mval/s7005 Mval/s3.29×measured
range filter throughput1416 Mval/s1668 Mval/s1.18×measured
python benchmarks/maestro/smart2raw_bench.py your_data.csv

Python here is only the conductor: it uses the standard library for SQLite and calls the real C kernels through ctypes — the native engine is compiled from s2r_shim.c on first run. With no compiler it still prints memory, disk and data moved, which are exact counts, and skips the two timed sections. Columns that genuinely need 64 bits, or that are floating point, show 0% on purpose.

What that 161× is not. SQLite gives you SQL, indexes, transactions and durability; here it is doing one job — scanning and aggregating integer columns, with no index — against a column built for exactly that job. The number measures the distance between a general engine interpreting bytecode row by row and a SIMD loop over contiguous bytes. It is not a flaw in SQLite; it is the price of generality, and it is large precisely because of that.

And the data is small on purpose — the demo CSV is about 670 KB, so everything fits in cache and what dominates is the interpreter's per-row cost. On much larger data the timed ratio does not hold; what holds are the three rows marked exact, because those are byte counts rather than a clock: 1.49× on disk, 2.73× in memory, 3.15× less data moved per scan.

Look at the 1.18×. The range filter barely improves, and the row is here for that reason. SUM on u8 processes eight values per lane where int64 processes one, and jumps to 3.29×; the filter was already cheap per element and its bottleneck is elsewhere. A space win does not become a time win by decree — it becomes one when the operation was memory-bound, and this row is the counterexample inside our own table.

Measure it in your own browser

The demonstration on the home page times the same queries on your own data, in your own browser, with warm-up and a varying argument so nothing can be cached — and it prints the result of every path so you can see them agree.

It will also show you where the win in space does not become a win in time: on a column small enough to sit in cache, reading a quarter of the bytes does not take a quarter of the time. Raise the element count and the difference appears.

How the whole thing is checked

bash scripts/build_and_test.sh

That fuzz suite exists because of a real defect it found: an unsigned column crossing 2^63 used to return truncated values in the block-wise layer, with no error, no warning and a valid CRC. Twenty-five suites of chosen cases missed it. It is fixed, and the minimal case {1, UINT64_MAX} is now a fixed test.

Where to go next

Where the trade is

The regime where the dictionary wins, stated plainly, with the number.

Technical scope →

Reproduce all of it

One clone and one command. The fuzz seeds are fixed on purpose.

How to cite and reproduce →

Measure your data

The demonstration times the same queries on your column.

Go to the demonstration →