为什么这段代码使用不同的参数传递策略没有显着的性能差异?
Why no significant performance differences for this code with different param passing strategies?
我正在尝试编写一些代码并说服自己按值传递、按引用传递(rvalue
和 lvalue
参考)应该对性能有重大影响 (related question)。后来我想出了下面这段代码,我 认为 性能差异应该是可见的。
#include <iostream>
#include <vector>
#include <chrono>
#define DurationTy std::chrono::duration_cast<std::chrono::milliseconds>
typedef std::vector<int> VectTy;
size_t const MAX = 10000u;
size_t const NUM = MAX / 10;
int randomize(int mod) { return std::rand() % mod; }
VectTy factory(size_t size, bool pos) {
VectTy vect;
if (pos) {
for (size_t i = 0u; i < size; i++) {
// vect.push_back(randomize(size));
vect.push_back(i);
}
} else {
for (size_t i = 0u; i < size * 2; i++) {
vect.push_back(i);
// vect.push_back(randomize(size));
}
}
return vect;
}
long d1(VectTy vect) {
long sum = 0;
for (auto& v : vect) sum += v;
return sum;
}
long d2(VectTy& vect) {
long sum = 0;
for (auto& v : vect) sum += v;
return sum;
}
long d3(VectTy&& vect) {
long sum = 0;
for (auto& v : vect) sum += v;
return sum;
}
int main(void) {
{
auto start = std::chrono::steady_clock::now();
long total = 0;
for (size_t i = 0; i < NUM; ++i) {
total += d1(factory(MAX, i % 2)); // T1
}
auto end = std::chrono::steady_clock::now();
std::cout << total << std::endl;
auto elapsed = DurationTy(end - start);
std::cerr << elapsed.count() << std::endl;
}
{
auto start = std::chrono::steady_clock::now();
long total = 0;
for (size_t i = 0; i < NUM; ++i) {
VectTy vect = factory(MAX, i % 2); // T2
total += d1(vect);
}
auto end = std::chrono::steady_clock::now();
std::cout << total << std::endl;
auto elapsed = DurationTy(end - start);
std::cerr << elapsed.count() << std::endl;
}
{
auto start = std::chrono::steady_clock::now();
long total = 0;
for (size_t i = 0; i < NUM; ++i) {
VectTy vect = factory(MAX, i % 2); // T3
total += d2(vect);
}
auto end = std::chrono::steady_clock::now();
std::cout << total << std::endl;
auto elapsed = DurationTy(end - start);
std::cerr << elapsed.count() << std::endl;
}
{
auto start = std::chrono::steady_clock::now();
long total = 0;
for (size_t i = 0; i < NUM; ++i) {
total += d3(factory(MAX, i % 2)); // T4
}
auto end = std::chrono::steady_clock::now();
std::cout << total << std::endl;
auto elapsed = DurationTy(end - start);
std::cerr << elapsed.count() << std::endl;
}
return 0;
}
我在 gcc
(4.9.2) 和 clang
(t运行k) 上用 -std=c++11
选项测试了它。
但是我发现 only 在使用 clang T2
编译时需要更多时间(对于一个 运行,以毫秒为单位,755,924,752,750 ).我还编译了 -fno-elide-constructors
版本,但结果相似。
(更新:使用 Clang 编译时 T1
、T3
、T4
存在轻微的性能差异 (t运行k) Mac OS X.)
我的问题:
- 在理论上弥合
T1
、T2
、T3
之间的潜在性能差距的优化应用是什么? (你可以看到我在 factory
中也试图避免 RVO。)
- 在这种情况下,gcc 对
T2
应用的可能优化是什么?
这是因为 r 值引用。您按值传入 std::vector - 编译器计算出具有移动构造函数并优化要移动的副本。
有关右值引用的详细信息,请参阅以下 link:http://thbecker.net/articles/rvalue_references/section_01.html
更新:
以下三种方法是等价的:
在这里,你直接在函数d1
中传入工厂的return,编译器知道值returned是一个临时值,std::vector (VectTy)
有一个移动构造函数定义 - 它只是调用那个移动构造函数(所以这个函数等同于 d3
long d1(VectTy vect) {
long sum = 0;
for (auto& v : vect) sum += v;
return sum;
}
这里你是通过引用传递的,所以没有复制- OTOH,这不应该编译。除非你使用的是 MSVC——在那种情况下你应该禁用语言扩展
long d2(VectTy& vect) {
long sum = 0;
for (auto& v : vect) sum += v;
return sum;
}
当然这里不会有任何副本,您正在将临时向量(右值)从工厂移动到 d3
long d3(VectTy&& vect) {
long sum = 0;
for (auto& v : vect) sum += v;
return sum;
}
如果您想重现复制性能问题,请尝试推出您自己的矢量 class:
template<class T>
class MyVector
{
private:
std::vector<T> _vec;
public:
MyVector() : _vec()
{}
MyVector(const MyVector& other) : _vec(other._vec)
{}
MyVector& operator=(const MyVector& other)
{
if(this != &other)
this->_vec = other._vec;
return *this;
}
void push_back(T t)
{
this->_vec.push_back(t);
}
};
并使用它代替 std::vector
,您肯定会遇到您正在寻找的性能问题
类型 vector<T>
的右值将被另一个 vector<T>
窃取,如果您试图从它构造第二个 vector<T>
。如果你赋值,它可能被盗,或者它的内容可能被移动,或者其他什么(它在标准中未指定)。
从相同的右值类型构造称为移动构造。对于一个向量,(在大多数实现中)它包括读取 3 个指针、写入 3 个指针和清除 3 个指针。这是一个廉价的操作,无论向量拥有多少数据。
factory
中没有任何内容可以阻止 NRVO(一种省略)。无论如何,当您 return 局部变量(在 C++11 中完全匹配 return 值类型,或在 C++14 中找到兼容的右值构造函数)时,它被隐式视为右值如果省略不发生。因此 factory
中的参数将被 return 值省略,或者移动其内容。成本上的差异是微不足道的,任何差异都可以通过优化消除。
你的三个函数 d1
d2
和 d3
应该更好地称为 "by-value", "by-lvalue" 和 "by-rvalue".
调用 L1 将 return 值省略到 d1
的参数中。如果此省略失败(假设您阻止了它),它将变成一个移动构造,这会更加昂贵。
调用 L2 强制复制。
调用 L3 没有副本,L4 也没有。
现在,在 as-if 规则下,如果你能证明它没有副作用,你甚至可以跳过副本(或者,更准确地说,如果消除它是标准下可能发生的情况的有效变体) . gcc 可能正在这样做,这可能解释了为什么 L2 不慢。
无意义任务基准测试的问题在于,在 as-if 下,一旦编译器可以证明该任务无意义,它就可以将其消除。
但我对 L1 L3 和 L4 相同并不感到惊讶,因为标准要求它们在成本上基本相同,最多有一些指针洗牌。
我正在尝试编写一些代码并说服自己按值传递、按引用传递(rvalue
和 lvalue
参考)应该对性能有重大影响 (related question)。后来我想出了下面这段代码,我 认为 性能差异应该是可见的。
#include <iostream>
#include <vector>
#include <chrono>
#define DurationTy std::chrono::duration_cast<std::chrono::milliseconds>
typedef std::vector<int> VectTy;
size_t const MAX = 10000u;
size_t const NUM = MAX / 10;
int randomize(int mod) { return std::rand() % mod; }
VectTy factory(size_t size, bool pos) {
VectTy vect;
if (pos) {
for (size_t i = 0u; i < size; i++) {
// vect.push_back(randomize(size));
vect.push_back(i);
}
} else {
for (size_t i = 0u; i < size * 2; i++) {
vect.push_back(i);
// vect.push_back(randomize(size));
}
}
return vect;
}
long d1(VectTy vect) {
long sum = 0;
for (auto& v : vect) sum += v;
return sum;
}
long d2(VectTy& vect) {
long sum = 0;
for (auto& v : vect) sum += v;
return sum;
}
long d3(VectTy&& vect) {
long sum = 0;
for (auto& v : vect) sum += v;
return sum;
}
int main(void) {
{
auto start = std::chrono::steady_clock::now();
long total = 0;
for (size_t i = 0; i < NUM; ++i) {
total += d1(factory(MAX, i % 2)); // T1
}
auto end = std::chrono::steady_clock::now();
std::cout << total << std::endl;
auto elapsed = DurationTy(end - start);
std::cerr << elapsed.count() << std::endl;
}
{
auto start = std::chrono::steady_clock::now();
long total = 0;
for (size_t i = 0; i < NUM; ++i) {
VectTy vect = factory(MAX, i % 2); // T2
total += d1(vect);
}
auto end = std::chrono::steady_clock::now();
std::cout << total << std::endl;
auto elapsed = DurationTy(end - start);
std::cerr << elapsed.count() << std::endl;
}
{
auto start = std::chrono::steady_clock::now();
long total = 0;
for (size_t i = 0; i < NUM; ++i) {
VectTy vect = factory(MAX, i % 2); // T3
total += d2(vect);
}
auto end = std::chrono::steady_clock::now();
std::cout << total << std::endl;
auto elapsed = DurationTy(end - start);
std::cerr << elapsed.count() << std::endl;
}
{
auto start = std::chrono::steady_clock::now();
long total = 0;
for (size_t i = 0; i < NUM; ++i) {
total += d3(factory(MAX, i % 2)); // T4
}
auto end = std::chrono::steady_clock::now();
std::cout << total << std::endl;
auto elapsed = DurationTy(end - start);
std::cerr << elapsed.count() << std::endl;
}
return 0;
}
我在 gcc
(4.9.2) 和 clang
(t运行k) 上用 -std=c++11
选项测试了它。
但是我发现 only 在使用 clang T2
编译时需要更多时间(对于一个 运行,以毫秒为单位,755,924,752,750 ).我还编译了 -fno-elide-constructors
版本,但结果相似。
(更新:使用 Clang 编译时 T1
、T3
、T4
存在轻微的性能差异 (t运行k) Mac OS X.)
我的问题:
- 在理论上弥合
T1
、T2
、T3
之间的潜在性能差距的优化应用是什么? (你可以看到我在factory
中也试图避免 RVO。) - 在这种情况下,gcc 对
T2
应用的可能优化是什么?
这是因为 r 值引用。您按值传入 std::vector - 编译器计算出具有移动构造函数并优化要移动的副本。
有关右值引用的详细信息,请参阅以下 link:http://thbecker.net/articles/rvalue_references/section_01.html
更新: 以下三种方法是等价的:
在这里,你直接在函数d1
中传入工厂的return,编译器知道值returned是一个临时值,std::vector (VectTy)
有一个移动构造函数定义 - 它只是调用那个移动构造函数(所以这个函数等同于 d3
long d1(VectTy vect) {
long sum = 0;
for (auto& v : vect) sum += v;
return sum;
}
这里你是通过引用传递的,所以没有复制- OTOH,这不应该编译。除非你使用的是 MSVC——在那种情况下你应该禁用语言扩展
long d2(VectTy& vect) {
long sum = 0;
for (auto& v : vect) sum += v;
return sum;
}
当然这里不会有任何副本,您正在将临时向量(右值)从工厂移动到 d3
long d3(VectTy&& vect) {
long sum = 0;
for (auto& v : vect) sum += v;
return sum;
}
如果您想重现复制性能问题,请尝试推出您自己的矢量 class:
template<class T>
class MyVector
{
private:
std::vector<T> _vec;
public:
MyVector() : _vec()
{}
MyVector(const MyVector& other) : _vec(other._vec)
{}
MyVector& operator=(const MyVector& other)
{
if(this != &other)
this->_vec = other._vec;
return *this;
}
void push_back(T t)
{
this->_vec.push_back(t);
}
};
并使用它代替 std::vector
,您肯定会遇到您正在寻找的性能问题
类型 vector<T>
的右值将被另一个 vector<T>
窃取,如果您试图从它构造第二个 vector<T>
。如果你赋值,它可能被盗,或者它的内容可能被移动,或者其他什么(它在标准中未指定)。
从相同的右值类型构造称为移动构造。对于一个向量,(在大多数实现中)它包括读取 3 个指针、写入 3 个指针和清除 3 个指针。这是一个廉价的操作,无论向量拥有多少数据。
factory
中没有任何内容可以阻止 NRVO(一种省略)。无论如何,当您 return 局部变量(在 C++11 中完全匹配 return 值类型,或在 C++14 中找到兼容的右值构造函数)时,它被隐式视为右值如果省略不发生。因此 factory
中的参数将被 return 值省略,或者移动其内容。成本上的差异是微不足道的,任何差异都可以通过优化消除。
你的三个函数 d1
d2
和 d3
应该更好地称为 "by-value", "by-lvalue" 和 "by-rvalue".
调用 L1 将 return 值省略到 d1
的参数中。如果此省略失败(假设您阻止了它),它将变成一个移动构造,这会更加昂贵。
调用 L2 强制复制。
调用 L3 没有副本,L4 也没有。
现在,在 as-if 规则下,如果你能证明它没有副作用,你甚至可以跳过副本(或者,更准确地说,如果消除它是标准下可能发生的情况的有效变体) . gcc 可能正在这样做,这可能解释了为什么 L2 不慢。
无意义任务基准测试的问题在于,在 as-if 下,一旦编译器可以证明该任务无意义,它就可以将其消除。
但我对 L1 L3 和 L4 相同并不感到惊讶,因为标准要求它们在成本上基本相同,最多有一些指针洗牌。