现代C++能够免费提高性能吗?

213
有时候人们声称,即使只是编译 C++98 代码,C++11/14 也能给你带来性能提升。这种说法通常是基于移动语义的,因为在某些情况下,右值构造函数会被自动生成或成为 STL 的一部分。现在我想知道这些情况之前是否已经被 RVO 或类似的编译器优化处理过了。
那么我的问题是,如果您能给我一个实际的 C++98 代码示例,而不需要修改,使用支持新语言特性的编译器运行更快。我确实理解标准符合的编译器不需要进行复制省略,仅仅因为这个原因移动语义可能会带来速度优势,但如果可以,请给我展示一个非病态案例。
编辑:为了明确起见,我并不是在问新编译器是否比旧编译器更快,而是是否有代码可以通过将 -std=c++14 添加到编译器选项中来运行得更快(避免复制),但如果您能想到除移动语义之外的其他任何东西,我也会感兴趣。

3
请记住,在使用复制构造函数构造新对象时,会执行复制省略和返回值优化。但是,在复制赋值运算符中,没有复制省略(因为编译器不知道如何处理已经构造的非临时对象)。因此,在这种情况下,C++11/14取得了巨大的胜利,因为它为您提供了使用移动赋值运算符的可能性。关于您的问题,我认为如果由C++11/14编译器编译,C++98代码不应该更快,也许之所以更快是因为编译器更新了。 - vsoftco
30
使用标准库的代码潜在地更快,即使你将其完全兼容于C++98,因为在C++11/14中,底层库在可能的情况下使用内部移动语义。因此,在使用标准库对象如向量、列表等并且移动语义有所不同的情况下,看起来在C++98和C++11/14中相同的代码将在后者中更快(可能)。 - vsoftco
1
@vsoftco,这就是我所提到的情况,但我无法举出例子:据我所记,如果我必须定义复制构造函数,则移动构造函数不会自动生成,这将使我们拥有非常简单的类,其中RVO,我认为,总是有效的。一个例外可能是与STL容器相关的东西,其中rvalue构造函数由库实现者生成(这意味着我不必更改任何代码即可使用移动)。 - alarge
类不需要简单才能没有复制构造函数。C++依赖于值语义,而复制构造函数、赋值运算符、析构函数等应该是例外。 - sp2danny
1
@Eric 谢谢你提供的链接,很有趣。然而,我快速浏览了一下,它的速度优势似乎主要来自添加 std::move 和移动构造函数(这需要修改现有代码)。与我的问题真正相关的唯一事情是“您只需重新编译即可获得即时速度优势”,但没有任何示例支持这一点(它在同一张幻灯片上提到了STL,就像我在我的问题中提到的那样,但没有具体说明)。我正在寻求一些例子。如果我误解了幻灯片,请告诉我。 - alarge
显示剩余3条评论
2个回答

229

我知道有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
每次在C++03中重新分配foo的缓冲区时,它会复制bar中的每个向量。而在C++11中,它会移动bar::data,这基本上是免费的。在这种情况下,这依赖于std容器vector内的优化。在下面的每种情况中,使用std容器仅因为它们是C++对象,在升级编译器后具有有效的移动语义。包含std容器的不阻止它的对象还继承了自动改进的move构造函数。
当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;
}
我们有三个值--返回值和函数内的两个不同值。省略允许函数内的值与返回值“合并”,但它们不能相互合并。如果没有彼此合并,它们都无法与返回值合并。
基本问题在于NRVO(命名返回值优化)很脆弱,而在离return语句不远的代码更改时,该点的性能可能会突然降低,而没有发出任何诊断。在大多数NRVO故障情况下,C++11最终使用move,而C++03最终使用copy。
返回函数参数也不可能进行省略。
std::set<int> func(std::set<int> in){
  return in;
}

在C++11中这很便宜:而在C++03中,无法避免复制。函数的参数不能通过返回值省略,因为参数和返回值的生命周期和位置由调用代码管理。

然而,C++11可以从一个对象移动到另一个对象。(在一个不太简单的例子中,可能会对set执行某些操作)。

push_backinsert

最后,容器内的省略不会发生:但是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,然后将其复制到向量v中。分配了2个std::string缓冲区,每个缓冲区中都有相同的数据,其中一个被丢弃。
在C++11中,创建一个临时的whatever。然后,push_back重载 whatever&& 将该临时对象move到向量v中。分配了一个std::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中,这基本上是免费的。


要完全实现以上效果,你需要一个可以为你合成移动构造函数和赋值操作符的编译器。

MSVC 2013 在 std 容器中实现了移动构造函数,但不会为你的类型合成移动构造函数。

因此,在 MSVC2013 中不会对包含std::vector等内容的类型进行优化,但在 MSVC2015 中将开始进行优化。

clang 和 gcc 早就实现了隐式移动构造函数。Intel 的 2013 编译器将支持隐式生成移动构造函数,如果您传递 -Qoption,cpp,--gen_move_operations (他们不会默认使用此选项以与 MSVC2013 兼容)。


5
这是一个糟糕的优化器实现,因为返回的不同命名对象生命周期没有重叠,理论上仍然可能使用RVO优化。 - Ben Voigt
1
@ildjarn,感谢您的评论,但正如我在问题中提到的那样,我不是在寻找最小限度符合编译器实现之间的收益(即没有RVO等),而是要寻找真实生活代码可能因新语言功能而加速的示例:即新的、以前无法使用的优化在不增加开发人员成本的情况下变得可能。 - alarge
2
@alarge 在一些情况下,省略操作会失败,例如当两个对象具有重叠的生命周期,可以将它们简化为第三个对象,但不能互相简化。这时需要在C++11中使用move,在C++03中使用copy(忽略as-if)。省略操作在实践中常常是脆弱的。上面使用std容器大都是因为它们是一种便宜移动、昂贵复制的类型,你可以在C++11重新编译后免费获取。 vector::resize是一个例外:在C++11中它使用move - Yakk - Adam Nevraumont
27
我只看到了一个总类,即移动语义,以及它的五个特殊情况。 - Johannes Schaub - litb
3
我明白您的意思,您认为“导致程序不分配许多千字节的内存,并转而移动指针”的表述并不足够。您想要时间上的结果。微基准测试不能证明性能改进,也不能证明您从根本上做得更少。除非有广泛行业中的几百个真实应用程序通过真实任务剖析进行了分析,否则剖析并不能真正证明性能。我将模糊的“免费性能”声明具体化,成为关于C++03和C++11下程序行为差异的具体事实。 - Yakk - Adam Nevraumont
显示剩余10条评论

47

如果你有这样的东西:

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

// some code

v = foo();

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


4
复制省略如何在赋值中发生? - Jarod42
2
@Jarod42 我也认为在赋值中不可能进行复制省略,因为左侧已经构建完成,编译器无法合理地知道如何处理从右侧窃取资源后的“旧”数据。但也许我错了,我很想一劳永逸地找到答案。当你进行复制构造时,复制省略是有意义的,因为对象是“新鲜的”,并且不存在决定如何处理旧数据的问题。据我所知,唯一的例外是:“基于 as-if 规则,只能省略赋值操作”。 - vsoftco
4
在这种情况下,良好的C++03代码已经通过foo().swap(v);进行了移动。 - Ben Voigt
@BenVoigt 当然,但并不是所有的代码都被优化了,也不是所有出现这种情况的地方都容易到达。 - Yakk - Adam Nevraumont
复制省略可以在赋值中起作用,就像@BenVoigt所说的那样。更好的术语是RVO(返回值优化),只有在foo()被实现为这样时才起作用。 - jaques-sam

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接