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 3: Containers and memory

vector: size, capacity and growth

Lesson · 9 min

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:

  1. allocate a new buffer, larger by a constant factor;
  2. move (or copy, see Module 2) every element into it;
  3. destroy the old elements and free the old buffer.

libstdc++ and libc++ double the capacity; MSVC grows by one and a half times.

112234445868788891610161116121613161416151616161732capacity after each push_back (doubling)
Capacity after each push_back with a growth factor of two. Red bars are reallocations: allocate, move every element, free the old buffer. They get rarer as the vector grows, so the average cost per push is constant, but each one is a spike.

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 for n elements once. Pushes up to n never 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::array with 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 if n exceeds 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.
  • insert and erase in the middle shift every later element. For small trivially copyable elements this is a fast memmove; 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++?

Question 2 of 3

Which of these keeps a reused vector from allocating on every message?

Question 3 of 3

Why is "amortised O(1)" not enough for a hot path?

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.