C++字符串拼接优化

4

看这样的代码(已添加注释):

std::string some_var;
std::string some_func(); // both are defined, but definition is irrelevant
...
return "some text " + some_var + "c" + some_func(); // intentionally "c" not 'c'

我在想,std::stringoperator +在哪些情况下需要进行复制(即使用复制构造/赋值,而不是内部缓冲区被复制,例如如果应用了SSO),以及实际上复制了什么。快速查看cppreference只有部分帮助,因为它列出了12种(!)不同的情况。部分原因是我想确认我对该页面的理解:
  • 情况1) 复制lhs,然后将rhs复制到此副本的末尾
  • 在C++98情况2) - 5)中,从char/const char*参数构造了一个临时字符串,随后结果变为情况1)
  • 在C++11情况2) - 5)中,从char/const char*参数构造了一个临时字符串,随后结果为情况6)或7)
  • 在C++11情况6) - 12)中,r-value参数将通过insert/append进行变异,并且如果提供了char/const char*参数,则由于对insert/append的重载而不需要临时值。在所有情况下,返回r-value以便于进一步链接。不进行任何复制(除了要附加/插入到插入位置的参数的副本)。字符串的内容可能需要移动。
如上例所示,链式操作的结果应该是: 2) -> 6) -> 11) -> 8),不会复制任何lhs,只会修改第一个操作(创建temp-string的r-value缓冲区)的r-value结果的缓冲区。因此,一旦operator +使用至少一个r-value参数,它似乎与operator +=一样有效。这正确吗?在C++11及以后的版本中,除非两个参数都是l-value字符串,否则还有使用operator +=的意义吗?
编译器还可以进行哪些优化?请澄清问题意图。初始部分仅涉及语言的具体细节(不考虑实现);最后一个问题是关于额外的优化。

我修正了我认为是一个打字错误的问题。如果你的意思是 some_fun(),请回滚。 - Bathsheba
这并不是真正的打字错误,但我猜你的版本在C ++环境中更清晰。 - midor
使用 g++ -save-temps 进行编译,并查看不同优化级别下生成的汇编代码输出,这是非常有益的。在 -O3 优化级别下,您的代码将调用 string::reserve() 一次和 string::append() 四次。 - G. Sliepen
更简单的是:使用编译器浏览器。 - MikeMB
我认为,老实说,我更关心重新分配复制字符串时的性能,而不是通过值返回,因为如前所述,后者很可能可以被相当好地优化。 - Mark B
重申一下,我在一个答案中评论的是:我不太关心效率,更关心语义,(...)。我更关心调用哪个版本的函数,而不是现在最终有多快。重新分配可能会发生在其中,这与问题本身无关,因为除非您知道大小并为内容保留空间,否则无法避免。 - midor
1个回答

0

字符串是一个相当不透明的对象:它保存了一个内部字符缓冲区并以其希望的方式进行管理。向字符串添加单个字符可能会导致分配新缓冲区,初始部分的复制和添加部分的复制。所有这些都取决于分配的缓冲区是否足够大以接受添加的部分。

引文如下:

……没有进行任何复制(除了要附加/插入到插入位置的参数的副本)。字符串的内容可能需要移动

换句话说,需要进行新的分配、完全复制和旧缓冲区的释放……

而当你谈论效率和优化时,必须记住编译器不必遵循你编写程序的方式。由于“as-if”规则,它可以优化它想要的方式,只要遵守可观察行为即可。C++标准如下所述:

1.9 程序执行 [intro.execution]
...
5 执行良好的符合实现应该产生与相应的抽象机器实例的可能执行之一相同的可观察行为,具有相同的程序和相同的输入。

一条注释甚至解释了这一点:

实现可以忽略国际标准的任何要求,只要程序的可观察行为表现得好像已经遵守了该要求一样。
因此,a = a + b;a += b;很可能编译成完全相同的代码。
当你编写C++程序时,不必担心低级别的优化问题:编译器会处理这些问题,通常说“编译器比你更聪明”。只有在确定存在真正的瓶颈时才采用这种方式,并且要知道低级别的优化只适用于一个编译器、一个架构和一个配置。

我对效率并不是太在意,更关心语义是否正确。所以这个说法是正确的,虽然对我来说并不是什么新鲜事物,但它并没有回答我的问题。现在,我更关心调用了哪些版本的函数,而不是最终的速度有多快。重新分配内存的事实与问题本身无关,因为除非你知道内容的大小并为其保留空间,否则无法避免重新分配。 - midor
“因此,a = a + b; 和 a += b; 很可能被编译成完全相同的代码。”你真的检查过吗?我非常怀疑任何编译器都能够进行这种优化(除非a是一个临时变量)。 - MikeMB
在我看来,a = a + b 至少需要一个额外的移动赋值操作。最好的情况(假设可能发生),编译器意识到 a 即将被重新分配,因此它的值不再需要,并将其视为右值,这将导致第6种情况),从而在右值上进行追加。然而,生成的右值仍然需要赋值给 a,我没有看到任何应用 RVO 的方法,因为 append 没有返回新对象。 - midor

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