← blog4 min read

XAD in C++ and MatLogica AADC on the Monte Carlo Ladder

The earlier XAD result went through Python. This time I ran its C++ tape against NablaTensor and MatLogica AADC on the same two options.

benchmarksxadmatlogicac++monte-carloautodiffgreeks
Two charts compare value and Greeks throughput for XAD, NablaTensor, and MatLogica AADC on European and Asian options. MatLogica leads on the Asian; NablaTensor simd leads on the European.

The last comparison ran XAD through Python. Fair objection: what happens if I use its C++ API directly?

I tried it. XAD's European Greeks pass climbs from 0.215 million paths per second through Python to 7.37 million through C++. That is a big correction to the earlier impression. It still finishes behind the other engines on this test. The surprise is MatLogica AADC: it wins the longer Asian option.

What I ran

Two calls, both with five adjoint Greeks (delta, strike sensitivity, vega, rho, and maturity sensitivity):

  • European: one time step, 20 million paths.
  • Asian: 252 fixings, 300,000 paths.

Spot and strike are 100, volatility is 20%, the rate is 3%, and maturity is one year. Each engine prices the same seven spots, from 98 to 102. I warmed it up, ran three ladders with different seeds, and report the median of the warm calls. All runs were on one machine, one process at a time.

The table shows million paths per second for value and Greeks. Higher is better. Each row reuses its random draws across the spot ladder. The NablaTensor and MatLogica runs use eight threads. XAD's C++ benchmark uses one thread; I also tried an eight-thread pool added to my harness.

Engine, cached drawsThreadsEuropeanAsian
NablaTensor simd8274.474.29
NablaTensor cpu-jit8151.543.27
MatLogica AADC8130.765.41
XAD C++ tape17.370.197
XAD C++ tape, harness pool839.431.245

On the European, simd is about twice as fast as MatLogica and 37 times as fast as single-threaded XAD. On the Asian, MatLogica is 26% faster than simd. That Asian lead also appeared in two earlier runs, at 21–30%.

The draw cache matters. Without it, MatLogica's Asian result falls from 5.41 to 0.487 million paths per second: its Python harness rebuilds a 300,000 × 252 array before each replay. XAD's cached Asian result is 0.197, versus 0.127 with fresh draws. These numbers include the work each harness does to supply the paths.

How XAD and MatLogica performed

I used the public XAD C++ tape with one tape per path, following the approach in XAD's own quant benchmarks: record a path, run the adjoint sweep, clear the tape, repeat. That avoids retaining a giant tape for the whole simulation. The same payoff code runs with plain doubles for price only and XAD's active type for Greeks.

XAD's examples run on one core, so that is the main XAD row. To see how much threading would help, I split paths across eight std::thread workers, each with its own tape. That pool is benchmark code I added, not a feature supplied by XAD. It raises throughput by 5.3× on the European and 6.3× on the Asian. XAD still trails every other cached engine in the table.

The tape has a visible cost even before comparing libraries. On the European, XAD processes 169.52 million paths per second when it only prices, but 7.37 million when it also records and computes five Greeks. On the Asian, those figures are 1.044 and 0.197 million. A path with 252 fixings gives the tape a lot more work to do.

The prices line up, too. At spot 100, European estimates range from 9.412 to 9.415 across the engines; the closed-form price is 9.4134. Asian estimates range from 5.296 to 5.301. Those spreads are smaller than the Monte Carlo sampling error at these path counts.

One boundary on the result: this tests XAD's public tape, not XAD-Codegen. Xcelerit reports a faster code-generation backend, but I couldn't benchmark it through the public repository. The tape itself is available under AGPL-3.0-or-later, with a separate enterprise license offered.

Run it yourself

The benchmark branch has the harnesses and run logs for XAD, MatLogica, and NablaTensor. For a quick look at XAD, build its harness from that branch and run the cached passes:

cd bench/the-same-ladder-in-cpp/xad
cmake -B build -GNinja -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/xad_ladder european 20000000 greeks cached
./build/xad_ladder asian 300000 greeks cached
./build/xad_ladder asian 300000 greeks cached threads=8

The last command uses my harness pool. The cached Asian pass is the result I would compare first: that is where the ranking flips. At 300,000 Asian paths, cached draw arrays also push peak process memory above 1 GB for NablaTensor's simd engine; check memory before raising the path count.

XAD and MatLogica, in short: XAD's C++ tape runs about 34× faster than the Python result on the European Greeks pass. With cached draws, NablaTensor simd wins the European and MatLogica wins the Asian. An added thread pool helps XAD, but it still trails both. XAD-Codegen remains unmeasured.


Questions or corrections? open an issue