为什么这段代码使用不同的参数传递策略没有显着的性能差异?

Why no significant performance differences for this code with different param passing strategies?

我正在尝试编写一些代码并说服自己按值传递按引用传递(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(4.9.2) 和 clang(t运行k) 上用 -std=c++11 选项测试了它。 但是我发现 only 在使用 clang T2 编译时需要更多时间(对于一个 运行,以毫秒为单位,755,924,752,750 ).我还编译了 -fno-elide-constructors 版本,但结果相似。

(更新:使用 Clang 编译时 T1T3T4 存在轻微的性能差异 (t运行k) Mac OS X.)

我的问题:

这是因为 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 d2d3 应该更好地称为 "by-value", "by-lvalue" 和 "by-rvalue".

调用 L1 将 return 值省略到 d1 的参数中。如果此省略失败(假设您阻止了它),它将变成一个移动构造,这会更加昂贵。

调用 L2 强制复制。

调用 L3 没有副本,L4 也没有。

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

无意义任务基准测试的问题在于,在 as-if 下,一旦编译器可以证明该任务无意义,它就可以将其消除。

但我对 L1 L3 和 L4 相同并不感到惊讶,因为标准要求它们在成本上基本相同,最多有一些指针洗牌。