Quant developer HFT course · Module 1: Reading hot code
Virtual functions and vtables
The question
Your strategies all derive from a base class with a virtual on_quote. Someone says virtual calls are slow. Are they right?
What a virtual call compiles to
A class with virtual functions gets a table of function pointers, one table per class, generated by the compiler. Each object of the class carries a hidden pointer to its class's table, usually as the first eight bytes of the object. A call through a base reference becomes three steps:
struct Strategy {
virtual ~Strategy() = default;
virtual void on_quote(const Quote& q) = 0;
};
void dispatch(Strategy& s, const Quote& q) {
s.on_quote(q); // load s's vptr, load slot 1 of the table, call that address
}
So "are virtual calls slow?" has a precise answer. A virtual call costs two dependent loads, an indirect jump the CPU must predict, and the loss of inlining.
What that costs in practice
The loads are usually cheap. If the same few objects are called repeatedly, their vtables sit in L1 like any other hot data. The jump is predicted well when a call site keeps going to the same target, which is the common case: a strategy process often runs one strategy class.
The real cost is the lost optimisation. A small virtual function that could have been inlined into a loop becomes an opaque call. The compiler cannot hoist work out of it, vectorise around it, or assume anything about what it touches in memory. In a tight loop over thousands of quotes that can matter far more than the dispatch itself.
The cost rises when one call site sees many types in an unpredictable order: an array of mixed shapes, a heterogeneous list of event handlers. Then the indirect jump mispredicts regularly as well.
When the compiler removes it
Compilers devirtualise calls when they can prove the dynamic type:
- The object is a local or a member whose type is known at the call site.
- The class or the function is marked
final, so no override can exist. - With link-time optimisation and whole-program analysis, a class with no further subclasses.
struct Momentum final : Strategy {
void on_quote(const Quote& q) override;
};
void run(Momentum& m, const Quote& q) {
m.on_quote(q); // final: the compiler knows the target, can call directly and inline
}Alternatives in trading code
When the set of types is known at compile time, trading code usually avoids runtime dispatch on the hot path:
- Templates. Make the engine a template on the strategy type. Each instantiation calls its strategy directly and can inline it.
- CRTP. A base class template that calls the derived class through
static_cast, giving shared code without virtual calls. Module 3 covers it with templates. std::variantandstd::visit. A closed set of types, stored by value, dispatched by an index the compiler can turn into a jump table or a series of compares.- Grouping by type. Keep one vector per concrete type and loop over each, so every call site sees one target.
None of these is free. Templates increase compile time and code size; variants make the object as large as the largest alternative. Virtual functions remain the right tool for things that happen rarely, such as configuration, start-up and administration.
Check yourself
Question 1 of 3
What happens at run time when you call a virtual function through a base reference?
Virtual dispatch is a load of the vptr, a load of the slot, and an indirect call. There is no name lookup and no dynamic_cast involved.
Question 2 of 3
A loop calls a tiny virtual function on the same object a million times. What is likely the largest cost?
With one object, the vtable stays in cache and the target is predicted. What remains is the missed optimisation, which for a tiny body in a tight loop can be most of the cost.
Question 3 of 3
Which of these lets the compiler call the function directly?
final guarantees no further overrides, so with the static type known the target is fixed. override only checks the signature; a base pointer still needs dispatch.
In the interview
Say your own answer out loud first, then open the one we would give.
How expensive is a virtual function call?
The dispatch itself is two dependent loads and an indirect jump. When the vtables are hot and the call site keeps hitting the same target, that is a few cycles. The larger cost is usually that the compiler cannot inline the function, so it cannot optimise the caller and callee together. It gets worse when one call site sees many types in an unpredictable order.
How do you avoid virtual dispatch on the hot path?
If the type is known at compile time, make the hot component a template on it, or use CRTP. If there is a small closed set of types, a variant visited at the call site, or one container per type. Marking classes final helps the compiler devirtualise where the type is visible. I keep virtual interfaces for the parts that run rarely.
Where does the vptr live and what does it cost in memory?
In mainstream implementations it is the first pointer-sized field of the object, so every polymorphic object is eight bytes larger. For small objects stored in large arrays that is real memory traffic, another reason to keep hot data in plain structs.