Benchmark de rendimiento: constructor por rango de std::vector vs initializer list vs reserve() & push_back

Existen varias formas de llenar un std::vector en C++ si conoces el número de elementos de antemano, como por ejemplo en

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

Usando el constructor por rango

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

Usando 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]};

Usando reserve() y 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]);
}

Resultados del benchmark

En un Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz, compilado usando

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

los resultados del benchmark son los siguientes:

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

Observaciones:

Por lo tanto, podemos concluir que para aplicaciones críticas en rendimiento donde el tamaño se conoce de antemano, usar el constructor por rango es el método preferido para llenar un std::vector. Esto es particularmente cierto para conjuntos de datos más grandes, y también ayuda a mantener el código limpio y conciso.

Sin embargo, te animo a descargar el código fuente del benchmark a continuación y ejecutarlo en tu propio hardware para verificar estos resultados.

Código fuente del benchmark

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

// Datos de prueba fijos
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;
}();

// Variante 1: Constructor por rango
std::vector<double> variant1(size_t N) {
    return std::vector<double>(data.begin(), data.begin() + N);
}

// Variante 2: Push back con 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;
}

// Variantes reales de initializer list para diferentes tamaños
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])
    };
}

// Función de benchmark
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) {
        // Calentamiento
        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);

        // Imprimir resultados
        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";

        // Verificar que todas las variantes producen el mismo resultado
        auto r1 = variant1(N);
        auto r2 = variant2(N);

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

        // Verificar initializer list para los tamaños soportados
        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;
}

Echa un vistazo a artículos similares por categoría: C/C++