为什么向量范围构造函数比填充构造函数快 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 第二个测试显然更快。这是有道理的,因为它只需要将同一块内存写入两次,而无需读取;而第一个涉及写入一个内存块,然后将其读回以写入第二个块。
寓意:未经优化的代码可能非常糟糕,如果不尝试进行此类优化,可以优化成非常相似的代码的两段代码可能会截然不同。
我测试了以下两种用 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 第二个测试显然更快。这是有道理的,因为它只需要将同一块内存写入两次,而无需读取;而第一个涉及写入一个内存块,然后将其读回以写入第二个块。
寓意:未经优化的代码可能非常糟糕,如果不尝试进行此类优化,可以优化成非常相似的代码的两段代码可能会截然不同。