为什么向量范围构造函数比填充构造函数快 10 倍?

Why is the vector range constructer 10 times faster than the fill constructor?

我测试了以下两种用 100'000 个元素填充向量的方法:

#include <iostream>
#include <vector>
#include <chrono>

using std::cout;
using std::endl;
using std::vector;
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;

int main()
{
    const int n = 100'000;

    cout << "Range constructor: " << endl;
    high_resolution_clock::time_point t0 = high_resolution_clock::now();

    int nums10[n];
    for (int i = 0; i < n; ++i) {
        nums10[i] = i;
    }
    vector<int> nums11(nums10, nums10 + n);

    high_resolution_clock::time_point t1 = high_resolution_clock::now();
    cout << "Duration: " << duration_cast<std::chrono::microseconds>(t1 - t0).count() << endl;


    cout << "Fill constructor: " << endl;
    t0 = high_resolution_clock::now();
    
    vector<int> nums1(n);
    for (int i = 0; i < n; ++i) {
        nums1[i] = i;
    }

    t1 = high_resolution_clock::now();
    cout << "Duration: " << duration_cast<std::chrono::microseconds>(t1 - t0).count() << endl;;
}

在我的例子中,范围构造函数的运行速度几乎快了 10 倍(600 微秒对 ~5000 微秒)。

为什么这里会有任何性能差异?据我了解,有等量的赋值操作。使用范围构造函数,将 100'000 个元素分配给数组,然后将它们全部复制到向量中。

这不应该与 fill 构造函数相同,其中 100'000 个元素首先默认初始化为 0,然后在 for 循环中为所有元素分配“真实”值?

Here's the compiled code on godbolt, with gcc -O0.

第一次测试中:

  • 填充数组的循环(程序集的第 49-57 行)被编译为一个简单的循环,每次迭代都会存储到内存中。它没有得到很好的优化(索引变量保存在堆栈上而不是寄存器中,并且来回冗余移动)但至少它是内联代码并且不会进行任何函数调用。

  • 范围构造函数是对库中预编译构造函数的一次调用(第 69 行)。我们可以假设库函数是通过积极优化编译的;它可能在手写汇编中调用了高度优化的 memcpy。所以它可能尽可能快。

第二次测试中:

  • 填充构造函数是一个库调用(第 113 行)。同样,这大概是尽可能快的(可能调用手动优化的 memset)。

  • 填充向量的循环(第 118-130 行)在每次迭代(第 126 行)中生成对 std::vector<int>::operator[] 的函数调用。尽管 operator[] 本身可能非常快,但经过预编译,每次函数调用的开销,包括使用其参数重新加载寄存器的代码,都是致命的。如果您使用优化进行编译,则可以内联此调用;所有这些开销都将消失,并且每次迭代您将再次只有一个存储到内存中。但是你没有优化,所以你猜怎么着?性能严重欠佳。

With optimizations 第二个测试显然更快。这是有道理的,因为它只需要将同一块内存写入两次,而无需读取;而第一个涉及写入一个内存块,然后将其读回以写入第二个块。

寓意:未经优化的代码可能非常糟糕,如果不尝试进行此类优化,可以优化成非常相似的代码的两段代码可能会截然不同。