Quant developer HFT course · Module 1: Reading hot code
Copies, moves and object layout
The question
What is sizeof this struct, and could you make it smaller without removing a field? Then: where in this function is the struct copied?
Alignment and padding
Every type has an alignment: the address of an object must be a multiple of it. On mainstream 64-bit platforms a double or int64_t aligns to 8, an int32_t to 4, a char to 1. The compiler lays fields out in declaration order and inserts padding so that each one lands on a valid address. It also pads the end of the struct, so that in an array every element's fields stay aligned.
struct A { char c1; double p; char c2; }; // sizeof 24 on x86-64
struct B { double p; char c1; char c2; }; // sizeof 16
In A, seven bytes of padding follow c1 so that p starts at offset 8, and seven more follow c2 so the size is a multiple of 8. B holds exactly the same data in 16 bytes. The rule of thumb is to order fields from the largest alignment to the smallest; offsetof and sizeof in a static_assert confirm it.
Why care about eight bytes? Because the hot path rarely reads one struct. It reads arrays of them, and a third smaller means a third fewer cache lines for the same scan. It also matters when a struct is copied into a message or a ring buffer slot, where size is bandwidth.
Hot and cold fields
Size is not the only layout question. An order object might carry fifteen fields, of which the matching path reads four: price, remaining quantity, side, and the next order at that level. The rest (client id, timestamps, the text tag) are read only for reporting. Putting the four hot fields together, at the front of the struct or in a separate array, means one cache line per order on the hot path instead of two or three. This is the same idea as the vector and the list, applied inside one object.
Copies you did not mean
The other half of object cost is copying it. Several common forms of C++ copy without saying so:
void route(Order o); // copies the order at every call
for (auto q : quotes) { ... } // copies every quote
std::vector<Order> snap = book.orders(); // copies if orders() returns by value,
// or a const& that you then copy into snap
auto [px, qty] = best_bid(); // fine if best_bid returns a small pair by value
For a 24-byte quote a copy is cheap: a few register moves. For an object holding a std::string and a std::vector, a copy allocates and copies both. The cost of a copy is the cost of copying everything the object owns.
Rules that keep copies under control:
- Pass large or owning types by
const&when the function only reads them. - Pass small, trivially copyable types (a few machine words) by value; references to them can cost more than the copy.
- Use
const auto&in range-for unless you need a copy. - When a function needs its own copy, take the parameter by value and
std::moveit into place, so callers with temporaries pay nothing (Module 2).
Seeing the layout
Two habits are worth mentioning in an interview. Put static_assert(sizeof(Quote) == 24) next to any struct that crosses a boundary (shared memory, a file, the network), so a change in layout fails the build rather than corrupting data. And use a tool such as pahole, or the compiler's own layout dump, to see the padding in a large struct.
Check yourself
Question 1 of 3
On x86-64, what is sizeof(S) for struct S { char a; int32_t b; char c; };?
b must be 4-aligned, so three bytes of padding follow a; c sits at offset 8; three more bytes pad the struct to a multiple of 4, giving 12. Reordered as int32_t b; char a; char c; it would be 8.
Question 2 of 3
Why does field order inside a struct matter for a hot-path scan over an array of them?
A 24-byte element where 16 would do means half as much again memory traffic for every scan. Grouping the fields read together also keeps one element's hot data in one line.
Question 3 of 3
Which of these makes an unintended copy of each element?
Plain auto deduces a value type, so each Order is copied, including anything it owns. The reference forms read the elements in place.
In the interview
Say your own answer out loud first, then open the one we would give.
How would you shrink this struct without removing fields?
Reorder the fields from largest alignment to smallest so the compiler needs no padding between them, and check with sizeof and offsetof in a static_assert. If some fields are rarely read, I would also consider moving them to a separate cold structure so the hot one fits in fewer cache lines.
Should I always pass by const reference?
For types that own resources or are larger than a couple of machine words, yes, when the function only reads. For small trivially copyable types such as a price, an id or a small struct of two integers, passing by value is as cheap or cheaper, and avoids aliasing questions for the compiler.
Why put a static_assert on sizeof?
Structures that go into shared memory, files or network messages must match on both sides. An assert turns an accidental layout change, such as a new field or a different compiler setting, into a build failure instead of corrupted data in production.