Skip to content

Add fetch-throughput A/B ODBC benchmark harness (mssql-odbc vs msodbcsql18) - #190

Draft
Saurabh Singh (saurabh500) wants to merge 2 commits into
mainfrom
dev/saurabh/fetch-ab-bench-harness
Draft

Add fetch-throughput A/B ODBC benchmark harness (mssql-odbc vs msodbcsql18)#190
Saurabh Singh (saurabh500) wants to merge 2 commits into
mainfrom
dev/saurabh/fetch-ab-bench-harness

Conversation

@saurabh500

@saurabh500 Saurabh Singh (saurabh500) commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

What

Adds a standalone C/C++ ODBC benchmark (mssql-odbc/tests/e2e/bench/fetch_bench.cpp + run_bench.ps1) that times the fetch / row-throughput path — the SQLFetch + SQLGetData drain over a large result set — and runs an A/B between the native ODBC Driver 18 for SQL Server and the Rust mssql-odbc dev driver in a single invocation.

This establishes the BEFORE baseline that the sans-I/O core work is meant to beat — it is not a final verdict on the Rust driver. It is independent of the in-flight sans-I/O refactor — this branch is off main and is not stacked on #186.

Scope note: benchmarks the FETCH/decode path only. It deliberately does not benchmark SQLExecDirect (closed as I/O-bound, #186). The query returns many rows so row decode dominates the timing.

Design

  • Reuses the e2e harness plumbing (ODBCTestConfig for server/uid/pwd/database, diagnostics helpers) but is a plain executable — a normal ctest run does not invoke it. New non-CTest fetch_bench CMake target.
  • Workload: SELECT TOP (N) of an int / bigint / varchar / nvarchar / float row set materialised server-side (a sys.all_objects self cross-join), so the client spends its time decoding rows. Every column is retrieved via SQLGetData as character data — the one column path both drivers share.
  • Driver-manager bypass (no admin, no registry): each leg LoadLibrarys the driver DLL directly and calls its exported SQLxxxW entry points as a tiny DM (--driver LABEL=dll:PATH). This runs the unregistered Rust dev driver with zero admin rights and keeps both legs on an identical, DM-free code path (a fairer compare). A bare-name leg routes through odbc32.dll (the real DM) instead. run_bench.ps1 takes either DLL path as a param (-RustDll / -NativeDll, or -Release) so the Rust leg can be re-pointed at future sync-core builds without editing the script.
  • Methodology: QueryPerformanceCounter around each execute+drain rep; ≥9 reps with the first (warmup) discarded; every retained rep printed (ms + rows/sec) plus the median; per-driver deltas <15% treated as noise. A checksum over all decoded values is summed so nothing is optimised away — it must stay 685409346 on both legs on every (re-)measure, so a decode regression can never silently change what's being compared.

Baseline results (Windows, live SQL Server on 127.0.0.1,1433, msodbcsql18 installed)

Build + connect succeeded for both drivers. Release-vs-release — both optimised builds, the only honest comparison. 200k rows, 9 reps, checksum 685409346 identical on both legs.

  • native = ODBC Driver 18 for SQL Server (C:\Windows\System32\msodbcsql18.dll)
  • rust = mssql-odbc, main @ async/block_on, RELEASE build (target\release\msodbcsql18.dll) — the BEFORE baseline the sans-I/O core is meant to beat

Raw per-rep (rep 1 = discarded warmup):

rep native ms native rows/s rust ms rust rows/s
1 (warmup) 613.78 325,849 663.56 301,403
2 364.99 547,955 665.43 300,556
3 353.63 565,559 670.39 298,332
4 356.56 560,921 663.45 301,452
5 319.18 626,611 659.44 303,288
6 357.88 558,850 675.09 296,258
7 327.77 610,182 676.70 295,553
8 322.22 620,694 671.39 297,888
9 339.09 589,811 683.77 292,494
median 346.36 577,430 670.89 298,110

A/B ratio (rust median / native median) = 1.94x. Across two independent release runs the Rust median was 670.89 ms / 672.20 ms (≈0.2% apart, reps tight 659–684 ms); native floats more run-to-run (321–346 ms median), so the ratio lands in ~1.9–2.1x (~2x).

Framing: unoptimized main (async/block_on) is ~2x slower on this fetch/decode path — the starting line, not the score. The sans-I/O sync core plus #186 batching/recycling are the intervention meant to close it.

Interpretation — the gap is addressable decode-CPU headroom, not async tax: the async reactor round-trip (~10 µs) is per network refill, not per row, so it can't account for a per-row delta. In release the Rust leg is ~3.4 µs/row vs native ~1.7 µs/row; that remaining ~1.7 µs/row scales with row count, which points at per-row decode CPU — exactly what the sync core + batching/recycling target.

⚠️ A debug Rust build measures ~12.6x, but that is dominated by debug-vs-release codegen (no inlining, overflow checks), not the driver path — apples-to-oranges. Always benchmark the Rust leg with cargo build --release (run_bench.ps1 -Release).

Notes

  • datetime2 is intentionally excluded from the column set: the Rust Phase-1 SQLGetData cannot yet convert it to text, and including it would make the two A/B legs diverge.
  • The Windows ODBC driver on this host will not resolve the literal localhost ("No such host is known"); run_bench.ps1 auto-rewrites localhost127.0.0.1.
  • The 7 pre-existing cert-fixture unit-test failures are unrelated to this change.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

…sql18)

Add a standalone C++ benchmark that times the SQLFetch + SQLGetData drain
over a large result set and runs an A/B between the native ODBC Driver 18
for SQL Server and the Rust mssql-odbc dev driver in one invocation. It
reuses the e2e harness config/diagnostics plumbing but is a plain executable
(not a CTest test). Each leg loads its driver DLL directly (its own tiny
driver manager) so the unregistered dev driver runs with no admin/registry
and both legs share an identical, DM-free code path. Reports raw per-rep
ms/rows-per-sec, per-driver medians, and the rust/native ratio; a checksum
over decoded values guards against elision and confirms both legs did equal
work. run_bench.ps1 builds the Rust driver + benchmark and runs both legs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3c262b9-1962-4d93-b08e-e3bc84aa4ac0
A debug mssql-odbc build is 10-30x slower purely from codegen and swamps the
real driver-path cost; document that the Rust leg must be a --release build so
the A/B baseline reflects the driver, not debug overhead.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a3c262b9-1962-4d93-b08e-e3bc84aa4ac0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant