现代C++可以免费获得性能吗?

ala*_*rge 205 c++ performance move-semantics c++11 c++14

有时声称即使仅编译C++ 98代码,C++ 11/14也可以提高性能.理由通常是移动语义,因为在某些情况下,rvalue构造函数是自动生成的,或者现在是STL的一部分.现在我想知道这些案例以前是否已经由RVO或类似的编译器优化处理过了.

那么我的问题是,如果你能给我一个C++ 98代码的实际例子,使用支持新语言特性的编译器,无需修改就可以更快地运行.我确实理解标准符合编译器不需要执行复制省略,因此移动语义可能会带来速度,但我希望看到一个较少病态的案例,如果你愿意的话.

编辑:为了清楚,我不是在问新的编译器是否比旧的编译器更快,而是如果有代码将-std = c ++ 14添加到我的编译器标志它会运行得更快(避免副本,但如果你除了移动语义之外还能提出其他任何东西,我也会感兴趣)

Yak*_*ont 221

我知道重新编译C++ 03编译器的5个常规类别,因为C++ 11可以导致无限的性能提升,实际上与实现质量无关.这些都是移动语义的变体.

std::vector 重新分配

struct bar{
  std::vector<int> data;
};
std::vector<bar> foo(1);
foo.back().data.push_back(3);
foo.reserve(10); // two allocations and a delete occur in C++03
Run Code Online (Sandbox Code Playgroud)

每次时间foo的缓冲器被在C++ 03重新分配它复制每个vectorbar.

在C++ 11中它改为移动bar::datas,这基本上是免费的.

在这种情况下,这依赖于std容器内的优化vector.在下面的每种情况下,std容器的使用仅仅是因为它们是C++对象,move在升级编译器时"自动" 地在C++ 11 中具有高效的语义.不阻止包含std容器的对象也会继承自动改进的move构造函数.

NRVO失败

当NRVO(名为返回值优化)失败时,在C++ 03中它回落到副本上,在C++ 11上它又回落到移动状态.NRVO的失败很简单:

std::vector<int> foo(int count){
  std::vector<int> v; // oops
  if (count<=0) return std::vector<int>();
  v.reserve(count);
  for(int i=0;i<count;++i)
    v.push_back(i);
  return v;
}
Run Code Online (Sandbox Code Playgroud)

甚至:

std::vector<int> foo(bool which) {
  std::vector<int> a, b;
  // do work, filling a and b, using the other for calculations
  if (which)
    return a;
  else
    return b;
}
Run Code Online (Sandbox Code Playgroud)

我们有三个值 - 返回值,以及函数中的两个不同值.Elision允许函数中的值与返回值"合并",但不能彼此合并.它们都不能与返回值合并而不相互合并.

基本问题是NRVO缺陷是脆弱的,并且具有不在return站点附近的变化的代码可以突然在该点处具有大量性能降低而没有诊断发射.在大多数NRVO失败的情况下,C++ 11最终得到一个move,而C++ 03最终得到一个副本.

返回一个函数参数

Elision在这里也是不可能的:

std::set<int> func(std::set<int> in){
  return in;
}
Run Code Online (Sandbox Code Playgroud)

在C++ 11中这很便宜:在C++ 03中没有办法避免副本.函数的参数不能用返回值来省略,因为参数和返回值的生命周期和位置由调用代码管理.

但是,C++ 11可以从一个移动到另一个.(在较少玩具的例子中,可能会做一些事情set).

push_back 要么 insert

最后,不会发生对容器的省略:但C++ 11会重载rvalue移动插入操作符,从而节省了副本.

struct whatever {
  std::string data;
  int count;
  whatever( std::string d, int c ):data(d), count(c) {}
};
std::vector<whatever> v;
v.push_back( whatever("some long string goes here", 3) );
Run Code Online (Sandbox Code Playgroud)

在C++ 03 whatever中创建临时,然后将其复制到向量中v.std::string分配2个缓冲区,每个缓冲区具有相同的数据,并丢弃一个缓冲区.

在C++ 11 whatever中创建了一个临时的.然后whatever&& push_back超载move暂时进入向量v.std::string分配一个缓冲区,并移动到向量中.空的std::string被丢弃.

分配

从@ Jarod42的答案中偷来了.

分配时不能进行Elision,但移动可以.

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();
Run Code Online (Sandbox Code Playgroud)

这里some_function返回一个候选者来自,但因为它不是用来直接构造一个对象,所以不能省略它.在C++ 03中,上面的结果是临时被复制到的内容some_value.在C++ 11中,它被移入some_value,基本上是免费的.


为了达到上述目的,您需要一个能够为您合成移动构造函数和赋值的编译器.

MSVC 2013在std容器中实现移动构造函数,但不会在类型上合成移动构造函数.

所以包含std::vectors和类似的类型在MSVC2013中没有得到这样的改进,但是它们将开始在MSVC2015中获得它们.

clang和gcc早就实现了隐式移动构造函数.如果你通过,英特尔的2013编译器将支持隐式生成移动构造函数-Qoption,cpp,--gen_move_operations(默认情况下它们并不是为了与MSVC2013交叉兼容).

  • 我只看到1个移动语义的一般类别,以及5个特殊情况. (26认同)
  • 那是一个糟糕的优化器实现,因为返回的命名不同的对象没有重叠的生命周期,理论上RVO仍然是可能的. (5认同)
  • @sebro我了解,您不会认为“导致程序不分配千字节的千字节分配,而是四处移动指针”就足够了。您需要定时结果。与从根本上做得少的证明相比,微基准测试不再是性能改进的证明。现实世界中的任务剖析不足以说明众多行业中的数百个现实世界应用程序,这并不是真正的证明。我对“免费性能”含糊其词,并让他们就C ++ 03和C ++ 11下程序行为的差异提供了具体的事实。 (3认同)
  • @alarge在某些地方,省略会失败,例如可以将两个生命周期重叠的对象消除到第三个对象中,而不能互相消除。然后在C ++ 11中需要移动,并在C ++ 03中复制(忽略as-if)。在实践中,省略通常很脆弱。上面`std`容器的使用主要是因为在重新编译C ++ 03时,将它们免费转移到C ++ 11中“免费”获得的复制类型很便宜。`vector :: resize`是一个例外:它在C ++ 11中使用`move`。 (2认同)

Jar*_*d42 46

如果你有类似的东西:

std::vector<int> foo(); // function declaration.
std::vector<int> v;

// some code

v = foo();
Run Code Online (Sandbox Code Playgroud)

你在C++ 03中得到了一个副本,而你在C++ 11中得到了一个移动赋值.所以你可以在这种情况下进行免费优化.

  • @Yakk:如何在作业中发生复制? (4认同)
  • 在这种情况下,好的C++ 03代码已经通过`foo().swap(v);`进行了移动 (4认同)
  • @ Jarod42我也相信复制省略在分配中是不可能的,因为左侧已经构建,并且没有合理的方法让编译器在从右侧窃取资源后知道如何处理"旧"数据手边.但也许我错了,我很乐意一劳永逸地找到答案.复制构造时复制省略是有意义的,因为对象是"新鲜的"并且没有决定如何处理旧数据的问题.据我所知,唯一的例外是:"分配只能基于as-if规则来省略" (2认同)