Skip to content

Phase M1: Client (1, 8, 64 workers)

Three client arms over a private VPC against a 3-node double-redundancy ssd-2 FoundationDB 7.3.77 cluster (17 server processes, 9 storage servers), in three closed-loop concurrency phases: M1w1 (1 worker), M1w8 (8 workers) and M1w64 (64 workers). This is phase M1 of fdbmn-20261020.

ArmTreeClient
masterfa8725f1dcf6ff3ac66825ab6e3e819efbc0abccPure-Go client (pkg/fdbgo)
old Go267a41434a81723b07e7150869d702636a1b91a9Pure-Go client, the final tree of the 2026-10-14 campaign
CGofa8725f1dcf6ff3ac66825ab6e3e819efbc0abccApple’s Go binding over hash-pinned libfdb_c 7.3.77, API 730

Design and scope

  • After M2, data distribution was re-enabled and the cluster held zero data movement for 121.474 s, with stable roles and CPU pins, before M1w1.
  • Phases ran in the preregistered order M1w1 → M1w64 → M1w8. M1w32 was dropped for budget before measurement and was never replaced with capacity data.
  • Each workload runs six blocks, one for each order of the three arms: 18 fresh processes per workload. Transaction counts are totals across workers:
    • M1w1: 3,000 measured + 200 warmup per run (18,000 measured per arm and workload).
    • M1w8: 12,000 + 800 per run (72,000 per arm and workload).
    • M1w64: 32,000 + 1,600 per run (192,000 per arm and workload).
  • Workload order at every worker count: grv, get100, get1000, range100, set100, batchset50, batchset50x1000. Per-process 180 s and per-workload 900 s caps.
  • Claim scope: each ratio is a within-worker-level paired contrast. Worker levels ran in a fixed sequence with different transaction counts, so they are not compared with each other.
  • Metric scopes: lifecycle CPU (E−A) is process user+system CPU from after binary identity hashing through measurement, finalizer drain, database close and native network stop, divided by measured transactions. Go bytes and mallocs per transaction count the Go heap only and exclude the native libfdb_c heap, so CGo’s small Go counts do not describe its memory use.
  • Harness: all three arms use the identical amended harness archive (d94b4c613 plus the #948 lifecycle-drain fix and matching validator). Workloads, timing windows, pacing, CPU accounting and estimators are those of the frozen harness.
  • Polling change: from M1w8 on, the controller reused one SSH connection and polled every 60 s instead of 10 s. M1w64 ran entirely under the original controller.

Exclusions

  • M1w1: 4 of 7 workloads clean. Before set100’s first arm, the client pre-arm gate measured 17.17% busy on one CPU, above the 15% limit (aggregate 4.01%, no steal). The arm was not launched, and the preregistered stop-on-failure policy ended the phase before batchset50 and batchset50x1000. All three write comparisons are N/A.
  • M1w64: 5 of 7 clean. After the first master batchset50 arm, the server’s transaction-log queue was 426,278,385 bytes, above the 100 MB gate. That arm completed 32,000 of 32,000 commits with no errors or retries and passed validation; the whole comparison is still excluded. batchset50x1000 was not reached.
  • M1w8: 5 of 7 clean. The log queue was 118,497,454 bytes after the first master batchset50 arm. batchset50x1000 was not reached.
  • No case was rerun or partially salvaged.

Results

Figure M1.1: p50 latency, master / old GoBelow 1.0× means master has lower p50.

At 8 and 64 workers master has higher p50 than old Go on every get, range and set workload, from 1.112 to 1.435; grv p50 is lower (0.941 and 0.923). At 1 worker master p50 is 1.049–1.091.

0.7×0.7×1.0×1.0×1.4×1.4×2.0×2.0×2.5×2.5×← Master lowerOld Go lower →M1w1 (1 worker, 4/7 clean)grvgrv: 1.049× [1.006, 1.080]1.049× [1.006, 1.080]get100get100: 1.088× [1.053, 1.128]1.088× [1.053, 1.128]get1000get1000: 1.091× [1.082, 1.099]1.091× [1.082, 1.099]range100range100: 1.080× [1.064, 1.092]1.080× [1.064, 1.092]set100N/A (excluded)batchset50N/A (excluded)batchset50x1000N/A (excluded)M1w8 (8 workers, 5/7 clean)grvgrv: 0.941× [0.930, 0.954]0.941× [0.930, 0.954]get100get100: 1.113× [1.097, 1.128]1.113× [1.097, 1.128]get1000get1000: 1.154× [1.139, 1.172]1.154× [1.139, 1.172]range100range100: 1.112× [1.096, 1.129]1.112× [1.096, 1.129]set100set100: 1.137× [1.099, 1.168]1.137× [1.099, 1.168]batchset50N/A (excluded)batchset50x1000N/A (excluded)M1w64 (64 workers, 5/7 clean)grvgrv: 0.923× [0.907, 0.938]0.923× [0.907, 0.938]get100get100: 1.348× [1.308, 1.395]1.348× [1.308, 1.395]get1000get1000: 1.298× [1.254, 1.331]1.298× [1.254, 1.331]range100range100: 1.435× [1.099, 2.378]1.435× [1.099, 2.378]set100set100: 1.283× [1.273, 1.294]1.283× [1.273, 1.294]batchset50N/A (excluded)batchset50x1000N/A (excluded)
95% CI below 1.0× 95% CI includes 1.0× (inconclusive) 95% CI above 1.0×
Figure M1.2: p50 latency, master / CGoBelow 1.0× means master has lower p50 than libfdb_c 7.3.77.

Master p50 is below CGo at 1 worker (0.636–0.697) and at 64 workers for get100, get1000 and set100 (0.770–0.934). At 8 workers the get1000 and set100 intervals include 1.0, and grv is above it (1.025).

0.5×0.5×0.7×0.7×1.0×1.0×1.4×1.4×2.0×2.0×← Master lowerCGo lower →M1w1 (1 worker, 4/7 clean)grvgrv: 0.636× [0.616, 0.649]0.636× [0.616, 0.649]get100get100: 0.675× [0.661, 0.690]0.675× [0.661, 0.690]get1000get1000: 0.673× [0.666, 0.680]0.673× [0.666, 0.680]range100range100: 0.697× [0.636, 0.738]0.697× [0.636, 0.738]M1w8 (8 workers, 5/7 clean)grvgrv: 1.025× [1.007, 1.045]1.025× [1.007, 1.045]get100get100: 0.977× [0.961, 0.992]0.977× [0.961, 0.992]get1000get1000: 1.003× [0.986, 1.021]1.003× [0.986, 1.021]range100range100: 0.966× [0.949, 0.983]0.966× [0.949, 0.983]set100set100: 1.013× [0.978, 1.047]1.013× [0.978, 1.047]M1w64 (64 workers, 5/7 clean)grvgrv: 1.001× [0.970, 1.040]1.001× [0.970, 1.040]get100get100: 0.934× [0.911, 0.966]0.934× [0.911, 0.966]get1000get1000: 0.770× [0.730, 0.821]0.770× [0.730, 0.821]range100range100: 0.867× [0.656, 1.480]0.867× [0.656, 1.480]set100set100: 0.893× [0.887, 0.899]0.893× [0.887, 0.899]
95% CI below 1.0× 95% CI includes 1.0× (inconclusive) 95% CI above 1.0×
Figure M1.3: Lifecycle CPU (E−A) per transactionBelow 1.0× means master uses less process CPU. Includes startup, warmup, validation, and shutdown.

Master uses less CPU than old Go in every 8- and 64-worker cell except range100 at 64 workers, down to 0.553 (set100, 8 workers), and more at 1 worker (1.125–1.292). Against CGo it uses more on get, range and set at 8 and 64 workers (1.037–1.354) and less on grv.

0.5×0.5×0.7×0.7×1.0×1.0×1.4×1.4×2.0×2.0×← Master less CPUDenominator less CPU →master / old Go: M1w1 (1 worker, 4/7 clean)grvgrv: 1.292× [1.255, 1.330]1.292× [1.255, 1.330]get100get100: 1.190× [1.166, 1.211]1.190× [1.166, 1.211]get1000get1000: 1.186× [1.166, 1.205]1.186× [1.166, 1.205]range100range100: 1.125× [1.108, 1.138]1.125× [1.108, 1.138]set100N/A (excluded)batchset50N/A (excluded)batchset50x1000N/A (excluded)master / old Go: M1w8 (8 workers, 5/7 clean)grvgrv: 0.817× [0.792, 0.843]0.817× [0.792, 0.843]get100get100: 0.762× [0.750, 0.774]0.762× [0.750, 0.774]get1000get1000: 0.762× [0.745, 0.780]0.762× [0.745, 0.780]range100range100: 0.865× [0.801, 0.977]0.865× [0.801, 0.977]set100set100: 0.553× [0.542, 0.565]0.553× [0.542, 0.565]batchset50N/A (excluded)batchset50x1000N/A (excluded)master / old Go: M1w64 (64 workers, 5/7 clean)grvgrv: 0.707× [0.692, 0.720]0.707× [0.692, 0.720]get100get100: 0.738× [0.719, 0.759]0.738× [0.719, 0.759]get1000get1000: 0.742× [0.716, 0.771]0.742× [0.716, 0.771]range100range100: 1.180× [0.999, 1.567]1.180× [0.999, 1.567]set100set100: 0.586× [0.572, 0.599]0.586× [0.572, 0.599]batchset50N/A (excluded)batchset50x1000N/A (excluded)master / CGo: M1w1 (1 worker, 4/7 clean)grvgrv: 0.783× [0.768, 0.797]0.783× [0.768, 0.797]get100get100: 0.806× [0.779, 0.833]0.806× [0.779, 0.833]get1000get1000: 0.800× [0.783, 0.821]0.800× [0.783, 0.821]range100range100: 0.908× [0.893, 0.921]0.908× [0.893, 0.921]master / CGo: M1w8 (8 workers, 5/7 clean)grvgrv: 0.728× [0.715, 0.740]0.728× [0.715, 0.740]get100get100: 1.037× [1.027, 1.046]1.037× [1.027, 1.046]get1000get1000: 1.133× [1.096, 1.182]1.133× [1.096, 1.182]range100range100: 1.354× [1.268, 1.513]1.354× [1.268, 1.513]set100set100: 1.148× [1.119, 1.179]1.148× [1.119, 1.179]master / CGo: M1w64 (64 workers, 5/7 clean)grvgrv: 0.565× [0.550, 0.581]0.565× [0.550, 0.581]get100get100: 1.135× [1.082, 1.203]1.135× [1.082, 1.203]get1000get1000: 1.156× [1.111, 1.197]1.156× [1.111, 1.197]range100range100: 1.275× [1.061, 1.731]1.275× [1.061, 1.731]set100set100: 1.249× [1.213, 1.284]1.249× [1.213, 1.284]
95% CI below 1.0× 95% CI includes 1.0× (inconclusive) 95% CI above 1.0×
Figure M1.4: p50 and p99 latency at 8 and 64 workersBar is p50, whisker is p99, in µs per transaction (medians of six runs).

Each worker count ran as a separate phase with its own transaction count, so compare arms within a count.

0 µs2000 µs4000 µs6000 µs8000 µs10000 µs12000 µs14000 µsM1w8 grvM1w8 grv master: p50 1745.5 µs, p99 2733.4 µsmaster 1746 / p99 2733 µsM1w8 grv old Go: p50 1848.9 µs, p99 2937.6 µsold Go 1849 / p99 2938 µsM1w8 grv CGo: p50 1704.8 µs, p99 2890.4 µsCGo 1705 / p99 2890 µsM1w8 get100M1w8 get100 master: p50 2317.0 µs, p99 3663.5 µsmaster 2317 / p99 3664 µsM1w8 get100 old Go: p50 2058.8 µs, p99 3597.4 µsold Go 2059 / p99 3597 µsM1w8 get100 CGo: p50 2354.6 µs, p99 3978.9 µsCGo 2355 / p99 3979 µsM1w8 get1000M1w8 get1000 master: p50 2400.3 µs, p99 3954.8 µsmaster 2400 / p99 3955 µsM1w8 get1000 old Go: p50 2078.9 µs, p99 3287.3 µsold Go 2079 / p99 3287 µsM1w8 get1000 CGo: p50 2413.0 µs, p99 4166.0 µsCGo 2413 / p99 4166 µsM1w8 range100M1w8 range100 master: p50 2911.2 µs, p99 4832.3 µsmaster 2911 / p99 4832 µsM1w8 range100 old Go: p50 2638.5 µs, p99 4681.3 µsold Go 2638 / p99 4681 µsM1w8 range100 CGo: p50 3020.2 µs, p99 4714.7 µsCGo 3020 / p99 4715 µsM1w8 set100M1w8 set100 master: p50 4430.7 µs, p99 6440.9 µsmaster 4431 / p99 6441 µsM1w8 set100 old Go: p50 3844.9 µs, p99 5662.3 µsold Go 3845 / p99 5662 µsM1w8 set100 CGo: p50 4357.0 µs, p99 6731.7 µsCGo 4357 / p99 6732 µsM1w64 grvM1w64 grv master: p50 1884.8 µs, p99 3418.3 µsmaster 1885 / p99 3418 µsM1w64 grv old Go: p50 2054.9 µs, p99 3844.9 µsold Go 2055 / p99 3845 µsM1w64 grv CGo: p50 1909.2 µs, p99 3612.3 µsCGo 1909 / p99 3612 µsM1w64 get100M1w64 get100 master: p50 2922.1 µs, p99 4812.1 µsmaster 2922 / p99 4812 µsM1w64 get100 old Go: p50 2216.8 µs, p99 4230.2 µsold Go 2217 / p99 4230 µsM1w64 get100 CGo: p50 3195.9 µs, p99 6104.8 µsCGo 3196 / p99 6105 µsM1w64 get1000M1w64 get1000 master: p50 3039.8 µs, p99 5019.0 µsmaster 3040 / p99 5019 µsM1w64 get1000 old Go: p50 2350.7 µs, p99 4753.0 µsold Go 2351 / p99 4753 µsM1w64 get1000 CGo: p50 3964.5 µs, p99 13230.4 µsCGo 3964 / p99 13230 µsM1w64 range100M1w64 range100 master: p50 3425.4 µs, p99 6721.9 µsmaster 3425 / p99 6722 µsM1w64 range100 old Go: p50 3101.4 µs, p99 7527.6 µsold Go 3101 / p99 7528 µsM1w64 range100 CGo: p50 5130.3 µs, p99 9909.4 µsCGo 5130 / p99 9909 µsM1w64 set100M1w64 set100 master: p50 5383.5 µs, p99 8122.7 µsmaster 5384 / p99 8123 µsM1w64 set100 old Go: p50 4186.4 µs, p99 7382.2 µsold Go 4186 / p99 7382 µsM1w64 set100 CGo: p50 5993.7 µs, p99 9176.0 µsCGo 5994 / p99 9176 µs
master old Go CGo

Geometric mean of six paired block ratios, with nominal 95% block-bootstrap intervals (20,000 resamples, preregistered seed 492817). Below 1 means the numerator has lower latency, CPU or allocation.

On Linux, master’s pure-Go client coalesces outgoing requests before a write batch, using the same 10 µs minimum and 20 µs maximum delay settings as libfdb_c 7.3.77. The delay depends on time since the last write and is jittered. fdb.WithCoalesceDelay(minDelay, maxDelay time.Duration) changes those settings.

M1w1 (1 worker, 18,000 tx/arm per workload — 4/7 clean)

M1w1: master / old Go

WorkloadMean latencyp50 latencyp99 latencyE−A CPU per txGo bytes per txGo mallocs per tx
grv1.046 [1.002, 1.076]1.049 [1.006, 1.080]1.073 [0.927, 1.245]1.292 [1.255, 1.330]0.879 [0.878, 0.881]0.790 [0.790, 0.791]
get1001.094 [1.062, 1.133]1.088 [1.053, 1.128]1.108 [0.984, 1.242]1.190 [1.166, 1.211]0.924 [0.924, 0.925]0.785 [0.785, 0.785]
get10001.088 [1.071, 1.103]1.091 [1.082, 1.099]1.121 [0.965, 1.302]1.186 [1.166, 1.205]0.941 [0.941, 0.942]0.785 [0.785, 0.785]
range1001.077 [1.062, 1.091]1.080 [1.064, 1.092]1.106 [1.021, 1.202]1.125 [1.108, 1.138]0.891 [0.891, 0.892]0.797 [0.796, 0.797]
set100N/A (pre-arm client CPU gate)—————
batchset50N/A (not reached)—————
batchset50x1000N/A (not reached)—————

M1w1: master / CGo

WorkloadMean latencyp50 latencyp99 latencyE−A CPU per tx
grv0.644 [0.622, 0.657]0.636 [0.616, 0.649]0.713 [0.675, 0.762]0.783 [0.768, 0.797]
get1000.681 [0.664, 0.699]0.675 [0.661, 0.690]0.797 [0.735, 0.902]0.806 [0.779, 0.833]
get10000.677 [0.671, 0.683]0.673 [0.666, 0.680]0.756 [0.718, 0.801]0.800 [0.783, 0.821]
range1000.708 [0.655, 0.745]0.697 [0.636, 0.738]0.795 [0.715, 0.881]0.908 [0.893, 0.921]

M1w1: old Go / CGo

WorkloadMean latencyp50 latencyp99 latencyE−A CPU per tx
grv0.615 [0.606, 0.625]0.607 [0.600, 0.613]0.665 [0.572, 0.759]0.606 [0.581, 0.629]
get1000.622 [0.610, 0.636]0.621 [0.608, 0.634]0.720 [0.633, 0.827]0.678 [0.666, 0.689]
get10000.622 [0.611, 0.631]0.617 [0.608, 0.626]0.674 [0.596, 0.769]0.674 [0.667, 0.683]
range1000.658 [0.607, 0.689]0.645 [0.587, 0.679]0.719 [0.634, 0.817]0.807 [0.788, 0.826]

M1w8 (8 workers, 72,000 tx/arm per workload — 5/7 clean)

M1w8: master / old Go

WorkloadMean latencyp50 latencyp99 latencyE−A CPU per txGo bytes per txGo mallocs per tx
grv1.034 [1.023, 1.047]0.941 [0.930, 0.954]0.945 [0.895, 1.001]0.817 [0.792, 0.843]0.837 [0.836, 0.839]0.777 [0.776, 0.777]
get1001.110 [1.090, 1.133]1.113 [1.097, 1.128]1.067 [0.919, 1.244]0.762 [0.750, 0.774]0.870 [0.869, 0.870]0.766 [0.766, 0.766]
get10001.147 [1.136, 1.157]1.154 [1.139, 1.172]1.161 [1.024, 1.286]0.762 [0.745, 0.780]0.900 [0.899, 0.901]0.767 [0.767, 0.767]
range1001.118 [1.101, 1.139]1.112 [1.096, 1.129]1.056 [0.950, 1.191]0.865 [0.801, 0.977]0.894 [0.894, 0.895]0.809 [0.805, 0.815]
set1001.178 [1.138, 1.217]1.137 [1.099, 1.168]1.109 [1.047, 1.163]0.553 [0.542, 0.565]0.629 [0.628, 0.630]0.555 [0.554, 0.555]
batchset50N/A (log queue 118,497,454 bytes > 100 MB gate)—————
batchset50x1000N/A (not reached)—————

M1w8: master / CGo

WorkloadMean latencyp50 latencyp99 latencyE−A CPU per tx
grv1.021 [1.002, 1.042]1.025 [1.007, 1.045]0.996 [0.865, 1.157]0.728 [0.715, 0.740]
get1000.974 [0.953, 0.998]0.977 [0.961, 0.992]0.972 [0.866, 1.108]1.037 [1.027, 1.046]
get10000.988 [0.966, 1.017]1.003 [0.986, 1.021]0.924 [0.784, 1.083]1.133 [1.096, 1.182]
range1000.968 [0.949, 0.988]0.966 [0.949, 0.983]1.083 [0.983, 1.207]1.354 [1.268, 1.513]
set1001.010 [0.974, 1.046]1.013 [0.978, 1.047]0.936 [0.867, 1.000]1.148 [1.119, 1.179]

The set100 p99 upper bound rounds to 1.000; unrounded it is 0.99954.

M1w8: old Go / CGo

WorkloadMean latencyp50 latencyp99 latencyE−A CPU per tx
grv0.987 [0.970, 1.010]1.090 [1.069, 1.117]1.054 [0.896, 1.236]0.891 [0.856, 0.924]
get1000.877 [0.871, 0.884]0.878 [0.873, 0.882]0.911 [0.853, 0.993]1.361 [1.336, 1.384]
get10000.861 [0.840, 0.887]0.869 [0.849, 0.890]0.796 [0.687, 0.926]1.488 [1.445, 1.537]
range1000.866 [0.857, 0.874]0.869 [0.861, 0.876]1.025 [0.975, 1.076]1.566 [1.541, 1.595]
set1000.858 [0.840, 0.883]0.891 [0.870, 0.921]0.844 [0.803, 0.879]2.075 [2.033, 2.106]

M1w64 (64 workers, 192,000 tx/arm per workload — 5/7 clean)

M1w64: master / old Go

WorkloadMean latencyp50 latencyp99 latencyE−A CPU per txGo bytes per txGo mallocs per tx
grv0.956 [0.937, 0.974]0.923 [0.907, 0.938]0.923 [0.813, 1.036]0.707 [0.692, 0.720]0.831 [0.830, 0.831]0.790 [0.790, 0.790]
get1001.310 [1.273, 1.354]1.348 [1.308, 1.395]1.125 [1.057, 1.198]0.738 [0.719, 0.759]0.850 [0.850, 0.851]0.771 [0.771, 0.771]
get10001.261 [1.231, 1.290]1.298 [1.254, 1.331]1.077 [1.002, 1.150]0.742 [0.716, 0.771]0.887 [0.886, 0.888]0.772 [0.772, 0.772]
range1001.393 [1.079, 2.279]1.435 [1.099, 2.378]1.134 [0.875, 1.860]1.180 [0.999, 1.567]0.901 [0.893, 0.918]0.811 [0.806, 0.820]
set1001.265 [1.253, 1.277]1.283 [1.273, 1.294]1.110 [1.055, 1.190]0.586 [0.572, 0.599]0.612 [0.612, 0.612]0.561 [0.561, 0.561]
batchset50N/A (log queue 426,278,385 bytes > 100 MB gate)—————
batchset50x1000N/A (not reached)—————

Range100 at 64 workers is unstable: its master/old intervals are wide in every metric. Per-order, half-split, moving-block and leave-one-block-out diagnostics for every estimate are in analysis/M1-comparisons.csv.

M1w64: master / CGo

WorkloadMean latencyp50 latencyp99 latencyE−A CPU per tx
grv0.975 [0.927, 1.024]1.001 [0.970, 1.040]0.948 [0.772, 1.131]0.565 [0.550, 0.581]
get1000.877 [0.849, 0.910]0.934 [0.911, 0.966]0.773 [0.693, 0.853]1.135 [1.082, 1.203]
get10000.728 [0.693, 0.770]0.770 [0.730, 0.821]0.395 [0.336, 0.494]1.156 [1.111, 1.197]
range1000.858 [0.655, 1.438]0.867 [0.656, 1.480]0.869 [0.665, 1.453]1.275 [1.061, 1.731]
set1000.902 [0.896, 0.909]0.893 [0.887, 0.899]0.900 [0.835, 0.989]1.249 [1.213, 1.284]

M1w64: old Go / CGo

WorkloadMean latencyp50 latencyp99 latencyE−A CPU per tx
grv1.020 [0.983, 1.070]1.085 [1.059, 1.128]1.028 [0.893, 1.186]0.799 [0.776, 0.825]
get1000.670 [0.647, 0.689]0.693 [0.673, 0.713]0.688 [0.614, 0.770]1.537 [1.471, 1.605]
get10000.577 [0.549, 0.610]0.593 [0.563, 0.630]0.367 [0.308, 0.446]1.558 [1.526, 1.600]
range1000.616 [0.602, 0.633]0.604 [0.591, 0.623]0.767 [0.745, 0.790]1.081 [1.060, 1.106]
set1000.713 [0.707, 0.719]0.696 [0.689, 0.703]0.810 [0.784, 0.836]2.132 [2.088, 2.183]

p99.9 is in the detailed outputs but is not reported here: six blocks are too few for it.

Absolute values

Medians of six run-level statistics per arm. These are not quantiles of pooled transactions.

PhaseWorkloadArmTransactionsMean µsp50 µsp99 µsE−A CPU µs/txGo bytes/txGo mallocs/tx
M1w1grvold Go180001010.6984.31577.8288.13,83438.1
M1w1grvmaster180001078.31049.61835.6371.73,37030.1
M1w1grvCGo180001645.91620.62504.6465.342410.0
M1w1get100old Go180001404.71369.52529.5550.66,36769.2
M1w1get100master180001531.21491.42690.6665.15,88754.3
M1w1get100CGo180002262.22218.23586.7816.666416.0
M1w1get1000old Go180001429.51384.32374.5570.78,23469.2
M1w1get1000master180001539.11502.62785.0675.47,75054.3
M1w1get1000CGo180002286.72238.53670.4848.81,58316.0
M1w1range100old Go180001684.31622.42915.8772.950,28575.7
M1w1range100master180001812.11755.63243.3865.744,83560.3
M1w1range100CGo180002482.62420.73911.9950.125,676224.0
M1w8grvold Go720001706.31848.92937.6127.53,55035.8
M1w8grvmaster720001772.11745.52733.4103.12,97327.8
M1w8grvCGo720001738.81704.82890.4142.042510.0
M1w8get100old Go720002109.82058.83597.4278.36,26567.7
M1w8get100master720002354.02317.03663.5211.35,44951.9
M1w8get100CGo720002418.22354.63978.9204.366416.0
M1w8get1000old Go720002117.92078.93287.3291.88,12967.7
M1w8get1000master720002436.32400.33954.8220.67,31951.9
M1w8get1000CGo720002503.22413.04166.0197.81,57716.0
M1w8range100old Go720002675.92638.54681.3511.050,14774.5
M1w8range100master720002965.12911.24832.3422.144,84960.0
M1w8range100CGo720003074.63020.24714.7328.225,673224.0
M1w8set100old Go720003769.23844.95662.3436.310,295100.8
M1w8set100master720004485.24430.76440.9236.36,47955.9
M1w8set100CGo720004409.24357.06731.7208.144110.0
M1w64grvold Go1920002035.02054.93844.964.93,46734.3
M1w64grvmaster1920001946.01884.83418.345.72,87927.1
M1w64grvCGo1920001996.71909.23612.380.242410.0
M1w64get100old Go1920002302.22216.84230.2153.26,31766.5
M1w64get100master1920002961.32922.14812.1111.35,37151.3
M1w64get100CGo1920003416.53195.96104.899.066416.0
M1w64get1000old Go1920002463.72350.74753.0161.38,17066.5
M1w64get1000master1920003062.23039.85019.0120.57,25051.4
M1w64get1000CGo1920004228.23964.513230.4103.51,57616.0
M1w64range100old Go1920003336.13101.47527.6226.450,06872.8
M1w64range100master1920003597.43425.46721.9237.644,70458.7
M1w64range100CGo1920005402.15130.39909.4210.225,672224.0
M1w64set100old Go1920004303.94186.47382.2232.210,49198.8
M1w64set100master1920005470.95383.58122.7134.76,41955.4
M1w64set100CGo1920006055.65993.79176.0107.844010.0

Throughput (post-hoc, exploratory)

Figure M1.5: Throughput, operations per second (post-hoc)Above 1.0× means master completes more operations per second. Post-hoc, not preregistered.

At 8 and 64 workers master completes fewer operations per second than old Go on get, range and set (0.718–0.903), and more on grv at 64 workers (1.057). Against CGo its rate is higher or the interval includes 1.0 in every cell except grv at 8 workers (0.979).

0.4×0.4×0.7×0.7×1.0×1.0×1.4×1.4×2.0×2.0×← Denominator more ops/sMaster more ops/s →master / old Go: M1w1 (1 worker)grvgrv: 0.956× [0.929, 0.998]0.956× [0.929, 0.998]get100get100: 0.914× [0.883, 0.942]0.914× [0.883, 0.942]get1000get1000: 0.919× [0.907, 0.934]0.919× [0.907, 0.934]range100range100: 0.929× [0.917, 0.942]0.929× [0.917, 0.942]master / old Go: M1w8 (8 workers)grvgrv: 0.984× [0.970, 0.996]0.984× [0.970, 0.996]get100get100: 0.903× [0.885, 0.920]0.903× [0.885, 0.920]get1000get1000: 0.875× [0.867, 0.883]0.875× [0.867, 0.883]range100range100: 0.897× [0.881, 0.911]0.897× [0.881, 0.911]set100set100: 0.854× [0.828, 0.882]0.854× [0.828, 0.882]master / old Go: M1w64 (64 workers)grvgrv: 1.057× [1.036, 1.080]1.057× [1.036, 1.080]get100get100: 0.769× [0.745, 0.792]0.769× [0.745, 0.792]get1000get1000: 0.798× [0.779, 0.819]0.798× [0.779, 0.819]range100range100: 0.718× [0.440, 0.925]0.718× [0.440, 0.925]set100set100: 0.796× [0.787, 0.804]0.796× [0.787, 0.804]master / CGo: M1w1 (1 worker)grvgrv: 1.553× [1.523, 1.606]1.553× [1.523, 1.606]get100get100: 1.468× [1.431, 1.507]1.468× [1.431, 1.507]get1000get1000: 1.478× [1.465, 1.492]1.478× [1.465, 1.492]range100range100: 1.412× [1.341, 1.527]1.412× [1.341, 1.527]master / CGo: M1w8 (8 workers)grvgrv: 0.979× [0.959, 0.998]0.979× [0.959, 0.998]get100get100: 1.027× [1.002, 1.049]1.027× [1.002, 1.049]get1000get1000: 1.012× [0.985, 1.036]1.012× [0.985, 1.036]range100range100: 1.031× [1.010, 1.053]1.031× [1.010, 1.053]set100set100: 0.990× [0.956, 1.027]0.990× [0.956, 1.027]master / CGo: M1w64 (64 workers)grvgrv: 1.028× [0.979, 1.081]1.028× [0.979, 1.081]get100get100: 1.140× [1.097, 1.179]1.140× [1.097, 1.179]get1000get1000: 1.368× [1.294, 1.436]1.368× [1.294, 1.436]range100range100: 1.158× [0.691, 1.514]1.158× [0.691, 1.514]set100set100: 1.113× [1.104, 1.121]1.113× [1.104, 1.121]
95% CI above 1.0× 95% CI includes 1.0× (inconclusive) 95% CI below 1.0×

Post-hoc and exploratory: throughput was not preregistered. The rates and intervals below are derived afterwards from the retained raw data of the same eligible cases. They do not change any estimate, exclusion or result above.

  • Throughput: completed operations divided by the measured wall window, in operations per second. One operation is one workload transaction. The window excludes setup, warmup, validation and shutdown.
  • Busy cores: client process user+system CPU during the measured window divided by its wall time (1.0 = one core busy), including Go and native threads but not the FDB servers. This is not the lifecycle E−A CPU reported above.
  • Estimates: absolute ops/s and busy cores are medians of six processes per arm. Ratios are geometric means of six within-block throughput ratios, with the same bootstrap as above (20,000 resamples, seed 492817). Above 1 means the numerator completes more operations per second. Ratios are not quotients of the displayed medians.
  • Closed loop: each phase runs a fixed number of workers, so the measured rate is 0.973–1.000 (median 0.998) of workers divided by mean latency. The rates describe these operating points, not peak or saturated throughput.

M1w1: throughput

WorkloadOps/s old Go / master / CGomaster / old Gomaster / CGoold Go / CGoBusy cores old Go / master / CGo
grv989 / 927 / 6070.956 [0.929, 0.998]1.553 [1.523, 1.606]1.624 [1.600, 1.649]0.246 / 0.308 / 0.255
get100712 / 653 / 4420.914 [0.883, 0.942]1.468 [1.431, 1.507]1.607 [1.572, 1.638]0.351 / 0.387 / 0.328
get1000699 / 650 / 4370.919 [0.907, 0.934]1.478 [1.465, 1.492]1.607 [1.584, 1.637]0.353 / 0.397 / 0.338
range100594 / 552 / 4030.929 [0.917, 0.942]1.412 [1.341, 1.527]1.521 [1.452, 1.647]0.415 / 0.442 / 0.353

M1w8: throughput

WorkloadOps/s old Go / master / CGomaster / old Gomaster / CGoold Go / CGoBusy cores old Go / master / CGo
grv4,589 / 4,511 / 4,5980.984 [0.970, 0.996]0.979 [0.959, 0.998]0.995 [0.973, 1.011]0.477 / 0.404 / 0.563
get1003,784 / 3,397 / 3,3060.903 [0.885, 0.920]1.027 [1.002, 1.049]1.137 [1.128, 1.146]0.894 / 0.642 / 0.601
get10003,760 / 3,283 / 3,1940.875 [0.867, 0.883]1.012 [0.985, 1.036]1.157 [1.124, 1.186]0.945 / 0.653 / 0.559
range1002,975 / 2,692 / 2,5990.897 [0.881, 0.911]1.031 [1.010, 1.053]1.149 [1.140, 1.161]1.358 / 1.031 / 0.778
set1002,110 / 1,783 / 1,8140.854 [0.828, 0.882]0.990 [0.956, 1.027]1.159 [1.126, 1.183]0.793 / 0.393 / 0.332

M1w64: throughput

WorkloadOps/s old Go / master / CGomaster / old Gomaster / CGoold Go / CGoBusy cores old Go / master / CGo
grv30,937 / 32,673 / 31,8501.057 [1.036, 1.080]1.028 [0.979, 1.081]0.973 [0.928, 1.008]1.494 / 1.228 / 2.135
get10027,430 / 21,479 / 18,6130.769 [0.745, 0.792]1.140 [1.097, 1.179]1.481 [1.439, 1.532]3.590 / 2.147 / 1.580
get100025,634 / 20,749 / 15,0740.798 [0.779, 0.819]1.368 [1.294, 1.436]1.714 [1.620, 1.802]3.531 / 2.251 / 1.378
range10018,919 / 17,509 / 11,7760.718 [0.440, 0.925]1.158 [0.691, 1.514]1.613 [1.569, 1.650]3.742 / 3.726 / 2.296
set10014,688 / 11,654 / 10,4570.796 [0.787, 0.804]1.113 [1.104, 1.121]1.399 [1.385, 1.413]2.911 / 1.409 / 0.987

Range100 at 64 workers has a wide, unstable interval here as well. Process, absolute, paired and omission records, all marked post-hoc, are in analysis/M1-throughput.csv; formulas and scopes are in analysis/throughput-method.json.

Comparison with the 2026-10-14 campaign

Old Go is the revision the 2026-10-14 campaign measured as its final pure-Go tree. Its ratio to each campaign’s own CGo arm shows how much the two cohorts differ before any code change is considered: fresh hosts, a three-arm schedule, the harness amendment and different times. Blocks are never pooled across campaigns, and no cross-campaign interval or ratio of ratios is computed.

Old Go / CGo in both campaigns

Casep50, 2026-10-14p50, 2026-10-20p99, 2026-10-14p99, 2026-10-20E−A CPU, 2026-10-14E−A CPU, 2026-10-20
w1/grv0.580 [0.571, 0.591]0.607 [0.600, 0.613]0.498 [0.408, 0.601]0.665 [0.572, 0.759]0.570 [0.551, 0.588]0.606 [0.581, 0.629]
w1/get1000.698 [0.691, 0.703]0.621 [0.608, 0.634]0.865 [0.734, 1.037]0.720 [0.633, 0.827]0.660 [0.648, 0.671]0.678 [0.666, 0.689]
w1/get10000.662 [0.652, 0.673]0.617 [0.608, 0.626]0.705 [0.654, 0.779]0.674 [0.596, 0.769]0.629 [0.603, 0.653]0.674 [0.667, 0.683]
w1/range1000.627 [0.567, 0.665]0.645 [0.587, 0.679]0.715 [0.595, 0.861]0.719 [0.634, 0.817]0.747 [0.731, 0.761]0.807 [0.788, 0.826]
w8/grv1.137 [1.116, 1.159]1.090 [1.069, 1.117]1.101 [0.869, 1.457]1.054 [0.896, 1.236]0.904 [0.878, 0.931]0.891 [0.856, 0.924]
w8/get1000.889 [0.874, 0.904]0.878 [0.873, 0.882]0.815 [0.741, 0.882]0.911 [0.853, 0.993]1.291 [1.264, 1.331]1.361 [1.336, 1.384]
w8/get10000.946 [0.923, 0.971]0.869 [0.849, 0.890]0.931 [0.859, 0.993]0.796 [0.687, 0.926]1.362 [1.342, 1.382]1.488 [1.445, 1.537]
w8/range1000.882 [0.856, 0.909]0.869 [0.861, 0.876]0.925 [0.861, 0.987]1.025 [0.975, 1.076]1.544 [1.476, 1.651]1.566 [1.541, 1.595]
w8/set1000.998 [0.986, 1.009]0.891 [0.870, 0.921]0.916 [0.860, 0.976]0.844 [0.803, 0.879]2.142 [2.123, 2.160]2.075 [2.033, 2.106]
w64/grv1.090 [1.064, 1.121]1.085 [1.059, 1.128]0.960 [0.757, 1.190]1.028 [0.893, 1.186]0.736 [0.721, 0.752]0.799 [0.776, 0.825]
w64/get1000.682 [0.649, 0.721]0.693 [0.673, 0.713]0.679 [0.601, 0.776]0.688 [0.614, 0.770]1.429 [1.385, 1.478]1.537 [1.471, 1.605]
w64/get10000.665 [0.628, 0.710]0.593 [0.563, 0.630]0.385 [0.336, 0.438]0.367 [0.308, 0.446]1.608 [1.509, 1.705]1.558 [1.526, 1.600]
w64/range1000.532 [0.369, 0.653]0.604 [0.591, 0.623]0.732 [0.506, 0.912]0.767 [0.745, 0.790]1.069 [0.923, 1.180]1.081 [1.060, 1.106]
w64/set1000.884 [0.871, 0.893]0.696 [0.689, 0.703]0.872 [0.815, 0.927]0.810 [0.784, 0.836]1.981 [1.935, 2.021]2.132 [2.088, 2.183]
  • The same revision’s CPU ratio to CGo is similar in both cohorts. For set100, old Go/CGo E−A CPU was 2.142 (8 workers) and 1.981 (64 workers) on 2026-10-14, and is 2.075 and 2.132 now. Master/CGo is 1.148 and 1.249. That describes a smaller Go-versus-CGo CPU gap; the reported comparison remains the directly paired master/old contrast.
  • Latency ratios to CGo vary more between cohorts, notably set100 p50 at 64 workers (0.884 then, 0.696 now). Cross-campaign differences in absolute latency are not attributed to code.