← Back to Blog
Benchmarks

Same QuantLib source, seven compiler builds: which prices change, and why

We built QuantLib 1.41 seven ways and priced the same book through each. Compiler vendor and version change nothing. Fused multiply-add moves sixteen prices at the twelfth decimal. An ill-conditioned calibration moves in the fourth significant figure. Pick any two builds and see.

Natalija Karpichina
Natalija Karpichina
· 3 min read
QuantLib model validation compilers floating point reproducibility

A release note that says “no functional change” is a claim about source code. Whether the numbers changed is a different question, and the only way to answer it is to run the same book through both builds and compare.

So we did. QuantLib 1.41, built seven ways on one Debian machine: gcc 12.2 at -O2, gcc 12.2 at -O3 -march=native, clang 14, gcc 13, -ffast-math, and vectorisation with fused multiply-add on and off. One book of 38 plain prices and sensitivities (European, American, barrier, Heston, Asian, curves, swaps, swaptions, digitals), plus Heston and SABR calibrations and a Sobol barrier Monte Carlo. The book itself was compiled once; only the library differs.

Pick two builds

PriceBuild 1Build 2Relative difference

Same QuantLib 1.41 source, same inputs, 38 plain prices and sensitivities. Compiler vendor and version change nothing (0 of 38). Every difference at -O3 is fused multiply-add. The recorded AADC tape, replayed, gives the same bits under glibc and openlibm; native code does not. Run on 2 October 2026; every number here is from that run.

What did not move

Compiler vendor and compiler version changed nothing. gcc 12, gcc 13 and clang 14 at -O2 agree bit for bit on all 38 values and on every calibration. Vectorisation alone (-march=native with contraction off) also changed nothing.

That is worth saying plainly, because “we moved to a new compiler” is one of the things a model-risk function is asked to sign off, and for this library at this optimisation level the honest answer is: nothing moved.

What moved a little

Every difference at -O3 -march=native is fused multiply-add. The -O3 build and the “FMA only” build differ from baseline on the same sixteen values, value for value, and nowhere else. The largest relative difference is 4.7e-12, on an American option’s finite-difference gamma; most of the sixteen are one to six ulps.

Swapping the maths library (glibc for openlibm, same binary) moves 7 of 38 by at most 6.6e-12. The same Python wheel on Linux and on macOS, both x86-64, moves 4 of 38 by at most 2.7e-12. All of this is the last digit or two, amplified only where a finite-difference derivative divides a small difference by a small step.

What moved enough to matter

An iterative solver amplifies. A well-conditioned Heston calibration, converged to its 1e-8 tolerance, moves its parameters by about 1e-10 across builds and the prices off it by about 1e-12. Harmless.

An ill-conditioned one does not behave. With a SABR-generated market the Heston fit lands at theta around 1,019 and kappa around 5e-5, so only their product is identified. Across builds theta and kappa then move by 1.5e-4 relative, in the fourth significant figure, and a vanilla priced off the fitted model moves by 5e-7. That is from a compiler flag whose release note would say “no functional change”.

The one that was not about compilers at all

QuantLib’s unbiased Sobol barrier engine, run without a seed, returned 1.00751, 0.99311 and 0.99630 on three successive runs of the same binary, against an analytic 0.99346. Our own first reading, before we understood it, was “clang moved a price by one percent”.

It was the clock. The unbiased barrier pricer draws an extra uniform per step for its Brownian-bridge crossing test, and with no seed the generator is seeded from clock(). Seed it, and the seven builds agree to two ulps.

This is documented behaviour of that engine used without a seed. It is not a defect in QuantLib. The point is sharper than a bug: reading the source did not find it, and the recorded execution did. The tape for a 255-path run showed 21,658 random inputs for the 2,040 Sobol draws that were asked for, with 4,083 operations attributed to two lines of mcbarrierengine.cpp. The number of random inputs was the tell.

One more property of the recording

The recorded tape replays bit-identically under glibc and under openlibm. The native library does not. A tape recorded on the old platform is therefore a portable reference to compare the new platform against, with one caveat worth stating: the replay is itself a third set of numbers, a few ulps from either native run, so “the tape agrees with the library” is a statement with a tolerance, not an identity.

Reproduce it

Everything above comes from one folder of scripts and outputs on one machine, run on 2 October 2026. The library is QuantLib 1.41 from the release tarball; the tracing build is pip install aadc-quantlib-tracing. Nothing is extrapolated.


The recording and replay use adjoint differentiation tooling from MatLogica (AADC). The compiler comparison itself needs none of it: it is seven builds and a diff.

Want to see these results on your own portfolio?

Get in Touch

Interested in these opportunities?

Let's arrange a free demo for you and your team.

Book a Demo