The XAD showcase, on NablaTensor: Heston, SABR, XVA and LIBOR
Benchmark Heston, SABR calibration, XVA CVA, and LIBOR swaption kernels across XAD, NablaTensor, and MatLogica AADC.
Every article in this series so far has run the same seven-spot ladder — a
one-step European or a 252-fixing Asian call, against whatever library was
under test. That ladder is this project's own invention, not XAD's. XAD's own
README doesn't point at anything like it: it points at
auto-differentiation/ad-benchmarks,
a separate MIT-licensed repository with four different kernels, the ones
Xcelerit chose to publish as evidence XAD is "the fastest tape library" —
Heston stochastic-vol Monte Carlo, SABR surface calibration, a swap-portfolio
XVA CVA, and Mike Giles' LIBOR market model swaption. This article reproduces
those four, not the ladder, against NablaTensor's cpu-jit and simd — and,
since it's already this series' third look at MatLogica AADC's free Python
trial, against that too.
The rules
- Same formulas, same problem sizes, as published. Every model in this
article is a line-for-line reimplementation of
ad-benchmarks/src/{heston,sabr,xva,libor_swaption}.hppagainstcom.nablatensor.engine.SDoubleinstead of XAD'sxad::adj<double>— same market data, same portfolio, same path counts, same iteration counts. Nothing here is scaled down or simplified from what XAD's own suite runs. - Every quirk kept, none quietly fixed. Three of the four models have a
structural oddity worth knowing about before reading the numbers — SABR's
"500 iterations" is a fixed perturbation schedule, not a real converging
fit; LIBOR's
L0[0]is never stochastically evolved by design; XVA's additive rate-shock can blow up the CVA value at these path counts. Each is called out where it matters, and none is patched — the point is reproducing XAD's own published benchmark, not a better-behaved one. - 1 and 8 threads,
cpu-jitandsimd, draws cached — and XAD rebuilt on referential machine, not quoted from a different one. NablaTensor's-Dnablatensor.crn=oncommon-random-numbers cache, same as every prior article in this series. XAD is open source and self-serve (git clone+ CMakeFetchContent, no license, no trial clock), so this article does what the C++ ladder article did: fetchesauto-differentiation/ad-benchmarksitself, builds it with XAD enabled and the same-O3 -mavx2 -mfmaflags its ownCMakeLists.txtuses, and reruns its ownmain.cpp— FD and XAD numbers below are measured on the same box as every NablaTensor row, same session. XAD-Codegen, CppAD, Adept and autodiff are the one thing this article couldn't rebuild: XAD-Codegen has no public source or self-serve trial, and CppAD/Adept/autodiff were left out of this rebuild to skip dependencies this comparison didn't need. Those four stayad-benchmarks' own published numbers (Intel Xeon Platinum 8488C) and are labeled "different CPU" everywhere they appear — read as directional context, not as something this article measured. - MatLogica AADC measured the same way, on the same machine — but only
through its Python bindings.
pip install aadcis the same free trial an earlier article used, with no built-in Monte Carlo engine of its own: each benchmark's per-path kernel is recorded as a scalar computation andaadc.evaluateJIT-compiles and replays it across a numpy-array batch, one array element per path,aadc.ThreadPool(8)for its own parallelism — as it ships, same as every other row. Unlike XAD, MatLogica's C++ core has no self-serve path at all: the pip wheel ships a compiled Python extension with no headers and no library to link against, and AADC's documented C++ API needs an Enterprise or Desk/Machine-Bound license obtained by contacting MatLogica directly. Every MatLogica number below therefore includesaadc.evaluate()'s Python call and array-marshaling overhead on top of the compiled replay itself — the same distinction an earlier article in this series drew for XAD's own Python bindings, before that number turned out to be almost entirely the interpreter's cost, not the library's. - Nothing trusted until it's checked — for both engines. Heston's price and Greek signs, SABR's Hagan formula against the synthetic market data it was generated from, XVA's CVA non-negativity, and LIBOR's 161-wide reverse sweep against a finite-difference bump on three representative inputs — every benchmark prints its own sanity line before the timing numbers, and the MatLogica harnesses run every one of the same checks independently, not just NablaTensor's. None of the tables below should be read without it.
1. Heston stochastic-vol Monte Carlo
Euler-discretized Heston model, European call, 100 time steps, 10,000 paths,
8 differentiable inputs (spot, strike, maturity, rate, initial variance,
mean-reversion speed, long-run variance, vol-of-vol; correlation fixed at
−0.7). XAD records one tape per path — tape.clearAll(), register 8 inputs,
newRecording(), price, computeAdjoints(), accumulate — the same idiom the
C++ ladder article used. NablaTensor records
the same per-path kernel once as a Nabla.model, batches the 10,000 paths
through scenarios(10_000), and lets cpu-jit/simd compile and thread the
replay.
| engine | threads | machine | primal | gradient (value + 8 Greeks) |
|---|---|---|---|---|
| FD | 1 | referential machine | 7.43 ms | 67.45 ms |
| XAD | 1 | referential machine | 7.43 ms | 36.39 ms |
XAD-Codegen (ad-benchmarks) | 1 | different CPU | 9.25 ms | 21.20 ms |
CppAD (ad-benchmarks) | 1 | different CPU | 9.27 ms | 268.26 ms |
Adept (ad-benchmarks) | 1 | different CPU | 9.27 ms | 90.69 ms |
cpu-jit + cache | 1 | referential machine | 9.45 ms | 23.07 ms |
cpu-jit + cache | 8 | referential machine | 9.49 ms | 3.61 ms |
simd + cache | 1 | referential machine | 7.06 ms | 17.63 ms |
simd + cache | 8 | referential machine | 7.20 ms | 3.83 ms |
| MatLogica AADC (Python) | 8 | referential machine | 1.01 ms | 1.64 ms |
Price at spot 100 / strike 105: NablaTensor 7.835519, all four
configurations agreeing to every printed digit (same seed, same Philox
stream, different code generator); MatLogica 7.555955, a different draw
stream (numpy default_rng, not Philox, not mt19937) landing in the same
economically sensible range. The Greeks agree closely across both
implementations — delta 0.5995 (NablaTensor) / 0.5934 (MatLogica), vega-like
d/dv0 42.95 / 41.99, d/dtheta 59.66 / 56.90, d/dxi −1.84 / −1.88 — three
independent implementations of the same published formula landing on the
same numbers, which is the point of checking at all: no bit-exact draws were
ever expected across three different random number generators.
MatLogica is fastest here, Python overhead and all. 1.64 ms beats
NablaTensor's best (cpu-jit + cache at 8 threads, 3.61 ms) by 2.2x, and
XAD's tape by 22x. That number is the whole Python round trip — building the
input dict, calling aadc.evaluate, unpacking the result — not a bare
measurement of AADC's compiled kernel, since there's no self-serve way to
strip that overhead out and measure the C++ core alone. That it still wins
by this much says the compiled replay underneath is doing something
seriously fast; it doesn't say how much faster a pure-C++ AADC call would be.
cpu-jit beats XAD's own tape on a single thread, before any threading
helps — and this is the same machine, not a cross-CPU guess. 23.07 ms
against XAD's 36.39 ms, rebuilt and rerun right here, is 1.58x; simd at one
thread is 2.06x. This isn't a batching story yet — one thread, no scenario
parallelism engaged beyond what the engine already does per call — it's
compiled replay against a per-path tape recorded fresh 10,000 times. XAD's
own number on this box (36.39 ms) is actually a little faster than
Xcelerit's published one (39.81 ms on their Xeon) — a single-thread workload
like this one rewards a high boost clock more than a server chip's extra
cores, worth remembering before assuming bigger hardware always wins.
XAD-Codegen's 21.20 ms is Xcelerit's own number and stays un-rebuildable (no
public source, no self-serve trial) — the chart shows its bar faded for
exactly that reason, a reference point rather than a measurement. At 8
threads NablaTensor doesn't need that comparison to be close anyway: 3.61 ms
is 5.9x faster than Codegen's published number, whichever CPU either one
ran on.
2. SABR surface calibration
Hagan 2002 implied-vol formula, 5 expiries fit simultaneously (alpha, rho, nu
per expiry, 15 inputs total), 100 strike/expiry points, wrapped in
ad-benchmarks' own "500-iteration optimizer loop" — a fixed cosine
perturbation schedule applied to the parameters each iteration, not a real
converging fit. What's timed is 500 sequential gradient evaluations. There is
no Monte Carlo anywhere in this benchmark — the entire tape is
deterministic, which matters for reading the chart below.
| engine | threads | machine | primal (500 iters) | gradient (500 iters) |
|---|---|---|---|---|
| FD | 1 | referential machine | 1.95 ms | 30.93 ms |
| XAD | 1 | referential machine | 1.86 ms | 7.31 ms |
XAD-Codegen (ad-benchmarks) | 1 | different CPU | 2.12 ms | 4.90 ms |
CppAD (ad-benchmarks) | 1 | different CPU | 2.12 ms | 32.03 ms |
Adept (ad-benchmarks) | 1 | different CPU | 2.10 ms | 18.67 ms |
autodiff (ad-benchmarks) | 1 | different CPU | 2.10 ms | 38.09 ms |
cpu-jit | 1 | referential machine | 12.68 ms | 6.61 ms |
cpu-jit | 8 | referential machine | 21.52 ms | 15.11 ms |
simd | 1 | referential machine | 77.67 ms | 142.28 ms |
simd | 8 | referential machine | 93.81 ms | 171.88 ms |
| MatLogica AADC (Python) | 8 | referential machine | 79.65 ms | 87.88 ms |
(SABR has no Monte Carlo, so there's no -Dnablatensor.crn cache to turn on
— every row here is the same "cached" concept trivially, since nothing is
random.)
Hagan's formula at the true parameters reproduces the synthetic market vols
it was generated from to 0.000e+00 absolute error, and the adjoint gradient
of d(objective)/d(alpha_0) matches a finite-difference bump to six decimal
places (6.037100 both ways) in both implementations — MatLogica's
independent Python recording lands on exactly the same 6.037100, since this
benchmark has no randomness to disagree about, only the same formula
computed twice by two different AD systems. The implementation is correct in
both before any of the timings above are trusted.
This is the one benchmark where NablaTensor's design doesn't help, and
MatLogica's actively hurts. cpu-jit at one thread is still 1.11x faster
than XAD's tape, rebuilt and rerun on this same machine — a real, if narrow,
edge, since NablaTensor is compiling and replaying a kernel rather than
recording an object graph fresh each time. But eight threads make it
slower (15.11 ms, worse than one thread), because there are 500
sequential, data-dependent iterations to run and nothing to split across
cores — the perturbation schedule doesn't depend on the previous iteration's
result, but the harness runs it as XAD's own does, one call after another.
simd is worse again, by an order of magnitude in both directions: its
batched-replay design has a fixed per-call setup cost that never gets to
amortize when every call is a batch of exactly one scenario. MatLogica pays
the same tax and then some: 87.88 ms for the full loop is 12.0x slower than
XAD's tape, because every one of the 500 iterations is a separate
aadc.evaluate() Python call into a kernel that itself runs in
microseconds — the batch that would let MatLogica's compiled replay show
what it can do (as it did on the three Monte-Carlo benchmarks) is a batch of
exactly one call, 500 times over, and Python's own per-call cost dominates
every single one. XAD-Codegen's published 4.90 ms would be the fastest thing
on this chart, but it's Xcelerit's own number on a different CPU, not
something rebuilt here — the only same-machine, self-serve comparison
available, cpu-jit at one thread against XAD's tape right here, is a
narrow NablaTensor win.
Did you know? The Hagan formula being calibrated in this section — SABR's asymptotic expansion for Black-implied volatility — comes from a single 2002 Wilmott paper, Managing Smile Risk, by Hagan, Kumar, Lesniewski and Woodward. Two decades and a great deal of fully-numerical competition later, it's still the default vol-smile interpolation on most trading desks — not because nothing more accurate exists, but because "closed-form, one line, differentiable by inspection" keeps winning against alternatives that need a solver of their own just to produce the number this formula gets in a dozen arithmetic operations.
3. XVA CVA
A 15-swap interest-rate portfolio, credit valuation adjustment over 20
semi-annual time buckets, 10,000 Monte-Carlo paths, 40 differentiable market
inputs: 20 zero-curve points, 10 hazard-curve points, 10 vol-term-structure
points. The heaviest of the four kernels — ad-benchmarks estimates
5,000–10,000 operations per path.
| engine | threads | machine | primal | gradient (value + 40 sensitivities) |
|---|---|---|---|---|
| FD | 1 | referential machine | 505.3 ms | 20.75 s |
| XAD | 1 | referential machine | 516.3 ms | 2.169 s |
XAD-Codegen (ad-benchmarks) | 1 | different CPU | 591.0 ms | 571.9 ms |
CppAD (ad-benchmarks) | 1 | different CPU | 590.3 ms | 7.972 s |
Adept (ad-benchmarks) | 1 | different CPU | 590.1 ms | 7.076 s |
cpu-jit + cache | 1 | referential machine | 675.3 ms | 1.239 s |
cpu-jit + cache | 8 | referential machine | 683.4 ms | 278.1 ms |
simd + cache | 1 | referential machine | 288.7 ms | 1.416 s |
simd + cache | 8 | referential machine | 282.7 ms | 728.3 ms |
| MatLogica AADC (Python) | 8 | referential machine | 19.0 ms | 55.2 ms |
MatLogica is fastest on the heaviest kernel of the four, Python overhead
included. 55.2 ms is 5.0x cpu-jit + cache at 8 threads (278.1 ms) and
39.3x XAD's tape, rebuilt and rerun right here. This is the benchmark where
a real Monte-Carlo batch (10,000 paths, ~5,000–10,000 ops each) gives a
compiled, vectorized replay the most room to work with, and both MatLogica
and NablaTensor's cpu-jit show it: cpu-jit at 8 threads already beats
XAD-Codegen's published 571.9 ms — on a CPU this article can't verify, since
XAD-Codegen has no public source or self-serve trial — by 2.06x, and against
XAD's own tape, same machine, cpu-jit at one thread is already 1.75x
faster and simd at one thread 1.53x, before 8 threads pull cpu-jit to
7.80x.
Now the caveat this benchmark needs — and it holds for every engine
measured, MatLogica included, which is itself useful confirmation.
ad-benchmarks' additive rate-shock model re-applies the same per-path
Gaussian draw at all 20 time buckets — the shock array is generated once
per path and never redrawn between buckets, so it compounds linearly rather
than as a random walk with independent increments. Over the 20,000
independent draws behind 10,000 paths x 20 rate points, a handful will land
far enough in the tail that, multiplied by 20 buckets, a curve point
random-walks to something like −13. Because exp(-rate * t) sits inside the
swap discount factor, that single path's exposure comes out on the order of
10^40+, and dominates the mean — NablaTensor's CVA value is 5.8399 ×
10^43; MatLogica's, an entirely independent Python implementation with its
own numpy random stream, lands at 2.6319 × 10^44 — a different draw
sequence finding a different worst-case path, same astronomical order of
magnitude, both correctly computed and both completely uninterpretable as a
real risk number. Confirmed a third way, outside either implementation
entirely, with a standalone Python re-implementation of just the diffusion
loop: the worst curve point over 20,000 trials at these parameters reaches
roughly −13, and exp(13 × 10) alone is already 10^56-scale.
ad-benchmarks never prints or checks the CVA value itself — only the
timing — so this is a property of the published toy model that its own
authors would have no reason to have noticed. The gradient sanity check
(CVA >= 0) still holds in both implementations, because every term in the
sum is non-negative by construction; it just holds at an astronomical scale.
Read this scenario as a computational-load benchmark, the same as
ad-benchmarks itself does, not as a real CVA methodology — the
SA-CVA article and the
CVA capital showcase
in the main engine repo are where a real, bounded exposure model lives.
4. LIBOR market model swaption
Mike Giles' LIBOR market model benchmark
(testlinadj.cpp),
by way of ad-benchmarks/src/libor_swaption.hpp: 15 swaptions, 80 forward
LIBOR rates, 10,000 paths, 161 differentiable inputs — 1 accrual period
plus 80 initial rates plus 80 vols. This is the benchmark Giles &
Glasserman's Smoking Adjoints exists to justify: a naive one-input-at-a-time
bump needs 162 evaluations per gradient, so the whole point is getting every
adjoint out of a single reverse sweep.
| engine | threads | machine | primal | gradient (value + 161 sensitivities) |
|---|---|---|---|---|
| FD | 1 | referential machine | 106.5 ms | 17.24 s |
| XAD | 1 | referential machine | 107.5 ms | 752.9 ms |
XAD-Codegen (ad-benchmarks) | 1 | different CPU | 139.8 ms | 305.3 ms |
CppAD (ad-benchmarks) | 1 | different CPU | 138.0 ms | 4.572 s |
Adept (ad-benchmarks) | 1 | different CPU | 138.2 ms | 1.155 s |
cpu-jit + cache | 1 | referential machine | 210.2 ms | 531.1 ms |
cpu-jit + cache | 8 | referential machine | 208.4 ms | 89.9 ms |
simd + cache | 1 | referential machine | 128.2 ms | 645.8 ms |
simd + cache | 8 | referential machine | 121.5 ms | 312.4 ms |
| MatLogica AADC (Python) | 8 | referential machine | 11.7 ms | 42.8 ms |
MatLogica wins the widest-input benchmark too, and by the smallest margin
of the three Monte-Carlo scenarios. 42.8 ms, Python overhead included,
beats cpu-jit + cache at 8 threads (89.9 ms) by 2.1x — still a win, but
the narrowest of the three, against NablaTensor's strongest showing of the
article. cpu-jit + cache at 8 threads is itself 8.38x XAD's own tape,
rebuilt and rerun right here (752.9 ms) — the widest input count is where
NablaTensor's compiled, batched replay pulls furthest ahead of XAD's
per-path recording. XAD-Codegen's published 305 ms would still be ahead of
cpu-jit at one thread (531.1 ms), but it's Xcelerit's number on a CPU this
article never touched; both cpu-jit at 8 threads and MatLogica beat it
regardless of that caveat.
One detail worth being precise about, because it looks like a bug and isn't:
L0[0] — the very first forward rate — is never touched by the stochastic
path-generation loop. Giles' evolution only updates L[n+1..] at time step
n, so L[0] stays pinned at its initial value for the entire path. It
still has a real, non-zero gradient, because the final discount-back loop
divides through by every one of L[0..N-1], L[0] included. NablaTensor's
sanity check catches exactly this: d(price)/d(L0_0) comes out to −0.383149
by adjoint and −0.383149 by a finite-difference bump under common random
numbers, agreeing to six decimal places, alongside delta (203.009482 both
ways) and lambda[0] (0.046194 both ways). MatLogica's independent
Python implementation, different draws entirely, checks the same three
inputs and agrees with itself just as tightly: delta 202.195978 adjoint
against 202.196005 finite-difference, L0_0 −0.381923 both ways,
lambda_0 0.040311 both ways. Two unrelated AD systems, two unrelated
random number generators, the same algorithm reproduced exactly rather than
"fixed" — and both pass the check that matters most for a 161-input reverse
sweep.
The pattern across all four
Batching is the whole story, and MatLogica makes the case even harder than
NablaTensor does. Heston, XVA and LIBOR are all Monte Carlo — 10,000
independent scenarios that a compiled, vectorized replay can chew through in
one call. On all three, cpu-jit already beats XAD's own single-threaded
tape — rebuilt and rerun on this same machine, not quoted from a different
one — before any threading (1.58x, 1.75x, 1.42x), and at 8 threads that same
same-machine comparison reaches 7.8x to 10.1x. MatLogica goes further still:
its Python bindings, call overhead included, beat every other engine
measured on all three, by 2.2x, 5.0x and 2.1x over cpu-jit's own best
8-thread numbers. SABR has no Monte Carlo — one deterministic 15-input tape,
500 sequential evaluations — and there cpu-jit at one thread is a narrow
1.11x ahead of XAD, same machine, adding threads makes it slower, simd
loses outright to a tape library with no batching of its own at all, and
MatLogica loses worse than either: 12.0x behind XAD, because 500 separate
Python calls into a kernel that runs in microseconds pay Python's own
per-call tax 500 times over. Same shape of engine, same batching design,
opposite verdict on opposite sides of "is there a batch" — for NablaTensor
and for MatLogica alike.
simd is consistently behind cpu-jit on all four benchmarks measured
here, which is a different result from the equity-option ladder articles
earlier in this series, where simd + cache was the fastest engine
measured. The four kernels in this article are heavier per path (100 Euler
steps with two correlated draws, a 20-bucket nested swap valuation, an
80-rate LIBOR evolution) and narrower in path count (10,000 against the
ladder's 20,000,000), a different point on the tradeoff simd's
wider-but-fixed-cost vectorization makes against cpu-jit's generated
scalar code — worth its own article rather than a guess here. MatLogica's
position swings the widest of any engine measured: fastest of all seven
rows on three benchmarks, second-slowest on the fourth, which is what a
compiled, batch-oriented replay wrapped in a thin Python layer looks like
when the batch disappears.
Did the RNG difference matter?
XAD seeds std::mt19937; NablaTensor seeds its own Philox counter-based
stream; MatLogica has no RNG of its own, so this harness seeds numpy's
default_rng. Three generators, three implementations, and nobody should
expect the same 10,000 paths' worth of draws from any pair of them — this
article never claims bit-exact agreement. What's checked instead is that
each implementation's own internal consistency holds, in both NablaTensor
and MatLogica: SABR's Hagan formula reproduces its own synthetic market data
exactly in each, to the same 6.037100 adjoint gradient; Heston's price and
Greeks have the signs a call should have and land within a few percent of
each other (7.835519 vs 7.555955, different draws); XVA's CVA is
non-negative by construction in both, at the same astronomical order of
magnitude; and LIBOR's 161 adjoints agree with finite differences to six
decimal places under common random numbers, independently, in each
implementation. That is the correctness bar this series has used
throughout, and it is the right one here too — the tables above compare
compute cost, not whether three different random number generators happened
to draw the same numbers.
Try it
Everything — the NablaTensor harness, the MatLogica scripts, and the exact
commands that rebuilt XAD — is in
bench/xad-showcase-on-nablatensor
on the main engine repo, not this site's own repo: one Java class per
benchmark plus a shared warmup-then-median timing harness under
nablatensor/, the four MatLogica scripts alongside it, and the log behind
every table above.
git clone https://github.com/nablatensor-dev/nablatensor
cd nablatensor && git checkout bench/xad-showcase-on-nablatensor
cd bench/xad-showcase-on-nablatensor/nablatensor
mvn -q dependency:build-classpath -Dmdep.outputFile=cp.txt
javac --add-modules jdk.incubator.vector -cp "$(cat cp.txt)" -d out *.java
java --add-modules jdk.incubator.vector -cp "out:$(cat cp.txt)" \
-Dengine=cpu-jit -Dthreads=8 -Dcached=true HestonBench
java --add-modules jdk.incubator.vector -cp "out:$(cat cp.txt)" \
-Dengine=simd -Dthreads=1 SabrBench
java --add-modules jdk.incubator.vector -cp "out:$(cat cp.txt)" \
-Dengine=cpu-jit -Dthreads=8 -Dpaths=10000 XvaBench
java --add-modules jdk.incubator.vector -cp "out:$(cat cp.txt)" \
-Dengine=simd -Dthreads=8 -Dpaths=10000 LiborBench
The XAD/FD numbers in every table above aren't quoted from ad-benchmarks'
own published results — they're a straight rebuild of the actual upstream
repository, on this same machine, same session:
git clone https://github.com/auto-differentiation/ad-benchmarks
cd ad-benchmarks
cmake -B build -GNinja -DCMAKE_BUILD_TYPE=Release \
-DENABLE_XAD=ON -DENABLE_CPPAD=OFF -DENABLE_ADEPT=OFF -DENABLE_AUTODIFF=OFF
cmake --build build -j"$(nproc)"
./build/ad_benchmarks # 10,000 paths, 10 iterations, 3 warmup — its own defaults
XAD-Codegen, CppAD, Adept and autodiff stay ad-benchmarks' own published
numbers, because XAD-Codegen needs a commercial license this article doesn't
have and the other three were left disabled to skip dependencies this
rebuild didn't need — every one of them is labeled "different CPU"
everywhere it's quoted.
MatLogica's four scripts sit next to the Java ones, one level up:
cd bench/xad-showcase-on-nablatensor
uv venv --python 3.12 venv-aadc && source venv-aadc/bin/activate
uv pip install aadc numpy
python heston_matlogica.py
python sabr_matlogica.py
python xva_matlogica.py
python libor_matlogica.py
pip install aadc activates a free trial silently, no account and no terms
text referential machine could find, 48 days left when this ran. As covered above,
this measures the Python bindings only: AADC's C++ API is real and
documented, but needs a license obtained by contacting MatLogica directly,
not a pip install.
ad-benchmarks itself is MIT-licensed (Xcelerit Computing Ltd.); nothing
here copies its C++, only its published formulas, problem sizes and
methodology — median of the timed samples after a discarded warmup, same as
its own src/timing.hpp. The LIBOR model is originally
Mike Giles',
reproduced by way of ad-benchmarks' own adaptation of it.