【问题标题】:Why no significant performance differences for this code with different param passing strategies?为什么使用不同的参数传递策略的这段代码没有显着的性能差异?
【发布时间】:2015-07-05 08:47:17
【问题描述】:

我正在尝试编写一些代码并说服自己按值传递按引用传递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;
}

我使用-std=c++11 选项在gcc(4.9.2) 和clang(trunk) 上对其进行了测试。 但是我发现 only 在使用 clang T2 编译时需要更多时间(一次运行,以毫秒为单位,755,924,752,750)。我还编译了-fno-elide-constructors 版本,但结果相似。

更新:在 Mac OS X 上使用 Clang(主干)编译时,T1T3T4 的性能略有不同。)

我的问题:

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

【问题讨论】:

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


    【解决方案1】:

    这是因为右值引用。您正在按值传递 std::vector - 编译器发现它具有移动构造函数并优化复制以移动。

    有关右值引用的详细信息,请参见以下链接: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;
    }
    

    这里你是通过引用传递的,所以没有复制 - 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;
    }
    

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

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

    【讨论】:

    • 能详细解释一下吗?
    • 对于T3,我特意创建了vect,然后传递给d2
    • 对于d1,你的意思是在像VectTy vect = temporary_vector这样的参数传递过程中,constructor是移动构造函数吗?
    • 对于您提供的包装器,我添加了begin/end 迭代器并删除了operator=,但仍然无法重现性能差异。也许编译器很聪明,发现没有副作用,优化到std::vector
    • 将您的 NUM 更改为 MAX 并重试
    【解决方案2】:

    如果您尝试从中构造第二个vector&lt;T&gt;vector&lt;T&gt; 类型的右值将被另一个vector&lt;T&gt; 窃取。如果你分配,它的内脏可能会被盗,或者它的内容可能会被移动,或者其他什么(标准中没有详细说明)。

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

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

    您的三个函数 d1 d2d3 应该更好地称为“按值”、“按左值”和“按右值”。

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

    调用 L2 强制复制。

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

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

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

    但我并不惊讶 L1 L3 和 L4 是相同的,因为标准要求它们在成本上基本相同,最多可以进行几次指针洗牌。

    【讨论】:

    • 对于factory,您的意思是 C++11/C++14 中的规则避免了复制(由于被盗或移动构造);这与 C++11 之前的标准不同;对于案例T2,您的意思是第一个构造(在T2 行)仍然避免复制,但对T1 的调用会强制复制。对吗?
    • T2 我用来指代循环体。对d1 的调用会强制在T2 注释之后的行上复制一份。提到的所有内容都假设 C++11 的基线,并加入了一些 C++14 cmets。在 C++03 中,您在 factory 中所做的也不太可能阻止 NRVO(除非没有省略标志),但是没有右值构造函数,也没有隐式的视为右值规则。
    猜你喜欢
    • 2023-04-08
    • 2012-07-28
    • 1970-01-01
    • 2012-09-22
    • 2011-08-10
    • 1970-01-01
    • 1970-01-01
    • 2014-12-01
    • 2019-07-26
    相关资源
    最近更新 更多