Plast interview

19. Exceptions vs Codes

In a matching engine’s hot path, unpredictable long tails are unacceptable. Many teams disable exceptions or confine error paths to preserve I‑cache and determinism. Consider the following non-throwing API.

struct Msg{int v;};
bool parse(const char* p, Msg& out) noexcept { if(!p) return false; out.v=1; return true; }
int process(const char* p) noexcept { Msg m{}; if(!parse(p,m)) return -1; return m.v; }

Part 1.

If we replaced parse/process with throwing APIs, what are the steady-state latency and code-size implications on modern ABIs, and why? Explain how marking functions noexcept influences codegen when errors are rare.

Part 2.

(1) Does noexcept change inlining, register allocation, or unwind info emission on the hot path?

(2) How to segregate cold error handling without exceptions to protect I-cache locality?

(3) Compare std::expected<T,E> with out-parameters for this case; cache and branch prediction considerations?

(4) What happens if a noexcept function throws, and how should a trading system mitigate this?

(5) With exceptions enabled, how can unwinding impact RAII destructors’ latency and determinism?

Answer

Answer (Part 1)

On Itanium-style “zero-cost” EH, normal execution has no runtime checks, but code size and unwind metadata increase, stressing I-cache and potentially reducing inlining or tail calls. Throwing APIs also add hidden unwind edges that constrain optimizations and may increase register pressure. Marking functions noexcept lets the compiler assume calls cannot throw, removing EH edges/landing pads, enabling simpler control flow and better register allocation. With rare errors, explicit non-throwing branches keep latency predictable and make worst-case costs visible.

Answer (Part 2)

(1) Yes. noexcept removes exception edges, often improving inlining opportunities, reducing spills, and allowing nounwind call treatment and tail calls.

(2) Isolate rare paths into separate cold functions (compiler-specific cold attributes), use [[unlikely]], and defer heavy logging; prefer fail-fast counters.

(3) std::expected is expressive but can add object moves/branches; out-params minimize traffic. With NRVO/inlining, differences shrink but still affect caches.

(4) std::terminate is invoked. Mitigate via strict contracts, upfront validation, exhaustive testing, and fail-fast watchdog restarts rather than unwinding.

(5) Unwinding triggers cascaded destructors, introducing jitter, contention, or I/O in cleanups. Keep destructors non-blocking and bounded; avoid unwinding across hot loops.