Quant developer HFT course · Module 3: Containers and memory
vector: size, capacity and growth
The question
push_back on a vector is amortised O(1). What does amortised hide, and how would you make it O(1) every time on a hot path?
Size and capacity
A vector has a size, the number of elements in it, and a capacity, the number it can hold before it has to allocate. push_back with size() < capacity() constructs the new element in the spare space: no allocation. With size() == capacity() it must grow:
- allocate a new buffer, larger by a constant factor;
- move (or copy, see Module 2) every element into it;
- destroy the old elements and free the old buffer.
libstdc++ and libc++ double the capacity; MSVC grows by one and a half times.
What amortised means
With geometric growth, the total cost of n pushes is O(n): the copies at each reallocation form a geometric series that sums to less than twice the final size. So the average cost per push is constant. That is what amortised O(1) means.
It says nothing about a single push. The push that triggers a reallocation of a vector of a million orders does a million moves and an allocation large enough to come straight from the kernel. On a hot path, that one push is a latency spike, and averages do not help a strategy that missed a fill during it.
Making every push cheap
reserve(n)at start-up allocates capacity fornelements once. Pushes up tonnever reallocate. Size it for the busiest day you have seen, with margin, and treat exceeding it as an event worth logging.- Reuse.
clear()destroys the elements and keeps the capacity, so a vector reused across messages stops allocating after warm-up. - Fixed capacity. If the bound is hard, use a type that cannot grow, such as
std::arraywith a count, or a fixed-capacity vector, and decide explicitly what happens when it is full. - Fail loudly. In test builds, assert that the capacity did not change in the hot path.
Other operations that touch capacity
resize(n)changes the size, constructing or destroying elements; it reallocates only ifnexceeds the capacity.shrink_to_fit()is a non-binding request to reduce the capacity to the size. Implementations do it by reallocating, so it is itself an allocation and a copy, and calling it on a reused buffer guarantees the next push reallocates.insertanderasein the middle shift every later element. For small trivially copyable elements this is a fastmemmove; for large ones it is not.
emplace_back and push_back
push_back(x) copies or moves an existing object into the vector. emplace_back(args...) constructs the element in place from constructor arguments.
std::vector<Quote> v;
v.reserve(1 << 20);
v.push_back(Quote{4711, seq, 101250, 5, 0}); // constructs a temporary, then moves it
v.emplace_back(4711, seq, 101250, 5, 0); // constructs in place (C++20 allows this for aggregates)
For a small trivially copyable struct the difference is nothing after optimisation. For types with expensive moves, or that cannot be moved, emplace_back avoids the temporary. Neither changes the allocation behaviour: both reallocate when the vector is full.
A detail interviewers like
std::vector<bool> is not a container of bool. It is specialised to pack bits, so operator[] returns a proxy object rather than a bool&, you cannot take the address of an element, and writes to different elements from different threads race because they share a byte. Use std::vector<char> or std::vector<std::uint8_t> when you want one byte per flag.
Check yourself
Question 1 of 3
A vector has size 8 and capacity 8. What does the next push_back do with libstdc++?
Growth is geometric, doubling in libstdc++ and libc++. Growing by one each time would make n pushes cost O(n²).
Question 2 of 3
Which of these keeps a reused vector from allocating on every message?
clear keeps the capacity, so later pushes reuse the buffer. shrink_to_fit releases it, which guarantees a reallocation next time.
Question 3 of 3
Why is "amortised O(1)" not enough for a hot path?
Amortised bounds average over a sequence. Latency-sensitive systems care about the worst individual operation, which reserve or a fixed capacity removes.
In the interview
Say your own answer out loud first, then open the one we would give.
What is the difference between size and capacity?
Size is the number of elements; capacity is how many the current allocation can hold. Pushing within capacity is cheap; pushing at capacity triggers a reallocation that moves everything to a larger buffer.
Why is push_back amortised constant time?
Capacity grows geometrically, so reallocations get exponentially rarer and the total copying over n pushes sums to O(n). Any single push that reallocates is O(n), though, which is why hot paths reserve capacity up front.
When would you use emplace_back?
When constructing the element from arguments avoids a temporary that is expensive to move or cannot be moved. For small trivially copyable types it makes no difference after optimisation. Neither avoids reallocation; only reserve does.