A pybind11 binding wraps a C++ pricer. Calling it 2 million times from Python is 6 times slower than the same loop written in C++, and running it on four threads gives no speedup at all. Explain both observations and say what you change at the boundary.

A pybind11 binding wraps a C++ pricer. Calling it 2 million times from Python is 6 times slower than the same loop written in C++, and running it on four threads gives no speedup at all. Explain both observations and say what you change at the boundary.

Approach: Count what one crossing of the language boundary costs, then ask what the extension holds for the duration of the call and what it must do to let another thread run.

Each boundary crossing costs roughly 100 to 300 nanoseconds in argument conversion, reference counting and the call protocol, which dominates a short pricer, and the binding holds the global interpreter lock for the whole call so four threads serialise; the fix is batching, so one crossing prices a whole array, plus releasing the lock inside the C++ body around the computation. Batching turns 2 million crossings into one and passes the input as a contiguous array through the buffer protocol with no per-element conversion, which closes almost all of the 6 times gap. Releasing the lock is safe only while the C++ code touches no Python objects, so the input array must be kept alive by the caller and read through a raw pointer. Returning a vector by value copies it into a fresh Python object, so return an array that takes ownership of the existing buffer instead.

Follow-up: How do you keep the released-lock section correct when the C++ code has to call back into a Python callable?

Key concepts: boundary crossing cost, global interpreter lock, batching, buffer protocol.