· Eugene Ostroukhov · Engineering · 3 min read
Avoid std::vector::reserve
std::vector::reserve usually seems like a sensible optimization. It usually is not...
Overview
The reserve method of std::vector might seem like a no-brainer optimization. It is supposed to preallocate memory for the vector so that it doesn’t have to reallocate memory as new elements are added. This should make it faster, right? Well, not quite. In this post, I’ll explain why you should avoid using reserve in most cases. I’ll also discuss specific scenarios where reserve is commonly used and why it might not be a good idea.
I’ve included the benchmark code on Godbolt, so you can check it out yourself. Unfortunately, it fails to run there (likely due to memory usage), but you can copy the code and run it locally. Godbolt link.
1. Vector length is known at compile time
std::vector is a dynamic array, designed for cases where the array size is unknown at compile time. If the size is known at compile time, you should use std::array. Unlike std::vector, std::array stores its elements inline, avoiding allocations altogether.
struct ChairWithFourLegs {
std::array<Leg, 4> legs;
};2. Vector length is known at a runtime but not at the compile time
Often, the number of elements is known when the vector is created. A common pattern is to create an empty vector and immediately call reserve to preallocate memory. This requires adding elements using push_back or emplace_back. However, even though we know there’s sufficient memory, each addition will still perform the checks.
In such cases, a better approach is to create a vector with default-initialized elements and assign values using the subscript operator:
template <size_t Count>
void BM_CreateWithReserve(benchmark::State& state) {
for (auto _ : state) {
std::vector<int> a;
a.reserve(Count);
for (size_t i = 0; i < Count; ++i) {
a.emplace_back(i);
}
benchmark::DoNotOptimize(a[20]);
}
}
template <size_t Count>
void BM_CreateWithSize(benchmark::State& state) {
for (auto _ : state) {
std::vector<int> a(Count);
for (size_t i = 0; i < Count; ++i) {
a[i] = i;
}
benchmark::DoNotOptimize(a[20]);
}
}On my machine, reserve is 30–50% slower:
| Benchmark | Time | CPU | Iterations |
|---|---|---|---|
| BM_CreateWithReserve<100> | 41.9 ns | 46.1 ns | 15160914 |
| BM_CreateWithReserve<10000> | 2530 ns | 2782 ns | 255213 |
| BM_CreateWithReserve<1000000> | 394688 ns | 434023 ns | 1619 |
| BM_CreateWithSize<100> | 30.3 ns | 32.3 ns | 21564511 |
| BM_CreateWithSize<10000> | 1812 ns | 1936 ns | 359189 |
| BM_CreateWithSize<1000000> | 190924 ns | 205347 ns | 3433 |
3. Adding elements to existing vector
Another common use case is adding multiple elements to an existing vector. For instance, when receiving data in batches on a server, you may want to append the data to a vector. The intuitive solution might be to reserve memory for the elements before appending them.
However, this is surprisingly slower than appending elements without reserving memory. By default, vectors allocate extra memory as elements are added, reducing the need for frequent reallocations. This gives vector operations an amortized complexity of O(1). When you call reserve, the complexity becomes O(n), even though it saves a small amount of memory.
template <size_t BatchSize>
void BM_Add1MBatchNoReserve(benchmark::State& state) {
for (auto _ : state) {
std::vector<int> a;
for (size_t i = 0; i < 1'000'000; i += BatchSize) {
for (size_t j = 0; j < BatchSize; ++j) {
a.emplace_back(i + j);
}
}
benchmark::DoNotOptimize(a[20]);
}
}
template <size_t BatchSize>
void BM_Add1MBatchReserve(benchmark::State& state) {
for (auto _ : state) {
std::vector<int> a;
for (size_t i = 0; i < 1'000'000; i += BatchSize) {
a.reserve(i + BatchSize);
for (size_t j = 0; j < BatchSize; ++j) {
a.emplace_back(i + j);
}
}
benchmark::DoNotOptimize(a[20]);
}
}The negative impact of reserve is especially noticeable with smaller batch sizes:
| Benchmark | Time | CPU | Iterations |
|---|---|---|---|
| BM_Add1MBatchReserve<100> | 590754271 ns | 649627700 ns | 1 |
| BM_Add1MBatchReserve<1000> | 64903617 ns | 71371330 ns | 10 |
| BM_Add1MBatchReserve<100000> | 2114290 ns | 2324970 ns | 286 |
| BM_Add1MBatchNoReserve<100> | 1514745 ns | 1665702 ns | 404 |
| BM_Add1MBatchNoReserve<1000> | 1446501 ns | 1590648 ns | 436 |
| BM_Add1MBatchNoReserve<100000> | 1450612 ns | 1594837 ns | 440 |
Conclusion
The simple advice: don’t use reserve. The nuanced advice: measure performance for your specific use case, then decide.
