std::vector range constructor vs initializer list vs reserve() & push_back performance benchmark

There are several ways to populate a std::vector in C++ if you know the number of elements in advance, such as in

data.cpp
const double data[10] = {1,2,3,4,5,6,7,8,9,10};

Using range constructor

vector_range_constructor.cpp
std::vector<double> vec1(data, data + 10);

Using initializer list

vector_initializer_list.cpp
std::vector<double> vec2 = {data[0], data[1], data[2], data[3], data[4],
                             data[5], data[6], data[7], data[8], data[9]};

Using reserve() and push_back()

vector_reserve_push_back.cpp
std::vector<double> vec3;
vec3.reserve(10);
for (size_t i = 0; i < 10; ++i) {
    vec3.push_back(data[i]);
}

Benchmark results

On Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz, compiled using

compile.sh
g++ --std=c++17 -o vector_benchmark vector_benchmark.cpp -march=native  -O3

the benchmark results are as follows:

SizeRange CtorPush BackInitializer
10.0233655 s0.0255561 s0.0298313 s
100.0207084 s0.0282417 s0.0210478 s
1000.0360425 s0.119358 s0.040467 s
10000.18665 s0.980142 sN/A
100001.89109 s15.4342 sN/A

Observations:

Hence, we can conclude that for performance-critical applications where the size is known in advance, using the range constructor is the preferred method to populate a std::vector. This is particularly true for larger datasets, and also helps to keep the code clean and concise.

Yet, I encourage you to download the benchmark source code below and run it on your own hardware to verify these results.

Benchmark source code

vector_benchmark.cpp
#include <vector>
#include <array>
#include <chrono>
#include <iostream>
#include <random>
#include <iomanip>
#include <functional>
#include <cassert>

// Fixed test data
const std::array<int, 10000> data = []{
    std::array<int, 10000> arr{};
    std::mt19937 gen(42);
    std::uniform_int_distribution<> dist(0, 1000);
    for (auto& val : arr) {
        val = dist(gen);
    }
    return arr;
}();

// Variant 1: Range constructor
std::vector<double> variant1(size_t N) {
    return std::vector<double>(data.begin(), data.begin() + N);
}

// Variant 2: Push back with reserve
std::vector<double> variant2(size_t N) {
    std::vector<double> out;
    out.reserve(N);
    for (size_t i = 0; i < N; ++i) {
        out.push_back(static_cast<double>(data[i]));
    }
    return out;
}

// Actual initializer list variants for different sizes
std::vector<double> variant3_1() {
    return {static_cast<double>(data[0])};
}

std::vector<double> variant3_10() {
    return {
        static_cast<double>(data[0]), static_cast<double>(data[1]),
        static_cast<double>(data[2]), static_cast<double>(data[3]),
        static_cast<double>(data[4]), static_cast<double>(data[5]),
        static_cast<double>(data[6]), static_cast<double>(data[7]),
        static_cast<double>(data[8]), static_cast<double>(data[9])
    };
}

std::vector<double> variant3_100() {
    return {
        static_cast<double>(data[0]), static_cast<double>(data[1]), static_cast<double>(data[2]), static_cast<double>(data[3]),
        static_cast<double>(data[4]), static_cast<double>(data[5]), static_cast<double>(data[6]), static_cast<double>(data[7]),
        static_cast<double>(data[8]), static_cast<double>(data[9]), static_cast<double>(data[10]), static_cast<double>(data[11]),
        static_cast<double>(data[12]), static_cast<double>(data[13]), static_cast<double>(data[14]), static_cast<double>(data[15]),
        static_cast<double>(data[16]), static_cast<double>(data[17]), static_cast<double>(data[18]), static_cast<double>(data[19]),
        static_cast<double>(data[20]), static_cast<double>(data[21]), static_cast<double>(data[22]), static_cast<double>(data[23]),
        static_cast<double>(data[24]), static_cast<double>(data[25]), static_cast<double>(data[26]), static_cast<double>(data[27]),
        static_cast<double>(data[28]), static_cast<double>(data[29]), static_cast<double>(data[30]), static_cast<double>(data[31]),
        static_cast<double>(data[32]), static_cast<double>(data[33]), static_cast<double>(data[34]), static_cast<double>(data[35]),
        static_cast<double>(data[36]), static_cast<double>(data[37]), static_cast<double>(data[38]), static_cast<double>(data[39]),
        static_cast<double>(data[40]), static_cast<double>(data[41]), static_cast<double>(data[42]), static_cast<double>(data[43]),
        static_cast<double>(data[44]), static_cast<double>(data[45]), static_cast<double>(data[46]), static_cast<double>(data[47]),
        static_cast<double>(data[48]), static_cast<double>(data[49]), static_cast<double>(data[50]), static_cast<double>(data[51]),
        static_cast<double>(data[52]), static_cast<double>(data[53]), static_cast<double>(data[54]), static_cast<double>(data[55]),
        static_cast<double>(data[56]), static_cast<double>(data[57]), static_cast<double>(data[58]), static_cast<double>(data[59]),
        static_cast<double>(data[60]), static_cast<double>(data[61]), static_cast<double>(data[62]), static_cast<double>(data[63]),
        static_cast<double>(data[64]), static_cast<double>(data[65]), static_cast<double>(data[66]), static_cast<double>(data[67]),
        static_cast<double>(data[68]), static_cast<double>(data[69]), static_cast<double>(data[70]), static_cast<double>(data[71]),
        static_cast<double>(data[72]), static_cast<double>(data[73]), static_cast<double>(data[74]), static_cast<double>(data[75]),
        static_cast<double>(data[76]), static_cast<double>(data[77]), static_cast<double>(data[78]), static_cast<double>(data[79]),
        static_cast<double>(data[80]), static_cast<double>(data[81]), static_cast<double>(data[82]), static_cast<double>(data[83]),
        static_cast<double>(data[84]), static_cast<double>(data[85]), static_cast<double>(data[86]), static_cast<double>(data[87]),
        static_cast<double>(data[88]), static_cast<double>(data[89]), static_cast<double>(data[90]), static_cast<double>(data[91]),
        static_cast<double>(data[92]), static_cast<double>(data[93]), static_cast<double>(data[94]), static_cast<double>(data[95]),
        static_cast<double>(data[96]), static_cast<double>(data[97]), static_cast<double>(data[98]), static_cast<double>(data[99])
    };
}

// Benchmark function
template<typename Func>
double benchmark(Func func, size_t iterations) {
    auto start = std::chrono::high_resolution_clock::now();
    for (size_t i = 0; i < iterations; ++i) {
        volatile auto result = func();
        (void)result;
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> elapsed = end - start;
    return elapsed.count();
}

int main() {
    const std::vector<size_t> sizes = {1, 10, 100, 1000, 10000};
    const size_t iterations = 1'000'000;

    std::cout << "Benchmarking vector initialization methods\n";
    std::cout << "=========================================\n";
    std::cout << std::setw(8) << "Size" << " | "
              << std::setw(20) << "Range Ctor" << " | "
              << std::setw(20) << "Push Back" << " | "
              << std::setw(20) << "Initializer\n";
    std::cout << "-----------------------------------------\n";

    for (size_t N : sizes) {
        // Warm-up
        for (int i = 0; i < 3; ++i) {
            variant1(N);
            variant2(N);
            if (N == 1) variant3_1();
            else if (N == 10) variant3_10();
            else if (N == 100) variant3_100();
        }

        // Benchmark
        double time1 = benchmark([N]{ return variant1(N); }, iterations);
        double time2 = benchmark([N]{ return variant2(N); }, iterations);
        double time3 = 0.0;

        if (N == 1) time3 = benchmark(variant3_1, iterations);
        else if (N == 10) time3 = benchmark(variant3_10, iterations);
        else if (N == 100) time3 = benchmark(variant3_100, iterations);

        // Print results
        std::cout << std::setw(8) << N << " | "
                  << std::setw(18) << time1 << " s | "
                  << std::setw(18) << time2 << " s | ";

        if (N <= 100) {
            std::cout << std::setw(18) << time3 << " s";
        } else {
            std::cout << std::setw(18) << "N/A";
        }
        std::cout << "\n";

        // Verify all variants produce the same result
        auto r1 = variant1(N);
        auto r2 = variant2(N);

        if (r1 != r2) {
            std::cerr << "Error: Variants produce different results for size " << N << "!\n";
            return 1;
        }

        // Verify initializer list for supported sizes
        if (N <= 100) {
            std::vector<double> r3;
            if (N == 1) r3 = variant3_1();
            else if (N == 10) r3 = variant3_10();
            else if (N == 100) r3 = variant3_100();

            if (r1.size() != r3.size() || !std::equal(r1.begin(), r1.end(), r3.begin())) {
                std::cerr << "Error: Initializer list produces different results for size " << N << "!\n";
                return 1;
            }
        }
    }

    return 0;
}

Check out similar posts by category: C/C++