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

The cost model, all at once

Lesson · 12 min

The question

Here is our quote handler. Take five minutes, tell me everything that worries you, and tell me what you would fix first.

The handler

This is the kind of code you will be shown. It works, it is readable, and it is slow in at least six places:

struct Book {
    std::map<std::int64_t, std::int32_t> bids, asks;   // price -> total quantity
};

class Engine {
    std::unordered_map<std::string, Book> books_;
    std::vector<std::shared_ptr<Strategy>> strategies_;
    std::function<void(std::string)> log_;
public:
    void on_quote(Quote q) {
        std::string sym = symbol_of(q.instrument);
        Book& b = books_[sym];
        auto& side = q.side == 0 ? b.bids : b.asks;
        if (q.qty == 0) side.erase(q.price); else side[q.price] = q.qty;
        for (auto s : strategies_) s->on_book(b, q);
        log_("quote " + sym + " " + std::to_string(q.price));
    }
};

Read it once more before going on, and list what you would say.

The findings, in order of size

1. Logging on the hot path. Every quote builds a string and calls through a std::function, which probably formats and writes. The string allocates, and whatever log_ does may take a lock or make a system call. This is often the largest single cost and the easiest to remove: write a fixed-size binary record to a ring buffer and let another thread format it (Modules 4 and 8).

2. A string key per quote. symbol_of builds a std::string from the instrument id, and the map then hashes that string. The feed already gives us an integer. Key the books by instrument id, or better, map ids to dense indices at start-up and keep the books in a vector.

3. The book is two std::maps. Every update is a tree walk through separately allocated nodes, and every new price level allocates. Prices sit on a tick grid near the touch, so an array indexed by price offset from a reference level, or a small sorted vector, gives contiguous storage (Module 9).

4. Copying shared_ptr in the loop. for (auto s : strategies_) copies each shared_ptr, which is an atomic increment and decrement on the reference count per strategy per quote. const auto& removes it. Owning the strategies with unique_ptr, or by value, removes the question.

5. A virtual call per strategy. If the strategy types are known when the engine is built, templates or a variant remove the dispatch and let small strategies inline. If there is one strategy per process, final on it may be enough.

6. Passing Quote by value. For a 24-byte trivially copyable struct this is cheap; mention it only to say it is fine. Knowing what not to fix is part of the answer.

There are smaller points: books_[sym] silently inserts a new book for an unknown symbol, which allocates and hides a bug; side[q.price] = q.qty inserts on update even when the level should already exist.

What to fix first

Rank by expected cost and by certainty. Logging and the string key are allocations and possible system calls on every quote; they come first. The book structure is the deepest change and needs a measurement to justify it. The shared_ptr copy is a one-character fix. The virtual call needs measuring before it is worth a redesign.

And say how you would confirm it: a benchmark on recorded quotes before and after each change, with the allocation count on the hot path checked to be zero.

The rewrite, in outline

class Engine {
    std::vector<Book> books_;                 // indexed by dense instrument index
    std::vector<std::uint32_t> index_of_;     // exchange id -> dense index, built at start-up
    Momentum strategy_;                       // concrete type: direct, inlinable calls
    LogRing& log_;                            // fixed-size records to a logging thread
public:
    void on_quote(const Quote& q) {
        Book& b = books_[index_of_[q.instrument]];
        b.apply(q.side, q.price, q.qty);      // array-backed levels near the touch
        strategy_.on_book(b, q);
        log_.push(LogRec{LogRec::Quote, q.instrument, q.seq, q.price, q.qty});
    }
};

No allocation, no string, no tree, no type erasure, one direct call. Each change maps back to one of the questions from the first lesson, which is how you should present it.

Check yourself

Question 1 of 3 · choose every correct option

Which change to the handler removes an allocation on every quote? Choose all that apply.

Question 2 of 3

What does for (auto s : strategies_) cost when strategies_ is a vector of shared_ptr?

Question 3 of 3

Why mention that passing Quote by value is fine?

In the interview

Say your own answer out loud first, then open the one we would give.

Review this handler for latency. What do you change first?

I look for work per message that is unbounded or can block: allocations, logging, string building, locks and system calls. Those come first. Then data structure choices on the hot path, such as tree-based maps and string keys. Then dispatch and copies. For each change I would say how I expect it to show up in a measurement.

How do you decide which of several issues matters most?

By the size of the cost and how often it is paid. A system call or allocation per message dominates anything measured in cycles. Then I confirm with a profile on realistic input, since a reading of the code is a hypothesis.

What would you leave alone?

Anything cheap and clear: small trivially copyable values passed by value, an occasional predictable branch, a virtual call on a path that runs once at start-up. Changing those makes the code harder to read for no measurable gain.