Skip to content

Commit c6a1441

Browse files
authored
Merge pull request #27 from hadashiA/claude/performance-optimization-1c59d4
Optimize read hot path and expand benchmarks
2 parents ee5e5fa + b2f16b4 commit c6a1441

19 files changed

Lines changed: 761 additions & 153 deletions

README.md

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
66
DryDB is a read-only embedded B+Tree based key/value database, implemented pure C#.
77

8-
```
9-
| Method | Mean | Error | StdDev |
10-
|------------------- |------------:|----------:|----------:|
11-
| DryDB_FindByKey | 37.57 us | 0.230 us | 0.120 us |
12-
| CsSqlite_FindByKey | 4,322.48 us | 44.492 us | 26.476 us |
13-
```
8+
<picture>
9+
<source media="(prefers-color-scheme: dark)" srcset="docs/benchmarks/point_lookup_dark.svg">
10+
<img alt="Point lookup benchmark: DryDB 29 ns, RocksDB 241 ns, SQLite (prepared + immutable) 943 ns, SQLite (default) 6,656 ns per query" src="docs/benchmarks/point_lookup_light.svg">
11+
</picture>
12+
13+
See [Performance](#performance) for details.
1414

1515
## Features
1616

@@ -44,6 +44,64 @@ DryDB is a read-only embedded B+Tree based key/value database, implemented pure
4444
- CLI tool
4545
- Support for large BLOBs. (Values exceeding 65,536 bytes are stored on a separated page.)
4646

47+
## Performance
48+
49+
All benchmarks read a table of 10,000 rows (int64 primary key, 13-byte value) with 4 KB pages, on Apple M-series / .NET 10, measured with BenchmarkDotNet. Lower is better.
50+
51+
Two SQLite configurations are shown to keep the comparison fair:
52+
53+
- **default** — a command is created and parsed for every query (typical naive usage)
54+
- **prepared + immutable** — the statement is prepared once and reused, and the file is opened with [`immutable=1`](https://www.sqlite.org/uri.html#uriimmutable), the fastest read-only setup SQLite offers
55+
56+
### Point lookup
57+
58+
<picture>
59+
<source media="(prefers-color-scheme: dark)" srcset="docs/benchmarks/point_lookup_dark.svg">
60+
<img alt="Point lookup benchmark: DryDB 29 ns, RocksDB 241 ns, SQLite (prepared + immutable) 943 ns, SQLite (default) 6,656 ns per query" src="docs/benchmarks/point_lookup_light.svg">
61+
</picture>
62+
63+
### Range scan
64+
65+
<picture>
66+
<source media="(prefers-color-scheme: dark)" srcset="docs/benchmarks/range_scan_dark.svg">
67+
<img alt="Range scan benchmark (100 rows): DryDB 1.4 µs, SQLite (prepared + immutable) 9.6 µs, RocksDB 14.0 µs, SQLite (default) 17.4 µs per query" src="docs/benchmarks/range_scan_light.svg">
68+
</picture>
69+
70+
### Count by key range
71+
72+
<picture>
73+
<source media="(prefers-color-scheme: dark)" srcset="docs/benchmarks/count_range_dark.svg">
74+
<img alt="Count benchmark (8,000 rows): DryDB 15.2 µs, SQLite (default) 108 µs, SQLite (prepared + immutable) 117 µs, RocksDB 982 µs per query" src="docs/benchmarks/count_range_light.svg">
75+
</picture>
76+
77+
<details>
78+
<summary>Raw BenchmarkDotNet results</summary>
79+
80+
1 op = 1000 queries for point lookup, 100 queries for range scan and count.
81+
82+
| Type | Method | Mean | Error | StdDev | Allocated |
83+
|--------------- |------------------------- |-------------:|-------------:|-------------:|-----------:|
84+
| ReadBenchmark | DryDB_FindByKey | 29.00 us | 2.133 us | 1.269 us | - |
85+
| ReadBenchmark | RocksDB_FindByKey | 240.74 us | 42.123 us | 27.862 us | 40032 B |
86+
| ReadBenchmark | CsSqlite_FindByKey_Fair | 943.04 us | 152.115 us | 100.615 us | 48000 B |
87+
| ReadBenchmark | CsSqlite_FindByKey | 6,655.59 us | 557.215 us | 368.563 us | 48000 B |
88+
| RangeBenchmark | DryDB_GetRange | 140.02 us | 6.377 us | 4.218 us | - |
89+
| RangeBenchmark | CsSqlite_GetRange_Fair | 958.48 us | 124.718 us | 82.493 us | 480000 B |
90+
| RangeBenchmark | RocksDB_GetRange | 1,396.66 us | 157.288 us | 104.036 us | 726432 B |
91+
| RangeBenchmark | CsSqlite_GetRange | 1,743.15 us | 218.851 us | 144.757 us | 480000 B |
92+
| CountBenchmark | DryDB_CountRange | 1,524.56 us | 123.628 us | 81.772 us | - |
93+
| CountBenchmark | CsSqlite_CountRange | 10,796.05 us | 956.138 us | 632.426 us | - |
94+
| CountBenchmark | CsSqlite_CountRange_Fair | 11,715.51 us | 926.636 us | 551.426 us | - |
95+
| CountBenchmark | RocksDB_CountRange | 98,221.60 us | 5,595.564 us | 3,701.119 us | 25606432 B |
96+
97+
</details>
98+
99+
> [!NOTE]
100+
> DryDB returns values as zero-copy slices of cached pages, so read paths allocate no managed memory.
101+
> The RocksDB numbers go through the C# binding ([rocksdb-sharp](https://github.com/curiosity-ai/rocksdb-sharp)), whose iterator allocates a `byte[]` per key/value access — the count benchmark in particular is dominated by that binding overhead rather than the storage engine itself.
102+
103+
The benchmark source is in [sandbox/DryDB.Benchmark](sandbox/DryDB.Benchmark).
104+
47105
## Why read-only ?
48106

49107
## Installation
Lines changed: 22 additions & 0 deletions
Loading
Lines changed: 22 additions & 0 deletions
Loading
Lines changed: 22 additions & 0 deletions
Loading
Lines changed: 22 additions & 0 deletions
Loading
Lines changed: 21 additions & 0 deletions
Loading
Lines changed: 21 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)