Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Quant developer HFT course · Module 1: Reading hot code

Virtual functions and vtables

Lesson · 10 min

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:

loadload, then callobjectvptrfields…vtable of Momentum[0] ~Momentum[1] on_quote[2] on_fillMomentum::on_quote
A virtual call: read the object's hidden pointer, read the slot in its table, jump to the address found there. Two dependent loads and an indirect jump, and the compiler cannot see through any of them to inline the body.
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::variant and std::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?

Question 2 of 3

A loop calls a tiny virtual function on the same object a million times. What is likely the largest cost?

Question 3 of 3

Which of these lets the compiler call the function directly?

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.