【问题标题】:Efficient string concatenation in C++C++ 中的高效字符串连接
【发布时间】:2010-10-11 07:24:09
【问题描述】:

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

【问题讨论】:

  • 基本上 + 不是连接运算符(因为它会生成一个新字符串)。使用 += 进行连接。
  • 从 C++11 开始,有一点很重要:如果该操作数是通过右值引用传递的,则 operator+ 可以修改其操作数之一并通过移动返回它。 libstdc++does this, for example。因此,当使用临时变量调用 operator+ 时,它可以实现几乎一样好的性能 - 出于可读性的考虑,这可能是一个支持默认它的论点,除非有基准表明它是一个瓶颈。但是,标准化的可变参数 append() 将是最佳的可读...

标签: c++ performance string concatenation


【解决方案1】:

额外的工作可能不值得,除非你真的需要效率。你可能会通过使用运算符 += 来获得更好的效率。

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

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,这样您就不需要遍历字符串的长度来查找字符串的结尾。

绳索数据结构:

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

【讨论】:

  • 注意:“STL”是指一个完全独立的开源库,最初由 HP 提供,其中一部分用作 ISO 标准 C++ 库的一部分。然而,"std::string" 从来都不是惠普 STL 的一部分,因此将 "STL" 和 "string" 一起引用是完全错误的。
  • 我不会说同时使用 STL 和字符串是错误的。见sgi.com/tech/stl/table_of_contents.html
  • 当 SGI 从 HP 接管 STL 的维护时,它被改装以匹配标准库(这就是为什么我说“从不属于 HP 的 STL”)。然而,std::string 的发起者是 ISO C++ 委员会。
  • 旁注:负责维护 STL 多年的 SGI 员工是 Matt Austern,他同时领导 ISO C++ 标准化委员会的图书馆小组。
  • 能否请您澄清或说明原因您可以将堆栈用于缓冲​​区而不是堆,这样效率更高。?这种效率差异从何而来?
【解决方案2】:

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

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

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

【讨论】:

    【解决方案3】:

    我不会担心的。如果您在循环中执行此操作,字符串将始终预先分配内存以最小化重新分配 - 在这种情况下只需使用 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+ 的正文中返回它。它不能占用临时内存并附加到该内存。表达式越大,必须完成的字符串副本就越多。

    Next Visual Studio 和 GCC 将支持 c++1x 的 移动语义(补充 复制语义)和右值引用作为实验性添加。这允许确定参数是否引用临时参数。这将使这样的添加速度惊人地快,因为以上所有内容都将在一个没有副本的“添加管道”中结束。

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

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

    append 调用将参数附加到*this,然后返回对其自身的引用。所以没有复制临时文件。或者,可以使用operator+=,但您需要丑陋的括号来确定优先级。

    【讨论】:

    • 我必须检查标准库实现者是否真的这样做了。 :P libstdc++ for operator+(string const&amp; lhs, string&amp;&amp; rhs)return std::move(rhs.insert(0, lhs))。那么如果两者都是临时的,它的operator+(string&amp;&amp; lhs, string&amp;&amp; rhs) 如果lhs 有足够的可用容量将直接append()。我认为这可能比operator+= 慢的地方是如果lhs 没有足够的容量,那么它会回退到rhs.insert(0, lhs),这不仅必须扩展缓冲区并添加新的内容,如append(),而且还需要顺着rhs的原内容向右移动。
    • operator+= 相比,另一个开销是operator+ 仍然必须返回一个值,因此它必须在move() 上附加任何操作数。不过,与深度复制整个字符串相比,我想这是一个相当小的开销(复制几个指针/大小),所以这很好!
    【解决方案4】:

    对于大多数应用程序来说,这无关紧要。只需编写代码,完全不知道 + 运算符的工作原理,只有当它成为明显的瓶颈时,才可以自己处理。

    【讨论】:

    • 当然在大多数情况下不值得,但这并不能真正回答他的问题。
    • 是的。我同意只是说“配置文件然后优化”可以作为对问题的评论:)
    • 很公平,但某些应用程序肯定需要它。因此,在这些应用程序中,答案简化为:“将事情掌握在自己手中”
    • 抱歉这么挑剔。我只是认为,他需要解释为什么 operator+ 效率不高,以确定在他的情况下他是否需要这样做。
    • @Pesto 在编程世界中有一个变态的概念,即性能并不重要,我们可以忽略整个交易,因为计算机越来越快。问题是,这不是人们使用 C++ 编程的原因,也不是他们发布有关有效字符串连接的堆栈溢出问题的原因。
    【解决方案5】:

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

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

    【讨论】:

    • 我不太清楚为什么这被否决了。标准不要求 50% 的数字,但 IIRC 或 100% 是实践中常见的增长衡量标准。这个答案中的其他所有内容似乎都无可非议。
    • 几个月后,我想它并不是那么准确,因为它是在 C++11 首次亮相之后很久才编写的,并且 operator+ 的重载其中一个或两个参数通过右值引用传递可以避免分配通过连接到其中一个操作数的现有缓冲区中来完全创建一个新字符串(尽管如果容量不足,它们可能必须重新分配)。
    【解决方案6】:

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

    【讨论】:

    • 特别是如果您在开始之前使用reserve() 使缓冲区足够大以容纳结果。
    • 我认为他在谈论 operator+= 。它也是连接的,尽管它是一个退化的情况。 james 是 vc++ mvp,所以我希望他对 c++ 有所了解:p
    • 我毫不怀疑他对 C++ 有广泛的了解,只是对这个问题存在误解。该问题询问 operator+ 的效率,它每次调用时都会返回新的字符串对象,因此使用新的字符缓冲区。
    • 是的。但后来他要求案例 operator+ 很慢,最好的方法是进行连接。这里 operator+= 进入游戏。但我同意詹姆斯的回答有点短。听起来我们都可以使用 operator+ 并且效率最高:p
    • @BrianR.Bondy operator+ 不必返回新字符串。如果该操作数是通过右值引用传递的,则实现者可以返回其修改后的操作数之一。 libstdc++does this, for example。因此,当使用临时调用 operator+ 时,它可以获得相同或几乎相同的性能 - 这可能是另一个支持默认使用它的论据,除非有基准表明它代表瓶颈。
    【解决方案7】:

    也许用 std::stringstream 代替?

    但我同意这样的观点,即您可能应该保持其可维护性和可理解性,然后进行分析以查看您是否真的遇到问题。

    【讨论】:

    【解决方案8】:

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

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

    【讨论】:

    • Glib::ustring::compose() 从 glibmm 绑定到 GLib 这样做:根据提供的格式字符串和可变参数估计和 reserve()s 的最终长度,然后是 append()s 每个(或其格式化替换) 在一个循环中。我希望这是一种非常常见的工作方式。
    【解决方案9】:

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

    我更喜欢 std::ostringstream 进行复杂的连接。

    【讨论】:

    • 什么是复串联?
    【解决方案10】:

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

    如果您想将大字符串输出到 GUI,那么无论您输出到什么,都可能比大字符串更好地处理字符串(例如,在文本编辑器中连接文本 - 通常它们会保留行作为单独的结构)。

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

    如果我从慢代码中删除了不必要的串联,我从来没有发现需要使串联更快。

    【讨论】:

      【解决方案11】:

      如果您在结果字符串中预先分配(保留)空间,可能会获得最佳性能。

      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】:

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

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

        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  |  
        

        【讨论】:

        • OP 对如何有效连接 std::string 感兴趣。他们没有要求替代字符串类。
        【解决方案13】:

        你可以试试这个,每个项目都有内存保留:

        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)), ...);
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2014-01-16
          • 1970-01-01
          • 2014-10-03
          • 2019-04-13
          • 1970-01-01
          • 2018-03-06
          • 1970-01-01
          相关资源
          最近更新 更多