Quant developer HFT course · Module 1: Reading hot code
The cost model, all at once
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.
Building the symbol string and the log string both allocate (the log line certainly; the symbol if it exceeds the small buffer). Passing the quote by reference and devirtualising do not change allocation.
Question 2 of 3
What does for (auto s : strategies_) cost when strategies_ is a vector of shared_ptr?
Copying a shared_ptr copies the pointer and bumps the shared count atomically; destroying the copy decrements it. Atomic read-modify-writes are not free, and they bounce cache lines between threads if other threads touch the same counts.
Question 3 of 3
Why mention that passing Quote by value is fine?
A strong review ranks issues. Saying what you would leave alone, and why, is part of that.
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.