【问题标题】:Why is the vector range constructer 10 times faster than the fill constructor?为什么向量范围构造函数比填充构造函数快 10 倍?
【发布时间】:2023-03-09 05:44:02
【问题描述】:

我测试了以下两种用 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 个元素分配给数组,然后将它们全部复制到向量中。

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

【问题讨论】:

  • 如果你认为范围构造函数使用了一个简单的循环,那你可能就错了。查看汇编输出以进行验证。提示:除了完全优化的构建,不要费心对任何东西进行基准测试。调试版本会产生完全没有意义的结果。
  • 以微秒为单位处理时,性能无关紧要
  • @tadman:当然,“仅对完全优化的构建进行基准测试”的必然结果是“玩具问题通常过于简单,因此请确保编译器不会完全优化所有工作”。如果您不使用结果,或者结果以在编译时可预测的方式使用,则其中一个或两个可能在运行时根本不起作用,即使真实世界的代码确实如此必须做类似的事情(由于缺乏可预测性)。我怀疑您使用的调试版本没有内联 vectoroperator[],造成人为差异,但您没有提供构建设置。
  • @ShadowRanger 确实如此,有时您最终会对两个不同的空函数进行基准测试,实际上是在测量您的 CPU 加速和/或热调节的速度。
  • @tadman 不幸的是,我不熟悉组装。如果它没有迭代和复制范围构造函数中的值,你能解释一下实际发生了什么吗?此示例已禁用优化。我知道这种差异在绝对意义上是没有意义的,但我很好奇为什么这里会有差异。

标签: c++ performance optimization vector std


【解决方案1】:

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

在第一次测试中:

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

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

在第二次测试中:

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

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

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

道德:未经优化的代码可能真的很糟糕,如果不尝试这种优化,可以优化成非常相似的两段代码可能会变得非常不同。

【讨论】:

  • 谢谢,这就是我要找的。我没有意识到来自 [] 运算符的函数调用在这种情况下很重要。
猜你喜欢
  • 1970-01-01
  • 2021-09-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多