Skip to content

Commit 2f06db7

Browse files
measure(harness): the PK comparison publishes BOTH postures; at-rest tax attributed (GCM explains ~15%)
Steps 1+2 of the agreed sequence, executed. Step 1 - honest publication (plan section 0.1-6): the --pk runner gained a third SharpCoreDB arm (fixed-width, at-rest default, read from a fresh DatabaseConfig so it cannot drift), every label carries the posture, and it prints per-posture gaps vs SQLite plus a per-operation at-rest tax. Result: arm INSERT READ UPDATE DELETE FW plaintext 120,832 114,747 238,446 162,449 FW at-rest (default) 63,532 75,875 147,458 147,964 SQLite 180,200 101,020 268,956 366,695 Gaps vs SQLite in the shipping posture: UPDATE 1.8x, DELETE 2.5x, INSERT 2.8x, READ 1.33x - not the ~1.1x the raw-only report implied. Step 2 - where the tax goes. At-rest tax per operation: INSERT 1.90x, READ 1.51x, UPDATE 1.62x, DELETE 1.10x (spread across the path, not concentrated on one operation). Crypto split at 120-byte payloads, 200K iters: Encrypt as shipped 1.34us | Decrypt 1.01us | fresh AesGcm ctor 0.69us | OS CSPRNG nonce 0.10us raw AEAD with a pooled cipher + counter nonce 0.27us (setup share of Encrypt: 59%) So per-call cipher setup is 59% of Encrypt and pooling it is ~5x on that call - but it is only ~15% of the UPDATE tax and less of the INSERT tax: one 1.34us Encrypt cannot explain +7.4us/row. The index build is not the cause either: with hash indexes off the at-rest INSERT tax is still 1.76x (103,754 -> 59,083 ops/s). Also found, and posture-independent: hash indexes cost 28% of INSERT in both postures (74,605 -> 103,754 plaintext), which makes them a lever of their own. Open, recorded in the plan: ~6us/row of the at-rest INSERT cost is still unattributed; the next probe is per-row allocation/GC counts plus the pooled-cipher prototype IN THE PRODUCT, since a standalone Encrypt timing may not survive the insert path's allocation pressure. Gate: SharpCoreDB.slnx 0 errors (harness included); SharpCoreDB.Tests 1848 / 0 failed / 16 skipped. No product code changed - measurement and documentation only; all temp probes removed.
1 parent dea9bbb commit 2f06db7

2 files changed

Lines changed: 69 additions & 12 deletions

File tree

‎docs/performance/INSERT_UPDATE_PERFORMANCE_PLAN.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,41 @@ arena and points at the per-record GCM work itself. §0.1-6 already requires eve
843843
attributing the GCM cost (per-record nonce generation / cipher instance vs raw AES throughput), is the next
844844
measurement task.
845845

846+
**Follow-up (2026-09-14, executed): both postures are now published, and the GCM hypothesis is partly
847+
disproven.** The `--pk` runner gained a third SharpCoreDB arm (`fixed-width, at-rest default`), its labels
848+
carry the posture, and it prints the gap per posture plus a per-operation at-rest tax:
849+
850+
| arm | INSERT | READ | UPDATE | DELETE |
851+
|---|---:|---:|---:|---:|
852+
| SharpCoreDB FW, plaintext | 120,832 | 114,747 | 238,446 | 162,449 |
853+
| **SharpCoreDB FW, at-rest (product default)** | **63,532** | **75,875** | **147,458** | **147,964** |
854+
| SQLite | 180,200 | 101,020 | 268,956 | 366,695 |
855+
856+
Gaps vs SQLite in the shipping posture: **UPDATE 1.8×, DELETE 2.5×, INSERT 2.8×, READ 1.33×**. At-rest tax
857+
(same arm, same knobs): **INSERT 1.90×, READ 1.51×, UPDATE 1.62×, DELETE 1.10×** — i.e. the tax is spread
858+
across the whole write path, not concentrated on one operation.
859+
860+
Crypto-level split (120-byte payload, 200K iterations):
861+
862+
| part | µs/call |
863+
|---|---:|
864+
| `Encrypt` as shipped | 1.34 |
865+
| `Decrypt` as shipped | 1.01 |
866+
| fresh `AesGcm` per call (inside both) | 0.69 |
867+
| OS CSPRNG nonce per call | 0.10 |
868+
| **raw AEAD, pooled cipher + counter nonce** | **0.27** |
869+
870+
Per-call cipher setup is ~59% of an `Encrypt` call, so pooling the cipher and replacing the per-record CSPRNG
871+
nonce is worth ~5× on that call — but it is only ~15% of the measured UPDATE tax and less of the INSERT tax:
872+
one 1.34 µs `Encrypt` cannot explain +7.4 µs/row. The remainder is **not** the index build either: with hash
873+
indexes off the at-rest INSERT tax is still 1.76× (103,754 → 59,083 ops/s). Separately, hash indexes cost
874+
**28% of INSERT in both postures** (74,605 → 103,754 plaintext), which makes them a posture-independent lever
875+
of their own.
876+
877+
**Open:** ~6 µs/row of the at-rest INSERT cost is still unattributed. Next probe: per-row allocation/GC counts
878+
and the pooled-cipher prototype **in the product** (not only in a micro-benchmark), because the standalone
879+
`Encrypt` timing may not survive the insert path's allocation pressure.
880+
846881
---
847882

848883
## 5. Phase 3 — INSERT: from competitive to ahead

‎tests/benchmarks/SharpCoreDB.Benchmarks.Comparative/Program.cs‎

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -971,7 +971,9 @@ static BenchmarkResult RunSharpCoreDBPk(
971971
SharpCoreDB.Interfaces.StorageEngineType engineType,
972972
bool fixedWidth = false,
973973
bool useDefaultConfig = false,
974-
string? defaultVariant = null)
974+
string? defaultVariant = null,
975+
bool noEncrypt = true,
976+
bool? atRestRecords = null)
975977
{
976978
var dbPath = Path.Combine(Path.GetTempPath(), $"bench-sharpcoredb-pk-{Guid.NewGuid()}");
977979
var result = new BenchmarkResult();
@@ -992,7 +994,7 @@ static BenchmarkResult RunSharpCoreDBPk(
992994
}
993995
else
994996
{
995-
config = BuildConfig(engineType, fixedWidth);
997+
config = BuildConfig(engineType, fixedWidth, noEncrypt, atRestRecords);
996998
}
997999

9981000
using var db = (SharpCoreDB.Database)factory.Create(
@@ -1143,29 +1145,49 @@ static void RunPkComparison(SharpCoreDB.Interfaces.StorageEngineType engineType)
11431145
Console.WriteLine($"Engine: {engineLabel}");
11441146
Console.WriteLine("(fair-PK arms run 3x; median time per phase is reported)");
11451147

1146-
Console.WriteLine("━━━ SharpCoreDB (SQL, PK, legacy variable-length) ━━━");
1148+
Console.WriteLine("━━━ SharpCoreDB (SQL, PK, legacy variable-length, plaintext) ━━━");
11471149
var scdb = RunPkMedian(() => RunSharpCoreDBPk(engineType));
11481150
Console.WriteLine();
11491151

1150-
Console.WriteLine("━━━ SharpCoreDB (SQL, PK, fixed-width) ━━━");
1152+
Console.WriteLine("━━━ SharpCoreDB (SQL, PK, fixed-width, plaintext) ━━━");
11511153
var scdbFw = RunPkMedian(() => RunSharpCoreDBPk(engineType, fixedWidth: true));
11521154
Console.WriteLine();
11531155

1156+
// §0.1-6: every target table must carry the encryption posture beside the plaintext number. The
1157+
// arms above are NoEncryptMode=true (BuildConfig's default); this arm is the PRODUCT default
1158+
// posture (at-rest records on, read from a fresh DatabaseConfig so it cannot drift).
1159+
Console.WriteLine("━━━ SharpCoreDB (SQL, PK, fixed-width, at-rest default) ━━━");
1160+
var scdbFwAtRest = RunPkMedian(() => RunSharpCoreDBPk(engineType, fixedWidth: true, noEncrypt: false));
1161+
Console.WriteLine();
1162+
11541163
Console.WriteLine("━━━ SQLite (reference) ━━━");
11551164
var sqlite = RunPkMedian(() => RunSQLite());
11561165
Console.WriteLine();
11571166

1158-
Console.WriteLine("║ Database │ INSERT │ READ │ UPDATE │ DELETE ║");
1159-
Console.WriteLine($"║ SharpCoreDB │ {scdb.InsertOpsPerSec,10:N0} │ {scdb.ReadOpsPerSec,8:N0} │ {scdb.UpdateOpsPerSec,8:N0} │ {scdb.DeleteOpsPerSec,8:N0} ║");
1160-
Console.WriteLine($"║ SharpCoreDB FW│ {scdbFw.InsertOpsPerSec,10:N0} │ {scdbFw.ReadOpsPerSec,8:N0} │ {scdbFw.UpdateOpsPerSec,8:N0} │ {scdbFw.DeleteOpsPerSec,8:N0} ║");
1161-
Console.WriteLine($"║ SQLite │ {sqlite.InsertOpsPerSec,10:N0} │ {sqlite.ReadOpsPerSec,8:N0} │ {sqlite.UpdateOpsPerSec,8:N0} │ {sqlite.DeleteOpsPerSec,8:N0} ║");
1162-
Console.WriteLine($"\n UPDATE gap: SQLite vs legacy {sqlite.UpdateOpsPerSec / (double)scdb.UpdateOpsPerSec:F1}x vs fixed-width {sqlite.UpdateOpsPerSec / (double)scdbFw.UpdateOpsPerSec:F1}x");
1163-
Console.WriteLine($" DELETE gap: SQLite vs legacy {sqlite.DeleteOpsPerSec / (double)scdb.DeleteOpsPerSec:F1}x vs fixed-width {sqlite.DeleteOpsPerSec / (double)scdbFw.DeleteOpsPerSec:F1}x");
1167+
Console.WriteLine("║ Database │ INSERT │ READ │ UPDATE │ DELETE ║");
1168+
Console.WriteLine($"║ SharpCoreDB legacy plaintext│ {scdb.InsertOpsPerSec,10:N0} │ {scdb.ReadOpsPerSec,8:N0} │ {scdb.UpdateOpsPerSec,8:N0} │ {scdb.DeleteOpsPerSec,8:N0} ║");
1169+
Console.WriteLine($"║ SharpCoreDB FW plaintext│ {scdbFw.InsertOpsPerSec,10:N0} │ {scdbFw.ReadOpsPerSec,8:N0} │ {scdbFw.UpdateOpsPerSec,8:N0} │ {scdbFw.DeleteOpsPerSec,8:N0} ║");
1170+
Console.WriteLine($"║ SharpCoreDB FW at-rest │ {scdbFwAtRest.InsertOpsPerSec,10:N0} │ {scdbFwAtRest.ReadOpsPerSec,8:N0} │ {scdbFwAtRest.UpdateOpsPerSec,8:N0} │ {scdbFwAtRest.DeleteOpsPerSec,8:N0} ║");
1171+
Console.WriteLine($"║ SQLite │ {sqlite.InsertOpsPerSec,10:N0} │ {sqlite.ReadOpsPerSec,8:N0} │ {sqlite.UpdateOpsPerSec,8:N0} │ {sqlite.DeleteOpsPerSec,8:N0} ║");
1172+
Console.WriteLine($"\n UPDATE gap vs SQLite: legacy {sqlite.UpdateOpsPerSec / (double)scdb.UpdateOpsPerSec:F1}x "
1173+
+ $"fixed-width {sqlite.UpdateOpsPerSec / (double)scdbFw.UpdateOpsPerSec:F1}x "
1174+
+ $"fixed-width at-rest {sqlite.UpdateOpsPerSec / (double)scdbFwAtRest.UpdateOpsPerSec:F1}x");
1175+
Console.WriteLine($" DELETE gap vs SQLite: legacy {sqlite.DeleteOpsPerSec / (double)scdb.DeleteOpsPerSec:F1}x "
1176+
+ $"fixed-width {sqlite.DeleteOpsPerSec / (double)scdbFw.DeleteOpsPerSec:F1}x "
1177+
+ $"fixed-width at-rest {sqlite.DeleteOpsPerSec / (double)scdbFwAtRest.DeleteOpsPerSec:F1}x");
1178+
Console.WriteLine($" INSERT gap vs SQLite: legacy {sqlite.InsertOpsPerSec / (double)scdb.InsertOpsPerSec:F1}x "
1179+
+ $"fixed-width {sqlite.InsertOpsPerSec / (double)scdbFw.InsertOpsPerSec:F1}x "
1180+
+ $"fixed-width at-rest {sqlite.InsertOpsPerSec / (double)scdbFwAtRest.InsertOpsPerSec:F1}x");
1181+
Console.WriteLine($" At-rest tax (same arm shape): INSERT {scdbFw.InsertOpsPerSec / (double)scdbFwAtRest.InsertOpsPerSec:F2}x "
1182+
+ $"READ {scdbFw.ReadOpsPerSec / (double)scdbFwAtRest.ReadOpsPerSec:F2}x "
1183+
+ $"UPDATE {scdbFw.UpdateOpsPerSec / (double)scdbFwAtRest.UpdateOpsPerSec:F2}x "
1184+
+ $"DELETE {scdbFw.DeleteOpsPerSec / (double)scdbFwAtRest.DeleteOpsPerSec:F2}x");
11641185

11651186
var results = new Dictionary<string, BenchmarkResult>
11661187
{
1167-
["SharpCoreDB (SQL, PK, legacy)"] = scdb,
1168-
["SharpCoreDB (SQL, PK, fixed-width)"] = scdbFw,
1188+
["SharpCoreDB (SQL, PK, legacy, plaintext)"] = scdb,
1189+
["SharpCoreDB (SQL, PK, fixed-width, plaintext)"] = scdbFw,
1190+
["SharpCoreDB (SQL, PK, fixed-width, at-rest default)"] = scdbFwAtRest,
11691191
["SQLite"] = sqlite,
11701192
};
11711193

0 commit comments

Comments
 (0)