C++中的高效字符串连接

sne*_*neg 97 c++ string performance concatenation

我听到一些人表达了对std :: string中"+"运算符的担忧以及加速连接的各种变通方法.这些都真的有必要吗?如果是这样,在C++中连接字符串的最佳方法是什么?

Bri*_*ndy 84

额外的工作可能不值得,除非你真的需要效率. 只需使用operator + =,您可能会有更好的效率.

现在在免责声明之后,我将回答你的实际问题......

STL字符串类的效率取决于您正在使用的STL的实现.

您可以通过c内置函数手动连接来保证效率更好地控制自己.

为什么operator +效率不高:

看看这个界面:

template <class charT, class traits, class Alloc>
basic_string<charT, traits, Alloc>
operator+(const basic_string<charT, traits, Alloc>& s1,
          const basic_string<charT, traits, Alloc>& s2)
Run Code Online (Sandbox Code Playgroud)

您可以看到每个+后都返回一个新对象.这意味着每次都使用新的缓冲区.如果你正在做大量的额外+操作,那就没有效率了.

为什么你可以提高效率:

  • 您保证效率,而不是相信代表有效地为您做到这一点
  • std :: string类对字符串的最大大小一无所知,也不知道你连接它的频率.您可能拥有此知识,并且可以根据获取此信息来执行操作.这将减少重新分配.
  • 您将手动控制缓冲区,这样您就可以确保在不希望发生这种情况时不会将整个字符串复制到新的缓冲区中.
  • 您可以将堆栈用于缓冲​​区而不是堆,这样可以提高效率.
  • string +运算符将创建一个新的字符串对象,并使用新的缓冲区返回它.

实施的考虑因素:

  • 跟踪字符串长度.
  • 保持指向字符串末尾和开头的指针,或者只是开始,并使用start + the length作为偏移量来查找字符串的结尾.
  • 确保存储字符串的缓冲区足够大,因此您无需重新分配数据
  • 使用strcpy而不是strcat,因此您不需要遍历字符串的长度来查找字符串的结尾.

绳索数据结构:

如果您需要非常快速的连接,请考虑使用绳索数据结构.

  • 注意:"STL"是指一个完全独立的开源库,最初由HP提供,其中一部分用作ISO标准C++库部分的基础.但是,"std :: string"从来都不是HP STL的一部分,所以将"STL和"字符串"引在一起是完全错误的. (6认同)
  • 你能否澄清或给出一些原因**你可以使用堆栈作为缓冲区而不是堆更高效.**?这种效率差异来自哪里? (4认同)
  • 附注:负责维护STL多年的SGI员工是Matt Austern,同时也是ISO C++标准化委员会的图书馆小组的负责人. (2认同)

Car*_*rra 74

之前保留最后一个空格,然后使用带缓冲区的append方法.例如,假设您希望最终的字符串长度为100万个字符:

std::string s;
s.reserve(1000000);

while (whatever)
{
  s.append(buf,len);
}
Run Code Online (Sandbox Code Playgroud)


Joh*_*itb 16

我不担心.如果你在循环中执行它,字符串将始终预分配内存以最小化重新分配 - 只是operator+=在这种情况下使用.如果你手动完成,这样或更长时间

a + " : " + c
Run Code Online (Sandbox Code Playgroud)

然后它正在创造临时性 - 即使编译器可以消除一些返回值副本.这是因为在连续调用时operator+它不知道引用参数是引用命名对象还是从子operator+调用返回的临时对象.在没有首先进行分析之前,我宁愿不担心它.但让我们举一个例子来证明这一点.我们首先引入括号以使绑定清晰.我将参数直接放在用于清晰的函数声明之后.在下面,我展示了结果表达式是什么:

((a + " : ") + c) 
calls string operator+(string const&, char const*)(a, " : ")
  => (tmp1 + c)
Run Code Online (Sandbox Code Playgroud)

现在,在那个添加中,tmp1是第一次调用operator +并返回显示的参数.我们假设编译器非常聪明并优化了返回值副本.因此,我们最终与包含串接一个新的字符串a" : ".现在,这发生了:

(tmp1 + c)
calls string operator+(string const&, string const&)(tmp1, c)
  => tmp2 == <end result>
Run Code Online (Sandbox Code Playgroud)

将其与以下内容进行比较:

std::string f = "hello";
(f + c)
calls string operator+(string const&, string const&)(f, c)
  => tmp1 == <end result>
Run Code Online (Sandbox Code Playgroud)

它对临时和命名字符串使用相同的函数!因此编译器必须将参数复制到一个新的字符串中并附加到该字符串并从其中返回operator+.它不能记住一个临时的并追加它.表达式越大,字符串的副本就越多.

接下来,Visual Studio和GCC将支持c ++ 1x的移动语义(补充复制语义)和rvalue引用作为实验添加.这允许确定参数是否引用临时参数.这将使得这样的添加速度惊人地快,因为上述所有内容将最终出现在一个没有副本的"添加管道"中.

如果它成为瓶颈,你仍然可以做到

 std::string(a).append(" : ").append(c) ...
Run Code Online (Sandbox Code Playgroud)

append调用参数追加到*this,然后返回一个引用到自己.因此,那里没有复制临时工.或者,operator+=可以使用,但你需要丑陋的括号来修复优先级.


Pes*_*sto 11

对于大多数应用来说,这无关紧要.只需编写代码,幸福地不知道+运算符的工作原理,只有当它成为一个明显的瓶颈时才能自己动手.

  • 当然,对于大多数情况来说,这是不值得的,但这并没有真正回答他的问题. (6认同)
  • 从技术上讲,他问这些是否是"必要的".他们不是,这回答了这个问题. (6认同)
  • @Pesto在编程世界中有一个变态的概念,即性能无关紧要,我们可以忽略整个过程,因为计算机的速度越来越快。问题是,这不是人们使用C ++进行编程的原因,也不是为什么他们在堆栈溢出中发布有关有效字符串连接的问题。 (3认同)
  • 是的。我同意只是说“配置文件然后优化”可以作为对问题的评论:) (2认同)

tim*_*rov 9

std::string operator+每次分配一个新字符串并复制两个操作数字符串。重复多次,它变得昂贵,O(n)。

std::string appendoperator+=在另一方面,50%每次字符串需要成长时间撞击的能力。这显着减少了内存分配和复制操作的数量,O(log n)。

  • 几个月后,我想它不是那么准确,因为它是在 C++11 首次亮相之后很久编写的,并且 `operator+` 的重载,其中一个或两个参数通过右值引用传递,可以通过连接到操作数之一的现有缓冲区(尽管如果容量不足,它们可能必须重新分配)。 (2认同)

Jam*_*ran 7

与.NET System.Strings不同,C++的std :: strings 可变的,因此可以通过简单的连接来构建,就像通过其他方法一样快.

  • 特别是如果你在启动之前使用reserve()使缓冲区足够大以获得结果. (2认同)

Tim*_*Tim 5

也许是std :: stringstream而已?

但我同意这样的观点,即你应该保持它的可维护性和可理解性,然后分析一下你是否确实遇到了问题.

  • stringstream很慢,请参阅https://groups.google.com/d/topic/comp.lang.c++.moderated/aiFIGb6za0w (2认同)
  • @ArtemGr stringstream 可能很快,请参阅 http://www.codeproject.com/Articles/647856/Performance-Improvement-with-the-StringBuilde (2认同)

Luc*_*tte 5

《不完美的 C++》中,Matthew Wilson 提出了一种动态字符串连接器,它预先计算最终字符串的长度,以便在连接所有部分之前仅进行一次分配。我们还可以通过使用表达式模板来实现静态连接器。

这种想法已在 STLport std::string 实现中实现——由于这种精确的 hack,它不符合标准。