现代C ++可以免费为您提供性能吗?


205

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

然后我的问题是,是否可以给我一个C ++ 98代码的实际示例,该示例无需修改即可使用支持新语言功能的编译器更快地运行。我确实理解不需要标准兼容的编译器来执行复制省略,仅由于这个原因,移动语义可能会带来速度,但是如果您愿意,我希望看到一种病态较少的情况。

编辑:为了清楚起见,我不是在问新的编译器是否比旧的编译器快,而是如果有代码将-std = c ++ 14添加到我的编译器标志中,它将运行得更快(避免复制,但是如果您除了移动语义之外,还可以提出其他建议,我也很感兴趣)


3
请记住,使用复制构造函数构造新对象时,将执行复制省略和返回值优化。但是,在复制赋值运算符中,没有复制省略符(怎么回事,因为编译器不知道如何处理已经构造的对象不是临时对象)。因此,在这种情况下,C ++ 11/14通过为您提供使用移动分配运算符的可能性而大获成功。但是,关于您的问题,我认为如果由C ++ 11/14编译器编译,C ++ 98代码应该不会更快,也许是因为编译器较新,所以它会更快。
vsoftco 2014年

27
即使使用标准库与C ++ 98完全兼容,使用标准库的代码也可能更快,因为在C ++ 11/14中,基础库在可能的情况下使用内部移动语义。因此,无论哪种情况,只要您使用标准库对象(例如向量,列表等)并移动语义,在C ++ 98和C ++ 11/14中看起来完全相同的代码都会(可能)更快。
vsoftco

1
@vsoftco,这就是我所提到的那种情况,但无法举一个例子:从我记得必须定义副本构造函数的角度来看,将不会自动生成move构造函数,这使我们有了我认为RVO总是很简单的类。异常可能与STL容器有关,其中rvalue构造函数由库实现程序生成(这意味着我不必为使用移动而在代码中进行任何更改)。
2014年

类不需要简单即可没有副本构造函数。C ++在价值语义上蒸蒸日上,复制构造函数,赋值运算符,析构函数等应为例外。
sp2danny

1
@Eric谢谢您的链接,这很有趣。但是,快速浏览后,它的速度优势似乎主要来自添加std::move和移动构造函数(这需要对现有代码进行修改)。与我的问题真正相关的唯一一句话是“您只需通过重新编译即可立即获得速度优势”,这没有任何示例的支持(确实在同一张幻灯片中提到了STL,就像我在问题中所做的那样,但没有具体说明) )。我在问一些例子。如果我看错幻灯片,请告诉我。
2014年

Answers:


221

我知道有5个常规类别,其中将C ++ 03编译器重新编译为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

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

在C ++ 11中,它移动bar::datas,而s基本上是免费的。

在这种情况下,这取决于stdcontainer 内的优化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;
}

甚至:

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;
}

我们有三个值-返回值和函数内的两个不同值。Elision允许将函数内的值与返回值“合并”,但不能相互合并。如果没有合并,它们都不能与返回值合并。

基本的问题是NRVO省略是脆弱的,并且不在return站点附近进行更改的代码会突然在该位置大幅降低性能,而不会发出诊断信息。在大多数NRVO失败的情况下,C ++ 11的结尾为move,而C ++ 03的结尾为一个副本。

返回函数参数

此处的省略也是不可能的:

std::set<int> func(std::set<int> in){
  return in;
}

在C ++ 11中,这很便宜:在C ++ 03中,无法避免复制。返回值不能忽略函数的参数,因为参数的寿命和位置以及返回值由调用代码管理。

但是,C ++ 11可以从一个迁移到另一个。(在一个不那么玩具的示例中,可以对进行某些操作set)。

push_back 要么 insert

最终省略到容器中不会发生:但是C ++ 11重载了右值移动插入运算符,从而保存了副本。

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) );

在C ++ 03中,将whatever创建一个临时文件,然后将其复制到vector中vstd::string分配了2个缓冲区,每个缓冲区具有相同的数据,而其中一个被丢弃。

在C ++ 11中,将whatever创建一个临时目录。whatever&& push_back然后move,将重载临时添加到向量中vstd::string分配了一个缓冲区,并将其移入向量。空将std::string被丢弃。

分配

从@ Jarod42的答案中被盗。

分配不能发生省略,但可以移动。

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();

这里some_function返回一个候选对象,但由于它不用于直接构造对象,因此不能被忽略。在C ++ 03中,以上结果导致临时内容被复制到中some_value。在C ++ 11中,它已移入some_value,基本上是免费的。


为了充分发挥上述作用,您需要一个可以为您合成move构造函数和赋值的编译器。

MSVC 2013在std容器中实现move构造函数,但不会在您的类型上合成move构造函数。

因此,包含std::vectors和类似内容的类型在MSVC2013中不会得到这种改进,但是会在MSVC2015中开始得到它们。

clang和gcc早就实现了隐式move构造函数。如果您通过,英特尔2013编译器将支持隐式生成move构造函数-Qoption,cpp,--gen_move_operations(默认情况下,为了与MSVC2013交叉兼容,它们不会这样做)。


1
@大是的。但是,要使移动构造函数的效率比复制构造函数高很多倍,通常必须移动资源而不是复制资源。无需编写自己的移动构造函数(只需重新编译C ++ 03程序),std库容器都将使用move“免费”构造函数进行更新,并且(如果您未阻止,则)使用上述对象的构造函数(和上述物件)在许多情况下将开始进行自由移动构造。在C ++ 03中,很多情况都包含在省略中:并非全部。
Yakk-Adam Nevraumont 2014年

5
那是一个糟糕的优化器实现,然后,由于返回的名称不同的对象没有重叠的生存期,因此从理论上讲RVO仍然可行。
Ben Voigt 2014年

2
@alarge在某些情况下,省略会失败,例如可以将两个生命周期重叠的对象消除到第三个对象中,而不能互相消除。然后在C ++ 11中需要移动,并在C ++ 03中复制(忽略as-if)。在实践中,省略通常很脆弱。std上面容器的使用主要是因为在重新编译C ++ 03时,将它们免费转移到C ++ 11中“免费”获得的复制类型很便宜。的vector::resize是一个例外:它使用move在C ++ 11。
Yakk-Adam Nevraumont 2014年

27
我只看到1个常规类别,即移动语义,以及5种特殊情况。
Johannes Schaub-litb 2014年

3
@sebro我了解,您不会认为“导致程序不分配千字节的千字节分配,而是将指针四处移动”就足够了。您需要定时结果。与从根本上做不到的证明相比,微基准测试不再是性能改进的证明。现实世界中的任务剖析不足以说明各种行业中的100个现实世界应用程序,这并不是真正的证明。我对“免费性能”提出了模糊的主张,并让他们就C ++ 03和C ++ 11下程序行为的差异提供了具体的事实。
Yakk-Adam Nevraumont

46

如果您有类似以下内容:

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

// some code

v = foo();

您在C ++ 03中获得了一个副本,而在C ++ 11中获得了一个移动分配。这样您就可以进行免费优化。


4
@Yakk:如何在分配中进行复制省略?
Jarod42 2014年

2
@ Jarod42我还认为,在作业中不可能进行复制省略,因为左侧已经构建好了,编译器没有合理的方法从右手窃取资源后知道如何处理“旧”数据手侧。但是也许我错了,我很乐意找出一劳永逸的答案。当您复制构造时,复制省略是有意义的,因为对象是“新鲜”的,并且没有决定如何处理旧数据的问题。据我所知,唯一的例外是:“只能根据
当下

4
好的C ++ 03代码在这种情况下已经采取了行动,通过foo().swap(v);
Ben Voigt 2014年

@BenVoigt可以肯定,但是并非所有代码都已优化,并非所有容易发生这种情况的地方。
Yakk-Adam Nevraumont 2014年

复制省略号可以在工作中使用,如@BenVoigt所说。更好的术语是RVO(返回值优化),并且只有在像这样实现foo()时才有效。
DrumM
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.