为什么这个代码与不同的param传递策略没有显着的性能差异?

时间:2015-04-27 03:17:11

标签: c++ performance c++11 pass-by-reference

我正在尝试编写一些代码并说服自己按值传递按引用传递rvaluelvalue参考)应该对绩效产生重大影响(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选项在clang(4.9.2)和-std=c++11(主干)上对其进行了测试。 但是我发现在使用 clang T2进行编译时需要更多时间(一次运行,以毫秒为单位,755,924,752,750)。我还编译了-fno-elide-constructors版本,但结果相似。

更新:在Mac OS X上使用Clang(主干)编译时,T1T3T4会有轻微的性能差异。)< / p>

我的问题:

  • 理论上应用哪些优化可以弥合T1T2T3之间的潜在性能差距? (你可以看到我也试图在factory中避免使用RVO。)
  • 在这种情况下,gcc对T2应用的优化是什么?

2 个答案:

答案 0 :(得分:1)

这是因为r值引用。你通过值传递std :: vector - 编译器指出它有移动构造函数并优化要移动的副本。

有关rvalue refs的详细信息,请参阅以下链接:http://thbecker.net/articles/rvalue_references/section_01.html

更新: 以下三种方法相当于:

在这里,你直接在函数d1中返回工厂,编译器知道返回的值是临时的,std::vector (VectTy)有一个移动构造函数定义 - 它只调用移动构造函数(所以这个功能相当于d3

long d1(VectTy vect) {
  long sum = 0;
  for (auto& v : vect) sum += v;
  return sum;
}

这里你通过引用传递,所以没有copy-OTOH,这不应该编译。除非你使用MSVC-在这种情况下你应该禁用语言扩展

long d2(VectTy& vect) {
  long sum = 0;
  for (auto& v : vect) sum += v;
  return sum;
}

当然这里不会有任何副本,你正在将临时矢量(rvalue)从工厂移到d3

long d3(VectTy&& vect) {
  long sum = 0;
  for (auto& v : vect) sum += v;
  return sum;
}

如果要重现复制性能问题,请尝试推出自己的矢量类:

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,您肯定会遇到您正在寻找的性能问题

答案 1 :(得分:0)

如果您尝试从中构建第二个vector<T>,则vector<T>类型的右值将被另一个vector<T>窃取。如果您指定,它可能会被盗,或者其内容可能被移动,或者其他内容(标准中未详细说明)。

从相同的右值类型构造称为移动构造。对于向量,(在大多数实现中)它包括读取3个指针,写入3个指针和清除3个指针。无论载体保存多少数据,这都是一种廉价的操作。

factory中的任何内容都不会阻止NRVO(一种省略)。无论如何,当您返回一个局部变量(在C ++ 11中与返回值类型完全匹配时,或在C ++ 14中找到兼容的rvalue构造函数)时,如果不发生省略,则将其隐式视为rvalue。因此,factory中的参数将被返回值省略,或者移动它的内容。成本的不同是微不足道的,然后无论如何都可以优化任何差异。

您的三个功能d1 d2d3应该更好地称为&#34;按值&#34;,&#34; by-lvalue&#34;和&#34; by-rvalue&#34;。

调用L1的返回值为d1的参数。如果这个省略失败(比如你阻止它),它就变成了一个移动构造,这种构造更加昂贵。

呼叫L2强制复制。

呼叫L3没有副本,L4也没有。

现在,根据as-if规则,如果你可以证明它没有任何副作用,你甚至可以跳过副本(或者更确切地说,如果消除它是标准下的有效变体,可能会发生什么)。 gcc可能正在这样做,这可能解释为什么L2不慢。

无意义任务基准测试的问题在于,如果编译器能够证明任务毫无意义,它可以消除它。

但我对L1 L3和L4是完全相同并不感到惊讶,因为标准要求它们在成本上基本相同,直到几个指针改组。