Quant developer HFT course · Module 1: Reading hot code
Cache lines, locality and pointer chasing
The question
You said the vector is faster because nearby memory is cheaper to read. Why is nearby memory cheaper?
Memory comes in lines
The core never reads a single byte from main memory. It reads the 64-byte line that contains the byte and copies the whole line into its caches. Every later access to any byte in that line is served from the cache, which is many times faster than going back to memory.
Three levels of cache are usual on server parts: a small L1 per core, a larger L2 per core, and a large L3 shared by the cores on a chip. Each level is bigger and slower than the one before it, and main memory is slower again by a wide margin. Module 5 goes through the hierarchy in detail. For reading code, one rule carries most of the weight: the cost of a load depends on where the line is when you ask for it.
Where the quotes fall
Our Quote is 24 bytes: four for the instrument, four for the sequence number, eight for the price, four for the quantity, one for the side, and three bytes of padding so that the next quote's price stays 8-byte aligned. Laid end to end in a vector, the lines fall like this:
A 64-byte line holds two and two-thirds quotes. Walking the vector, roughly one iteration in three or so triggers a fetch, and the others are served by a line already brought in. Some quotes straddle two lines, which is harmless when you are reading every quote in order, since the second line is needed next anyway.
Two kinds of locality
Spatial locality is using data that sits next to data you have just used. Contiguous arrays have it; a structure whose fields you read together has it if those fields are close together.
Temporal locality is using the same data again soon. A small lookup table that every iteration consults stays in L1 as long as nothing evicts it.
Code that has both runs from cache almost all the time. Hot-path code is usually written to have both on purpose: the data an event needs is packed together and stays resident between events.
Pointer chasing
The worst access pattern is one where the address of the next load is the result of the current one:
struct Node { Quote q; Node* next; };
std::int64_t total_qty(const Node* n) {
std::int64_t t = 0;
while (n) { t += n->q.qty; n = n->next; } // next address comes from this load
return t;
}
A modern core can have many loads in flight at once and keeps working on independent instructions while it waits. Here there is nothing independent to do: the loop cannot know where node k + 1 is until node k arrives. If every node misses the cache, the loop runs at the speed of memory latency, one round trip per node.
Each bar is one load that misses the cache. Array addresses are known in advance, so the CPU overlaps the misses; a list gives the next address only when the current load returns.
-
Step 1 / 4
Array: four loads are issued back to back, because their addresses are known in advance. List: node 0 is requested.
-
Step 2 / 4
All four array loads are done, overlapped. The list has only just received node 0, and only now learns where node 1 is.
-
Step 3 / 4
The list waits a full round trip for node 1 before it can ask for node 2.
-
Step 4 / 4
Three list nodes cost three memory round trips, one after another.
The array version is not just faster because each line serves several elements. Its loads do not depend on each other, so even the misses overlap.
Where pointer chasing hides
Linked lists are the obvious case. Less obvious ones show up all the time in interview code:
std::mapandstd::set: every step down the tree is a dependent load.std::unordered_map: with the standard library's node-based design, a lookup goes to the bucket array and then to a node that can be anywhere.- A
std::vector<std::unique_ptr<Order>>: the vector is contiguous, but the orders are not. - Virtual dispatch: object, then vtable, then code, three dependent loads before the function starts.
The fix is the same in every case: put the data the hot path reads in contiguous storage, and replace pointers with indices into that storage where you can.
Check yourself
Question 1 of 3
An int at address 0x1000 has just been loaded from main memory. Which of these loads is most likely to be served from the cache? Assume 64-byte lines and that nothing else has happened.
0x1000 to 0x103F is one 64-byte line, so 0x1038 came in with the first load. 0x1040 is the first byte of the next line. The hardware prefetcher may fetch that line too, but nothing guarantees it.
Question 2 of 3
Why is the linked-list loop limited by memory latency rather than memory bandwidth?
Bandwidth is how much data per second the memory system can deliver if you keep it busy. A dependent chain keeps only one request outstanding at a time, so you pay the full round trip on every node.
Question 3 of 3
Which container gives contiguous storage for the objects themselves?
Only the vector of values stores the orders themselves contiguously. A vector of pointers is a contiguous array of pointers to objects that can be anywhere.
In the interview
Say your own answer out loud first, then open the one we would give.
What is a cache line and why does it matter?
It is the unit the memory system moves between memory and the caches, 64 bytes on mainstream server CPUs. Loading one byte brings the whole line in, so data you read next to it is effectively free. Laying out hot data contiguously, and in the order it is read, is how you turn that into speed.
What is pointer chasing?
A sequence of loads where each address comes from the previous load: walking a list, a tree, or a chain of objects. The CPU cannot overlap those loads, so each cache miss costs a full trip to memory. Arrays and index-based structures avoid it because their addresses can be computed in advance.
You have a std::vector of unique_ptr to orders. Is that cache-friendly?
The pointers are contiguous; the orders are not. Iterating touches the pointer array in order and then jumps to each order wherever it was allocated. If the hot path walks all orders, I would store them by value in the vector, or in a pool, and hand out indices instead of pointers.