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

时间:2009-03-04 16:11:28

标签: c++ performance string concatenation

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

13 个答案:

答案 0 :(得分:79)

额外的工作可能不值得,除非你真的需要效率。你可能只需要使用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)

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

为什么你可以提高效率:

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

实施注意事项:

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

绳索数据结构:

如果您需要非常快速的连接,请考虑使用rope data structure

答案 1 :(得分:70)

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

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

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

答案 2 :(得分:16)

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

a + " : " + c

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

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

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

(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的移动语义(补充复制语义)和rvalue引用作为实验性添加。这允许确定参数是否引用临时参数。这将使得这样的添加速度惊人地快,因为上述所有内容将最终出现在一个没有副本的“添加管道”中。

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

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

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

答案 3 :(得分:11)

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

答案 4 :(得分:7)

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

答案 5 :(得分:5)

或许是std :: stringstream?

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

答案 6 :(得分:4)

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

这种想法已在STLport std :: string实现中实现 - 由于这种精确的黑客攻击而不符合标准。

答案 7 :(得分:3)

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

另一方面,

std::string appendoperator+=会在每次字符串需要增长时将容量提高50%。这显着减少了内存分配和复制操作的数量,O(log n)。

答案 8 :(得分:2)

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

我更喜欢std :: ostringstream用于复杂的连接。

答案 9 :(得分:2)

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

如果你想将大字符串输出到GUI,可能是你输出的任何内容都可以比一个大字符串更好地处理字符串(例如,在文本编辑器中连接文本 - 通常它们保持不变线作为单独的结构)。

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

如果我从慢速代码中删除不必要的连接,我从未发现需要更快地进行连接。

答案 10 :(得分: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  |  

答案 11 :(得分:0)

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

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

答案 12 :(得分: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)), ...);
}