CSharpDB vs SQLite: A Benchmark-Driven Guide

Need the full long-form article? The original markdown version is preserved as CSharpDB vs SQLite Benchmarking Source Reference.

Both CSharpDB and SQLite are excellent embedded databases for .NET, and "which one is faster?" is the wrong question. The right question is: for each workload, which API on which engine gets you the best number? Sometimes that's SQLite through ADO.NET. Sometimes it's CSharpDB through the direct engine API. Sometimes the gap between the two engines is smaller than the gap between a good API choice and a bad one on either side.

This post walks through four paired benchmarks on .NET 10 — bulk inserts, point lookups, concurrent writers, and EF Core — and shows the compilable code that produced each number. Everything in this post runs end-to-end with dotnet run -c Release.

The Setup

Hardware: Intel i9-11900K (16 logical cores), Windows 10.0.26300, .NET SDK 10.0.202, .NET runtime 10.0.6. The headline numbers in this post come from the v3.3.0 release-core run, captured with --release-core --repeat 3 --repro (priority=High, affinity=0xFF). The release guardrail comparison passed with PASS=185, WARN=0, SKIP=0, FAIL=0.

Both engines work against the same schema:

CREATE TABLE products (
    id       INTEGER PRIMARY KEY,
    name     TEXT NOT NULL,
    price    REAL NOT NULL,
    category TEXT
)

SQLite runs through Microsoft.Data.Sqlite 10.0.6 with journal_mode=WAL, synchronous=FULL, private cache, and pooling disabled — the standard durable local configuration. CSharpDB runs through three surfaces: CSharpDB.Data (ADO.NET), CSharpDB.Engine (direct engine API), and CSharpDB.EntityFrameworkCore. Every CSharpDB row below is durable and file-backed unless noted otherwise.

The full benchmark harness lives under tests/CSharpDB.Benchmarks/ in the repo; the headline comparison rows also appear in SQLITE_COMPARISON.md. Ratios below are CSharpDB throughput / SQLite throughput — values above 1.00× mean CSharpDB is faster. This is a same-machine local comparison, not a universal database ranking.

Let's walk through the four lessons, cheapest first.

Lesson 1: Scale Your Batch Size — InsertBatch Wins at Large Batches

The first thing you learn putting CSharpDB and SQLite head-to-head on durable inserts is that batch size is the biggest lever on either engine. Here is the v3.3.0 release-core comparison at four batch shapes, all with WAL+FULL durability and prepared reuse on both sides:

Workload CSharpDB SQLite WAL+FULL Ratio
Single-row auto-commit insert 279 ops/sec 282 ops/sec 0.99× (tied)
100 rows per transaction 26.7K rows/sec 25.5K rows/sec 1.05×
1,000 rows per InsertBatch 204K rows/sec 192K rows/sec 1.06×
10,000 rows per InsertBatch 798K rows/sec 540K rows/sec 1.48×

Single-row durable commits are a wash — both engines bottleneck on the fsync per commit. The CSharpDB lead grows with batch size because the bulk path ships a whole batch as one multi-row engine call, so per-row SQL parsing, plan lookup, and parameter binding all collapse into a single operation. Database.PrepareInsertBatch accepts a typed row buffer that is committed as a single atomic insert:

using CSharpDB.Engine;
using CSharpDB.Primitives;

await using var db = await Database.OpenAsync("bulk.cdb");

await db.ExecuteAsync("""
    CREATE TABLE IF NOT EXISTS products (
        id       INTEGER PRIMARY KEY,
        name     TEXT NOT NULL,
        price    REAL NOT NULL,
        category TEXT
    )
    """);

var batch = db.PrepareInsertBatch("products", initialCapacity: 10_000);
for (int i = 1; i <= 10_000; i++)
{
    batch.AddRow(
        DbValue.FromInteger(i),
        DbValue.FromText("Product"),
        DbValue.FromReal(9.99 + i),
        DbValue.FromText("Books"));
}
int inserted = await batch.ExecuteAsync();
Console.WriteLine($"Inserted {inserted} rows");

For comparison, here is the SQLite side of the same B10000 row — prepared statement, explicit transaction, parameter reuse — which is what the comparison benchmark uses as the SQLite baseline:

using Microsoft.Data.Sqlite;

await using var conn = new SqliteConnection("Data Source=bulk.db");
await conn.OpenAsync();

await using (var cmd = conn.CreateCommand())
{
    cmd.CommandText = """
        CREATE TABLE IF NOT EXISTS products (
            id       INTEGER PRIMARY KEY,
            name     TEXT NOT NULL,
            price    REAL NOT NULL,
            category TEXT
        )
        """;
    await cmd.ExecuteNonQueryAsync();
}

await using var tx = (SqliteTransaction)await conn.BeginTransactionAsync();
await using var insert = conn.CreateCommand();
insert.Transaction = tx;
insert.CommandText =
    "INSERT INTO products (id, name, price, category) VALUES (@id, @name, @price, @cat)";
var id    = insert.CreateParameter(); id.ParameterName    = "@id";
var name  = insert.CreateParameter(); name.ParameterName  = "@name";
var price = insert.CreateParameter(); price.ParameterName = "@price";
var cat   = insert.CreateParameter(); cat.ParameterName   = "@cat";
insert.Parameters.Add(id);
insert.Parameters.Add(name);
insert.Parameters.Add(price);
insert.Parameters.Add(cat);
insert.Prepare();

for (int i = 1; i <= 10_000; i++)
{
    id.Value    = i;
    name.Value  = "Product";
    price.Value = 9.99 + i;
    cat.Value   = "Books";
    await insert.ExecuteNonQueryAsync();
}
await tx.CommitAsync();

That is SQLite at its best on .NET: prepared statement, explicit transaction, parameter reuse. CSharpDB ships the same 10,000 rows in roughly two-thirds the wall time with less ceremony — no parameter setup, no prepared statement, no manual transaction. The packages you'll need:

<ItemGroup>
    <PackageReference Include="CSharpDB" Version="3.3.0" />
    <PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.6" />
</ItemGroup>

The practical takeaway: on CSharpDB, InsertBatch is how you get the 1.48× lead — it is not an optimization, it is the bulk-load API. Reach for it any time you are inserting more than a handful of rows from known data.

Lesson 2: Reads Favour CSharpDB — and Scale Harder Under Concurrency

Single-key lookups are where CSharpDB's storage layer has the clearest advantage. The v3.3.0 release-core SQL point lookup row, warm single-connection primary-key lookup on both engines:

Workload CSharpDB SQLite WAL+FULL Ratio
SQL point lookup (warm, single connection) 1.33M ops/sec (0.75 μs P50) 138.3K ops/sec (5.8 μs P50) 9.63×
8-reader burst — COUNT(*) hot read 10.77M ops/sec 110.0K ops/sec 97.95×

Single-connection lookups: CSharpDB is about 10× faster. Move to an 8-reader burst against a tiny hot read shape and the gap opens to nearly 98× — because CSharpDB's snapshot readers share the warm page cache with no per-reader lock contention, while SQLite's readers each serialize at the WAL gate for consistency. (Caveat: the burst row is intentionally a tiny hot read. It is not a broad analytical-query comparison.)

Here is the direct engine API version of the single-lookup path:

using CSharpDB.Engine;

await using var db = await Database.OpenAsync("bulk.cdb");

async ValueTask<string?> GetProductName(long id)
{
    await using var result = await db.ExecuteAsync(
        $"SELECT name FROM products WHERE id = {id}");

    await foreach (var row in result.GetRowsAsync())
        return row[0].AsText;

    return null;
}

Console.WriteLine(await GetProductName(42));

The speed comes from a short-circuit in Database.ExecuteAsync that recognises primary-key lookups and goes straight to the B-tree without running the full planner. You don't need a prepared command, and you don't need a special API — the engine does it for you.

If you need parameterised queries against user input (the normal case), the ADO.NET provider still keeps most of that advantage:

using CSharpDB.Data;

await using var conn = new CSharpDbConnection("Data Source=bulk.cdb");
await conn.OpenAsync();

await using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT name FROM products WHERE id = @id";
cmd.Parameters.AddWithValue("@id", 42L);

await using var reader = await cmd.ExecuteReaderAsync();
string? name = await reader.ReadAsync() ? reader.GetString(0) : null;
Console.WriteLine(name);

The ADO.NET wrapper adds overhead per lookup for parameter binding, reader state, and DbCommand plumbing, but stays well inside the 10× engine-to-engine gap. If you serve a read-heavy API from an embedded database — especially one where multiple threads hit the same hot tables — this is where CSharpDB earns its keep: an order of magnitude faster on warm single lookups, and roughly two orders of magnitude faster on the 8-reader burst shape, without changing anything else about your code.

Lesson 3: Concurrent Writers Are CSharpDB's Territory

This is the workload where the architectures diverge most. SQLite is fundamentally single-writer — WAL mode lets readers run alongside a writer, but writers themselves must take the reserved lock one at a time. Eight concurrent tasks doing 1,250 disjoint inserts each still serialize at the write lock.

CSharpDB 3.x exposes ImplicitInsertExecutionMode.ConcurrentWriteTransactions, which routes shared auto-commit inserts through isolated WriteTransaction state. Combined with UseDurableGroupCommit, disjoint-key writers can overlap and coalesce their fsyncs. The practical shape we observe on the concurrent-writer benchmark suites is:

  • At 1 writer, SQLite is typically ahead — UseDurableGroupCommit adds a small batching window and WriteOptimizedPreset trades single-writer latency for multi-writer throughput, so the default preset is the right choice when you know your workload is single-threaded.
  • At 2 writers, the crossover point: CSharpDB catches up and pulls slightly ahead.
  • At 4–8 writers with disjoint keys, SQLite scales linearly with writer count (because serialized writers queue at the lock), while CSharpDB stays roughly flat — so the gap opens up several× at 4 writers and keeps widening.

These numbers are workload-sensitive (key distribution, row size, disk) and move around with each release. Rerun the concurrent benchmark to get current figures for your machine:

dotnet run -c Release `
    --project .\tests\CSharpDB.Benchmarks\CSharpDB.Benchmarks.csproj `
    -- --concurrent-sqlite-capi-compare --repeat 3 --repro

Here is the CSharpDB side of the multi-writer scenario end-to-end:

using CSharpDB.Engine;
using CSharpDB.Primitives;

const int TotalRows = 10_000;
const int Writers   = 8;

var options = new DatabaseOptions
{
    ImplicitInsertExecutionMode = ImplicitInsertExecutionMode.ConcurrentWriteTransactions,
}.ConfigureStorageEngine(builder =>
{
    builder.UseWriteOptimizedPreset();
    builder.UseDurableGroupCommit(TimeSpan.FromMilliseconds(0.25));
});

await using var db = await Database.OpenAsync("ingest.cdb", options);

await db.ExecuteAsync("""
    CREATE TABLE IF NOT EXISTS products (
        id       INTEGER PRIMARY KEY,
        name     TEXT NOT NULL,
        price    REAL NOT NULL,
        category TEXT
    )
    """);

int rowsPerWriter = TotalRows / Writers;
Task[] tasks = Enumerable.Range(0, Writers)
    .Select(writerIndex => Task.Run(async () =>
    {
        int startId = writerIndex * rowsPerWriter + 1;

        // Each writer owns a disjoint primary-key range — no conflicts.
        var batch = db.PrepareInsertBatch("products", rowsPerWriter);
        for (int i = 0; i < rowsPerWriter; i++)
        {
            batch.AddRow(
                DbValue.FromInteger(startId + i),
                DbValue.FromText("Product"),
                DbValue.FromReal(9.99 + i),
                DbValue.FromText("Books"));
        }
        await batch.ExecuteAsync();
    }))
    .ToArray();

await Task.WhenAll(tasks);

Three things matter in that configuration:

  • ImplicitInsertExecutionMode.ConcurrentWriteTransactions lets auto-commit inserts run on isolated per-writer transaction state instead of serialising on the main write gate.
  • UseWriteOptimizedPreset() raises the checkpoint frame threshold and moves checkpoint work to a background slice, so the commit path stays tight.
  • UseDurableGroupCommit(0.25 ms) gives the flush leader a short window to coalesce overlapping commits into a single OS fsync. On a workload with eight concurrent writers, that turns eight flushes into closer to one or two.

The equivalent SQLite code for comparison is the standard connection-per-worker pattern with WAL mode enabled:

using Microsoft.Data.Sqlite;

const int TotalRows = 10_000;
const int Writers   = 8;
const string Cs     = "Data Source=ingest.db;Pooling=True";

// WAL + NORMAL sync is the most concurrency-friendly SQLite configuration.
await using (var init = new SqliteConnection(Cs))
{
    await init.OpenAsync();
    await using var cmd = init.CreateCommand();
    cmd.CommandText = @"
        PRAGMA journal_mode = WAL;
        PRAGMA synchronous  = NORMAL;
        PRAGMA busy_timeout = 30000;
        CREATE TABLE IF NOT EXISTS products (
            id       INTEGER PRIMARY KEY,
            name     TEXT NOT NULL,
            price    REAL NOT NULL,
            category TEXT
        );";
    await cmd.ExecuteNonQueryAsync();
}

int rowsPerWriter = TotalRows / Writers;
Task[] tasks = Enumerable.Range(0, Writers)
    .Select(writerIndex => Task.Run(async () =>
    {
        int startId = writerIndex * rowsPerWriter + 1;

        await using var conn = new SqliteConnection(Cs);
        await conn.OpenAsync();

        await using var tx = (SqliteTransaction)await conn.BeginTransactionAsync();
        await using var cmd = conn.CreateCommand();
        cmd.Transaction = tx;
        cmd.CommandText =
            "INSERT INTO products (id, name, price, category) VALUES (@id, @name, @price, @cat)";
        var id    = cmd.CreateParameter(); id.ParameterName    = "@id";
        var name  = cmd.CreateParameter(); name.ParameterName  = "@name";
        var price = cmd.CreateParameter(); price.ParameterName = "@price";
        var cat   = cmd.CreateParameter(); cat.ParameterName   = "@cat";
        cmd.Parameters.Add(id);
        cmd.Parameters.Add(name);
        cmd.Parameters.Add(price);
        cmd.Parameters.Add(cat);
        cmd.Prepare();

        for (int i = 0; i < rowsPerWriter; i++)
        {
            id.Value    = startId + i;
            name.Value  = "Product";
            price.Value = 9.99 + i;
            cat.Value   = "Books";
            await cmd.ExecuteNonQueryAsync();
        }
        await tx.CommitAsync();
    }))
    .ToArray();

await Task.WhenAll(tasks);

A note on interpreting the win: it is for disjoint-key inserts — each writer owns a slice of the primary-key space. A workload where every writer is hammering the same monotonic right edge of the index (typical for a shared identity column with no partitioning) will not see the same fan-in. v3.3.0 shipped hot-right-edge recovery work, but this shape is still the most constrained multi-writer pattern on either engine.

Lesson 4: Storage Presets Match the Workload, Not the Other Way Around

CSharpDB exposes storage presets on the StorageEngineOptionsBuilder that cover the common tuning axes. In v3.3.0, the same presets are also surfaced through the ADO.NET connection string (Storage Preset=WriteOptimized) and through the EF Core provider (UseStoragePreset(WriteOptimized)), so embedded users don't have to reach for the engine API to pick a tuning profile:

Preset When to use it
Default Single writer, no contention, short-lived processes
UseWriteOptimizedPreset Sustained write load, background checkpoints, multi-writer disjoint keys
UseLowLatencyDurableWritePreset Deferred planner stats, commit-path focus
WriteOptimized + DurableGroupCommit(...) Several concurrent writers, willing to trade some latency for flush sharing

The single-writer default is already well-tuned — write presets earn their keep under contention or on long-lived write-heavy services where the checkpoint pattern matters, not on small single-threaded loads. Measure your workload before adopting a preset; don't flip it on because it sounds faster.

Switching presets is a one-line change through the engine API:

using CSharpDB.Engine;

var options = new DatabaseOptions()
    .ConfigureStorageEngine(builder => builder.UseWriteOptimizedPreset());

await using var db = await Database.OpenAsync("app.cdb", options);

Or through the ADO.NET connection string — the v3.3.0 storage-tuning additions make this the easiest path for embedded users:

using CSharpDB.Data;

var cs = "Data Source=app.cdb;Storage Preset=WriteOptimized;Embedded Open Mode=Direct";
await using var conn = new CSharpDbConnection(cs);
await conn.OpenAsync();

Read-side presets like UseDirectLookupOptimizedPreset, UseHybridFileCachePreset, and UseDirectColdFileLookupPreset follow the same pattern.

Matching Durability Is Non-Negotiable for Fair Comparisons

If you're running your own CSharpDB-vs-SQLite numbers, the single biggest trap is comparing engines under mismatched durability settings. SQLite's default is journal_mode=DELETE with synchronous=FULL — an fsync per commit, full rollback journal. That's strict, and slow. CSharpDB's defaults are WAL-based with different fsync semantics.

The benchmarks in this post use two durability contracts, each matched on both sides:

  • Headline durable comparison (Lessons 1 and 2): SQLite runs with PRAGMA journal_mode=WAL; synchronous=FULL — the strict "every commit fsyncs" configuration. CSharpDB runs in its default durable file-backed mode. This is the comparison that appears in SQLITE_COMPARISON.md.
  • Concurrent writer comparison (Lesson 3): SQLite uses WAL; synchronous=NORMAL — the industry-standard "fast but crash-safe" configuration. This is a more generous setting for SQLite than the headline comparison uses, precisely because we want to show the contention behaviour even when SQLite is tuned for speed.

If you see any CSharpDB benchmark that beats SQLite by 100× on a single-row insert, check the durability settings before believing it. A 1.48× lead on durable B10000 bulk with both sides on WAL+FULL is a real and fair comparison; the same workload can look like a 10× gap if the SQLite side is accidentally running with synchronous=OFF.

A practical checklist when writing comparison code:

  • SQLite: set journal_mode (WAL for concurrent readers, MEMORY/OFF for pure speed) and synchronous (FULL for durable, NORMAL for WAL-safe, OFF for unsafe) explicitly in the connection initialisation.
  • CSharpDB: be explicit about the preset. The engine's default uses WAL and durable fsync on commit; the Buffered durability mode and the LowLatencyDurableWrite preset weaken specific guarantees — document the trade when you use them.
  • Both: use the same row count, same schema, same iteration harness, and the same batch size. BenchmarkDotNet's paired baseline/candidate pattern makes this easy.

EF Core: Parity on Writes, Slower on Hot Reads

The CSharpDB EF Core provider is newer than the ADO.NET one, and that shows up in the numbers. For bulk insert through DbContext.SaveChanges, CSharpDB and SQLite are roughly tied on throughput, with CSharpDB allocating noticeably less per row. For DbSet.Find by primary key on a warm table, the picture flips: the CSharpDB EF Core Find path is currently slower than SQLite's EF Core Find path, even though the underlying engine is about 10× faster than SQLite on the same lookup (Lesson 2). The gap lives at the EF Core provider layer — specifically in how EF Core's materialization and change tracking interact with the CSharpDB provider's current plan caching. v3.3.0 tightened provider option validation and connection setup, but did not retarget the Find hot path; that is follow-up work.

The practical workaround today is to drop a layer for hot lookups. Either use the ADO.NET provider inside your service:

using CSharpDB.Data;

public sealed class ProductReader(string connectionString)
{
    public async Task<string?> GetNameAsync(long id)
    {
        await using var conn = new CSharpDbConnection(connectionString);
        await conn.OpenAsync();

        await using var cmd = conn.CreateCommand();
        cmd.CommandText = "SELECT name FROM products WHERE id = @id";
        cmd.Parameters.AddWithValue("@id", id);

        await using var reader = await cmd.ExecuteReaderAsync();
        return await reader.ReadAsync() ? reader.GetString(0) : null;
    }
}

Or, if you own the context lifecycle, resolve the underlying CSharpDbConnection through the EF Core context and reuse it for the fast path:

using System.Data.Common;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

using var ctx = new AppDbContext(options);

// Writes through EF Core: tracked entities, migrations, change tracking.
ctx.Products.Add(new Product { Id = 1, Name = "Widget", Price = 9.99 });
await ctx.SaveChangesAsync();

// Reads through the same physical connection, bypassing the EF provider.
DbConnection conn = ctx.Database.GetDbConnection();
if (conn.State != System.Data.ConnectionState.Open)
    await conn.OpenAsync();

await using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT name FROM products WHERE id = @id";
var idParam = cmd.CreateParameter();
idParam.ParameterName = "@id";
idParam.Value = 1L;
cmd.Parameters.Add(idParam);

await using var reader = await cmd.ExecuteReaderAsync();
string? name = await reader.ReadAsync() ? reader.GetString(0) : null;

That keeps the write surface ergonomic while routing hot reads through the fast path the engine is already capable of serving.

A Decision Guide

Putting the four lessons together, here's the short decision tree:

If your workload is… Pick… Why
Single-threaded bulk load (large batches) CSharpDB InsertBatch at B10000 1.48× faster than SQLite prepared-bulk on WAL+FULL
Single-threaded bulk load (small/medium batches) Either engine, batch size 1,000–10,000 Tied to 1.06× in CSharpDB's favour; pick for API ergonomics
Read-heavy, point-lookup-dominated CSharpDB engine API or ADO.NET ~10× faster on warm single lookups; ~98× on 8-reader bursts
Concurrent writers, disjoint keys CSharpDB with ConcurrentWriteTransactions + DurableGroupCommit Approximately flat scaling vs SQLite's linear writer-count penalty
Concurrent writers, shared hot key range Either engine, expect linear scaling Fan-in is fundamentally limited in both
EF Core writes Either engine; CSharpDB has lower allocations Roughly tied at 10k-row SaveChanges
EF Core hot reads (v3.3.0) SQLite through EF Core, or CSharpDB with an ADO.NET escape hatch CSharpDB EF Core Find path is not yet tuned
Mixed workload with unknown ratios CSharpDB with defaults Read advantage dominates most real app profiles

Reproducing the Numbers

The full benchmark suite that produced the numbers in this post lives under tests/CSharpDB.Benchmarks/ in the CSharpDB repo. It references CSharpDB from source so you can point it at any commit, and it runs both engines through paired benchmark classes with matching call patterns. To reproduce the headline comparison rows in this post:

dotnet run -c Release `
    --project .\tests\CSharpDB.Benchmarks\CSharpDB.Benchmarks.csproj `
    -- --release-core --repeat 3 --repro

Or target individual comparison surfaces:

# Headline SQLite comparison (durable batched inserts, point lookup, reader burst)
dotnet run -c Release --project .\tests\CSharpDB.Benchmarks\CSharpDB.Benchmarks.csproj `
    -- --sqlite-compare --repeat 3 --repro

# Engine-to-engine concurrent writers (CSharpDB vs SQLite C API)
dotnet run -c Release --project .\tests\CSharpDB.Benchmarks\CSharpDB.Benchmarks.csproj `
    -- --concurrent-sqlite-capi-compare --repeat 3 --repro

# ADO.NET provider comparison
dotnet run -c Release --project .\tests\CSharpDB.Benchmarks\CSharpDB.Benchmarks.csproj `
    -- --concurrent-adonet-compare --repeat 3 --repro

# EF Core provider comparison
dotnet run -c Release --project .\tests\CSharpDB.Benchmarks\CSharpDB.Benchmarks.csproj `
    -- --efcore-compare --repeat 3 --repro

You should see the same directional shape on any modern .NET 10 machine — CSharpDB ahead on durable bulk batches, well ahead on point lookups and concurrent reads, and ahead on concurrent-writer disjoint-key workloads at 2+ writers. Absolute numbers will vary with CPU, disk, and filesystem. The canonical comparison rules and source benchmark files are documented in tests/CSharpDB.Benchmarks/SQLITE_COMPARISON.md.

Closing Thoughts

SQLite is a legendary piece of software, and the right default for a large class of .NET applications. CSharpDB isn't trying to replace it — it's a pure-C# embedded engine with a different set of trade-offs, and those trade-offs show up clearly when you put both engines under the same benchmark harness.

The real signal from this exercise isn't the horse-race: it's that the API you pick inside each engine matters at least as much as the engine itself. InsertBatch is what takes CSharpDB from "tied with SQLite" to "1.48× ahead" on durable bulk. Dropping from EF Core to ADO.NET is what recovers the engine's 10× lookup advantage. Matching durability is what keeps an apples-to-apples comparison from silently becoming apples-to-oranges. Those levers exist on both engines, and pulling them is usually cheaper than switching engines.

If your workload is a good fit for CSharpDB's sweet spots — concurrent disjoint-key writes, hot point-lookups, large durable batches, or an engine API that gets out of your way — the numbers will reward you. If it isn't, the same benchmark harness will tell you that too.