Benchmarks

The numbers on this page are reference figures from a controlled BenchmarkDotNet run. Real-world performance depends on record size, schema complexity, serialization choice, index configuration, storage media, and hardware — treat these as relative indicators.

Test environment

ItemValue
CPUIntel Core i7-9750H, 2.60 GHz, 6 physical / 12 logical cores
OSWindows 11 25H2 (10.0.26200.8457)
.NET runtime.NET 9.0.16, X64 RyuJIT AVX2
BenchmarkDotNetv0.15.8
Benchmark configInvocationCount=1, UnrollFactor=1
MemDb version6.1.0

Record schema

All benchmarks use the same record type:

public class BenchmarkRecord
{
    public long Id { get; set; }
    public string Name { get; set; }        // ~16 chars
    public string Category { get; set; }    // ~8 chars
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
    public decimal Amount { get; set; }
    public int Count { get; set; }
    public bool IsActive { get; set; }
}

The custom serializer writes a compact binary format using BinaryWriter (fixed-width fields; strings length-prefixed). The custom cloner copies all eight properties by value: the six value-type fields are bit-copied, and the two string fields are reference-copied — no new heap allocation occurs during cloning.

Workload

Benchmark groupOperations per invocationPre-seeded dataset
Insert250,000250,000 records
Update, delete2,500100,000 records
Find2,500250,000 records
Find (large volume)2,5003,000,000 records

Scenarios

Four configurations are benchmarked to isolate the impact of each optimization. Each row’s name shows what it builds on — note that scenarios 3 and 4 both branch from scenario 2, not from each other.

All four scenarios use durable storage: every insert and update is flushed to disk. MemDb also supports ephemeral-only modes that skip persistence entirely; write throughput in those configurations will be higher.

#ScenarioSerializerClonerIndex
1DefaultsJSON (built-in)JSON (round trip)None
2#1 + custom clone + serializeIMemDbSerializer<T>IMemDbCloner<T>None
3#2 + identity indexIMemDbSerializer<T>IMemDbCloner<T>Identity index
4#2 + applied indexIMemDbSerializer<T>IMemDbCloner<T>Applied index

Throughput

Single-threaded steady-state measurements.

Scenario 1 — Defaults

This is the zero-configuration baseline. JSON parse and serialize runs at every cache boundary — once on write, and again on every read through the round-trip cloner — and by-id operations also pay for a full sequential scan to locate the record.

OperationMeanOp/sAllocated
Insert (unencrypted)2.991 μs334,308738 B
Insert (encrypted)4.229 μs236,4791,345 B
Update by id236.329 μs4,231866 B
Delete by id33.510 μs29,841148 B
Find by id231.034 μs4,328472 B
Find by predicate (full scan)478.521 μs2,089448 B

Scenario 2 — Custom clone + serialize

Replacing the JSON serializer with a compact binary format and the JSON round-trip cloner with a property-copy implementation removes parse and serialize cost from every operation. Update, delete, and find by id are still scan-based — the gain comes from eliminating JSON overhead, not the lookup.

OperationMeanOp/sAllocated
Insert (unencrypted)0.8663 μs1,154,324770 B
Insert (encrypted)1.8730 μs533,9171,266 B
Update by id160.492 μs6,231898 B
Delete by id30.8396 μs32,425148 B
Find by id161.433 μs6,195168 B
Find by predicate (full scan)254.865 μs3,924144 B

Scenario 3 — Custom clone + serialize + identity index

The identity index’s impact on update, delete, and find by id is the most pronounced result on this page. Each of those operations must locate the record before acting on it, and the identity index reduces that step from a full sequential scan to a direct pointer lookup.

OperationMeanOp/sAllocated
Insert (unencrypted)0.8638 μs1,157,633871 B
Insert (encrypted)1.8898 μs529,1651,367 B
Update by id7.305 μs259,677834 B
Delete by id4.664 μs343,46484 B
Find by id0.048 μs20,800,550104 B
Find by id (large volume)0.0464 μs20,341,066104 B
Find by predicate (full scan)253.975 μs3,937144 B

Scenario 4 — Custom clone + serialize + applied index

The applied index does not accelerate update or delete by id — those remain scan-based — and adds maintenance overhead on every write, so insert, update, and delete throughput sits slightly below the no-index scenario.

OperationMeanOp/sAllocated
Insert (unencrypted)0.8599 μs1,162,863810 B
Insert (encrypted)1.8166 μs550,4811,306 B
Update by id (index assist)8.0782 μs123,7891370 B
Delete by id (index assist)20.5449 μs48,674620 B
Find by id (index assist)0.2061 μs4,852,571736 B
Find large volume (index assist)0.1492 μs6,701,457584 B
Find by predicate (full scan)269.765 μs1,543144 B

Memory footprint

Per-record overhead in the managed cache, measured as the delta in GC.GetTotalMemory(true) before and after inserting N records into an open database.

RecordsCache deltaPer record
250,00048.2 MB192.77 B
500,00096.4 MB192.77 B
1,000,000192.8 MB192.78 B

Per-record cost stabilizes at ~193 bytes for the benchmark record schema. The total process working set at 1,000,000 records was 263 MB, decomposing into ~193 MB of cached records plus ~70 MB of fixed overhead (CLR, JIT, MemDb internals, native buffers).

On-disk format

Pointer map overhead is exact and independent of record content:

ComponentSize
.map file header12 bytes
Per-record pointer39 bytes
.map total12 + (39 × record count) bytes

Data file size depends on the encoding and whether encryption is enabled:

EncodingAvg record size1M records on disk
JSON (default)182 B~182 MB
JSON + AES-256208 B~208 MB
Custom binary70 B~70 MB
Custom binary + AES-256208 B~208 MB

Cold start

Time from MemDb.Open to first successful read, measured with System.Diagnostics.Stopwatch.

RecordsDefaultsCustom clone + serializeCustom clone + serialize + AES-256
25,00073 ms21 ms66 ms
100,000231 ms110 ms291 ms
500,000870 ms379 ms958 ms
1,000,0001,799 ms786 ms1,869 ms
10,000,00018,391 ms8,105 ms19,976 ms

The custom binary serializer roughly halves cold start time compared to the default JSON deserializer. Adding AES-256 decryption brings cold start back in line with the JSON baseline — every record must be decrypted on load, which offsets the gains from the faster binary format.

Reproducing these numbers

The benchmark harness lives in the MemDb repository, under benchmark/HatTrick.MemDb.Benchmarks. It’s a BenchmarkDotNet console project that consumes the HatTrick.MemDb NuGet package — no local build of MemDb is required.

git clone https://github.com/HatTrickLabs/mem-db.git
cd mem-db/benchmark/HatTrick.MemDb.Benchmarks
dotnet run -c Release

The run executes, in order:

  • Memory footprint — cache delta at increasing record counts
  • Cold startMemDb.Open to first read, across record counts and serializer/encryption combinations
  • Throughput — the four scenarios above, via BenchmarkDotNet (DefaultsThroughput, CustomCloneAndSerializeThroughput, CustomCloneAndSerializeIdentityIndexThroughput, CustomCloneAndSerializeAppliedIndexThroughput)

BenchmarkDotNet writes per-scenario reports (Markdown, HTML, CSV) to BenchmarkDotNet.Artifacts/results/. A full run — including the 10,000,000-record cold start passes — takes a while; expect it to run for several minutes.