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


108

我听到一些人对std :: string中的“ +”运算符表示担忧,并提出了各种解决方法以加快连接速度。这些真的有必要吗?如果是这样,在C ++中串联字符串的最佳方法是什么?


13
基本上,+不是一个concatentation运算符(因为它会生成一个新字符串)。使用+ =进行串联。
马丁·约克

1
从C ++ 11开始,有一个重要的观点:operator +可以修改其操作数之一,如果该操作数是通过右值引用传递的,则可以按操作返回。libstdc++ 例如这样做。因此,当使用临时对象调用operator +时,它可以实现几乎一样好的性能-出于可读性考虑,也许赞成默认使用该参数,除非有基准表明它是瓶颈。但是,标准化的可变参数append()将是最佳可读的...
underscore_d

Answers:


85

除非您确实需要效率,否则额外的工作可能不值得。 仅使用运算符+ =可能会带来更高的效率。

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

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)

您可以看到每个+之后都会返回一个新对象。这意味着每次都使用一个新的缓冲区。如果您要执行大量额外的+操作,则效率不高。

为什么可以提高效率:

  • 您是在保证效率,而不是信任委托人为您高效地做到这一点
  • std :: string类对字符串的最大大小一无所知,也不知道连接它的频率。您可能具有此知识,并且可以根据此信息来做事。这将导致较少的重新分配。
  • 您将手动控制缓冲区,这样可以确保当您不希望将整个字符串复制到新缓冲区中时,可以确保不会将其复制到新缓冲区中。
  • 您可以将堆栈用于缓冲​​区而不是堆,这样效率更高。
  • string +运算符将创建一个新的字符串对象并使用新的缓冲区返回它。

实施注意事项:

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

绳索数据结构:

如果确实需要快速串联,请考虑使用绳索数据结构


6
注意:“ STL”指的是一个完全独立的开源库,最初是由HP提供的,其中一部分用作ISO标准C ++库的一部分的基础。“的std :: string”,但是,从来没有惠普的STL的一部分,所以这是完全错误的参考“STL和‘串’起来。
詹姆斯·柯伦

1
我不会说同时使用STL和字符串是错误的。参见sgi.com/tech/stl/table_of_contents.html
Brian R. Bondy 2009年

1
当SGI从HP接管STL的维护时,对其进行了改装以匹配标准库(这就是为什么我说“ HP STL的任何部分”)。但是,std :: string的创建者是ISO C ++委员会。
James Curran

2
旁注:负责维护STL多年的SGI员工是Matt Austern,他同时领导ISO C ++标准化委员会的图书馆小组。
James Curran

4
您能否澄清一下,或者指出为什么您可以将堆栈用于缓冲​​区而不是更有效的堆??效率差异来自何处?
h7r13年

76

保留最后的空间,然后将append方法与缓冲区一起使用。例如,假设您希望最终的字符串长度为一百万个字符:

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

while (whatever)
{
  s.append(buf,len);
}

17

我不会担心。如果循环执行,字符串将始终预分配内存以最大程度地减少重新分配-仅operator+=在这种情况下使用。如果您手动执行此操作,则可能需要更长的时间

a + " : " + c

然后它会创建临时文件-即使编译器可以消除一些返回值副本。这是因为在连续调用中,operator+它不知道引用参数是引用命名对象还是子operator+调用返回的临时对象。我宁愿不用先介绍一下就不必担心。但是,让我们举一个例子来说明这一点。我们首先引入括号以使绑定清楚。我将参数直接放在用于清晰起见的函数声明之后。在此之下,我显示了结果表达式是什么:

((a + " : ") + c) 
calls string operator+(string const&, char const*)(a, " : ")
  => (tmp1 + c)

现在,此外,tmp1还有第一次调用operator +显示的参数返回的内容。我们假设编译器确实很聪明,并且优化了返回值副本。因此,我们最终得到了一个新字符串,其中包含aand 的串联" : "。现在,这发生了:

(tmp1 + c)
calls string operator+(string const&, string const&)(tmp1, c)
  => tmp2 == <end result>

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

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

它对临时名称字符串使用相同的功能!因此,编译器必须将参数复制到新字符串中并追加到该字符串中,然后从中将其返回operator+。它不能占用临时存储器并附加到该临时存储器。表达式越大,必须完成的字符串副本越多。

下一步Visual Studio和GCC将支持c ++ 1x的移动语义(补充复制语义)和右值引用作为实验性的补充。这样就可以弄清楚参数是否引用了临时变量。这将使这样的添加速度惊人地快,因为以上所有内容最终都将成为一个没有副本的“添加管道”。

如果结果是瓶颈,您仍然可以

 std::string(a).append(" : ").append(c) ...

append调用参数追加到*this,然后返回一个引用到自己。因此,此处没有临时副本的复制。或者,operator+=也可以使用,但是您需要使用难看的括号来固定优先级。


我必须检查stdlib实现者是否确实做到了这一点。:P libstdc++operator+(string const& lhs, string&& rhs)return std::move(rhs.insert(0, lhs))。然后,如果两者都是临时的,则它operator+(string&& lhs, string&& rhs)是否lhs具有足够的可用容量将直接append()operator+=如果lhs没有足够的容量,我认为这可能会变慢,然后回落到rhs.insert(0, lhs),这不仅必须扩展缓冲区并添加新内容,例如append(),而且还需要沿rhs权利的原始内容转移。
underscore_d

与之相比的另一项开销operator+=operator+仍必须返回一个值,因此它必须move()附加到它所附加的任何操作数上。不过,与深度复制整个字符串相比,我想这还是一个相当小的开销(复制几个指针/大小),所以很好!
underscore_d

11

对于大多数应用程序,这无关紧要。只需编写您的代码,就很高兴地不知道+运算符的工作原理,并且只有当它成为明显的瓶颈时,才由您自己处理。


7
当然,在大多数情况下不值得,但这并不能真正回答他的问题。
Brian R. Bondy

1
是的 我同意只是说“配置文件然后优化”可以作为对此问题的评论:)
Johannes Schaub-litb

6
从技术上讲,他问这些是否“必要”。他们不是,这回答了这个问题。
萨曼莎·布兰纳姆

足够公平,但是对于某些应用程序绝对是必需的。因此,在这些应用程序中,答案简化为:“将事情交到您自己手中”
Brian R. Bondy

4
@Pesto在编程世界中有一个变态的概念,即性能并不重要,我们可以忽略整个过程,因为计算机的速度越来越快。问题是,这不是人们使用C ++进行编程的原因,也不是为什么他们在堆栈溢出中发布有关有效字符串连接的问题。
MrFox

7

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


2
特别是如果您在开始之前使用reserve()使缓冲区足够大以容纳结果。
Mark Ransom

我认为他是在谈论operator + =。尽管这是一个简陋的案例,但它也是串联的。詹姆斯是一个vc ++ mvp,所以我希望他对c ++有一些了解:p
Johannes Schaub-litb

1
我毫不怀疑他具有丰富的C ++知识,只是对这个问题有误解。该问题询问了operator +的效率,该函数每次调用时都会返回新的字符串对象,因此会使用新的char缓冲区。
Brian R. Bondy

1
是的 但随后他要求操作员+的速度慢,最好的方法是进行串联。在这里,operator + =进入了游戏。但我同意詹姆斯的回答有点短。它听起来像我们所有人都可以使用operator +一样,它是最高效率的:p
Johannes Schaub-litb

@ BrianR.Bondy operator+不必返回新字符串。如果该操作数是通过右值引用传递的,则实现者可以返回其操作数之一(已修改)。libstdc++ 例如这样做。因此,在operator+使用临时对象进行调用时,它可以实现相同或几乎相同的性能-这可能是赞成默认设置的另一种说法,除非有人有基准表明它代表瓶颈。
underscore_d


4

Imperfect C ++中,马修·威尔逊(Matthew Wilson)提出了一种动态字符串连接器,该功能可预先计算最终字符串的长度,以便在连接所有部分之前仅分配一次。我们还可以通过使用表达式模板来实现静态连接器。

这种想法已经在STLport std :: string实现中实现-由于这种精确的破解,因此不符合标准。


Glib::ustring::compose()从glibmm绑定到GLib的操作是:reserve()根据提供的格式字符串和varargs 估计和s最终长度,然后将append()每个(或其格式化替换)循环。我希望这是一种非常普遍的工作方式。
underscore_d

4

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

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


我不太确定为什么要拒​​绝这种投票。标准不要求50%的数字,但IIRC或100%是实践中常见的增长指标。这个答案中的所有其他内容似乎都令人反对。
underscore_d

几个月后,我想这还不是那么准确,因为它是在C ++ 11首次发布后很久才写的,并且重载operator+右值引用传递一个或两个参数的位置的重载可以避免通过串联到现有的缓冲区中来分配新字符串。操作数之一(如果容量不足,则可能必须重新分配)。
underscore_d

2

对于较小的字符串,没关系。如果您有很大的字符串,则最好将它们存储在矢量或其他集合中,作为它们的一部分。并添加您的算法以使用这样的数据集而不是一个大字符串。

我更喜欢std :: ostringstream进行复杂的串联。


2

与大多数事情一样,不做某事比做某事更容易。

如果要将大字符串输出到GUI,则可能是要输出的任何内容都比大字符串更好地处理字符串(例如,在文本编辑器中连接文本-通常它们将行分开结构)。

如果要输出到文件,请流式传输数据,而不要创建一个大字符串并输出。

如果我从慢速代码中删除了不必要的连接,我从未发现有必要使连接更快变得必要。


2

如果在结果字符串中预分配(保留)空间,则可能是最佳性能。

template<typename... Args>
std::string concat(Args const&... args)
{
    size_t len = 0;
    for (auto s : {args...})  len += strlen(s);

    std::string result;
    result.reserve(len);    // <--- preallocate result
    for (auto s : {args...})  result += s;
    return result;
}

用法:

std::string merged = concat("This ", "is ", "a ", "test!");

0

封装在一个类中的简单字符数组是最快的,该类跟踪数组的大小和分配的字节数。

诀窍是在开始时只进行一次大分配。

https://github.com/pedro-vicente/table-string

基准测试

对于Visual Studio 2015,x86调试版本比C ++ std :: string有了实质性的改进。

| API                   | Seconds           
| ----------------------|----| 
| SDS                   | 19 |  
| std::string           | 11 |  
| std::string (reserve) | 9  |  
| table_str_t           | 1  |  

1
OP对如何有效串联感兴趣std::string。他们不要求替代字符串类。
underscore_d

0

您可以尝试对每个项目进行内存保留的方法:

namespace {
template<class C>
constexpr auto size(const C& c) -> decltype(c.size()) {
  return static_cast<std::size_t>(c.size());
}

constexpr std::size_t size(const char* string) {
  std::size_t size = 0;
  while (*(string + size) != '\0') {
    ++size;
  }
  return size;
}

template<class T, std::size_t N>
constexpr std::size_t size(const T (&)[N]) noexcept {
  return N;
}
}

template<typename... Args>
std::string concatStrings(Args&&... args) {
  auto s = (size(args) + ...);
  std::string result;
  result.reserve(s);
  return (result.append(std::forward<Args>(args)), ...);
}
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.