Efficient string concatenation in C++
我听到一些人表达了对std :: string中"+"运算符的担忧以及加速连接的各种变通方法。 这些都真的有必要吗? 如果是这样,在C ++中连接字符串的最佳方法是什么?
- 基本上+不是连接运算符(因为它生成一个新的字符串)。 使用+ =进行连接。
- 从C ++ 11开始,有一个重点:operator +可以修改其中一个操作数,如果该操作数由rvalue引用传递,则返回它。 例如,libstdc++这样做。 因此,当调用operator + with temporaries时,它可以实现几乎同样良好的性能 - 可能是支持默认的论据,为了便于阅读,除非有基准显示它是一个瓶颈。 但是,标准化的变量append()既是最佳的又是可读的......
额外的工作可能不值得,除非你真的需要效率。只需使用operator + =,您可能会有更好的效率。
现在在免责声明之后,我将回答你的实际问题......
STL字符串类的效率取决于您正在使用的STL的实现。
您可以通过c内置函数手动连接来保证效率并更好地控制自己。
为什么operator +效率不高:
看看这个界面:
1 | template <class charT, class traits, class Alloc> |
您可以看到每个+后都返回一个新对象。这意味着每次都使用新的缓冲区。如果你正在做大量的额外+操作,那就没有效率了。
为什么你可以提高效率:
- 您保证效率,而不是相信代表有效地为您做到这一点
- std :: string类对字符串的最大大小一无所知,也不知道你连接它的频率。您可能拥有此知识,并且可以根据获取此信息来执行操作。这将减少重新分配。
- 您将手动控制缓冲区,这样您就可以确保在不希望发生这种情况时不会将整个字符串复制到新的缓冲区中。
- 您可以将堆栈用于缓冲??区而不是堆,这样可以提高效率。
- string +运算符将创建一个新的字符串对象,并使用新的缓冲区返回它。
实施的考虑因素:
- 跟踪字符串长度。
- 保持指向字符串末尾和开头的指针,或者只是开始,并使用start + the length作为偏移量来查找字符串的结尾。
- 确保存储字符串的缓冲区足够大,因此您无需重新分配数据
- 使用strcpy而不是strcat,因此您不需要遍历字符串的长度来查找字符串的结尾。
绳索数据结构:
如果您需要非常快速的连接,请考虑使用绳索数据结构。
- 注意:"STL"是指一个完全独立的开源库,最初由HP提供,其中一部分用作ISO标准C ++库部分的基础。但是,"std :: string"从来都不是HP STL的一部分,所以将"STL和"字符串"引在一起是完全错误的。
- 我不会说使用STL和字符串是错误的。请参阅sgi.com/tech/stl/table_of_contents.html
- 当SGI接管HP的STL维护时,它经过改装以匹配标准库(这就是为什么我说"永远不属于HP的STL")。然而,std :: string的发起者是ISO C ++委员会。
- 附注:负责维护STL多年的SGI员工是Matt Austern,同时也是ISO C ++标准化委员会的图书馆小组的负责人。
- 非常感谢您的信息
- operator +创建一个缓慢的新缓冲区但是如果你做x = x + y(其中x和y都是std :: string)你可以在operator =中得到一个额外的字符串缓冲区副本! ("可能"表示可能存在共享缓冲区的std :: string实现)。
- @Brian R. Bondy:"你可以看到每个+后都会返回一个新对象......"我不确定我能"看到"。是因为函数参数是const而返回类型不是?
- 您能否澄清或说明为什么您可以将堆栈用于缓冲??区而不是堆更高效的堆栈。这种效率差异来自哪里?
- @ h7r我认为这很难解释,几乎到了它应该被删除的程度。通过确保将新内容复制到目标字符串的现有缓冲区中,使用.reserve()和.append()来避免来自operator+的临时堆的额外堆分配。但是,如果字符串对于小字符串优化而言太长(或者stdlib实现,无论出于何种原因,不使用SSO),那么该缓冲区可能已经在堆上。而仅仅在堆上并不会对效率产生任何影响:它从堆中分配成本很高
- 从C ++ 11开始,这就错过了一个重要的观点:operator +可以修改其中一个操作数,如果该操作数是通过右值引用传递的话,则返回它。例如,libstdc++就是这样做的。因此,当调用operator + with temporaries时,它可以实现几乎同样良好的性能 - 可能是支持默认的论据,为了便于阅读,除非有基准显示它是一个瓶颈。但是,标准化的变量append()将是最佳和可读的......
之前保留最后一个空格,然后使用带缓冲区的append方法。例如,假设您希望最终的字符串长度为100万个字符:
1 | std::string s; |
我不担心。如果你在循环中执行它,字符串将始终预分配内存以最小化重新分配 - 在这种情况下只需使用operator+=。如果你手动完成,这样或更长时间
1 | a +" :" + c |
然后它正在创造临时性 - 即使编译器可以消除一些返回值副本。这是因为在连续调用的operator+中,它不知道引用参数是引用命名对象还是从子operator+调用返回的临时对象。在没有首先进行分析之前,我宁愿不担心它。但让我们举一个例子来证明这一点。我们首先引入括号以使绑定清晰。我将参数直接放在用于清晰的函数声明之后。在下面,我展示了结果表达式是什么:
1 | ((a +" :") + c) |
现在,在该添加中,tmp1是第一次调用operator +并返回显示的参数。我们假设编译器非常聪明并优化了返回值副本。因此,我们最终得到一个包含a和" :"串联的新字符串。现在,这发生了:
1 | (tmp1 + c) |
将其与以下内容进行比较:
1 | std::string f ="hello"; |
它对临时和命名字符串使用相同的函数!因此编译器必须将参数复制到一个新字符串中并追加到该字符串并从operator+的主体返回。它不能记住一个临时的并追加它。表达式越大,字符串的副本就越多。
接下来,Visual Studio和GCC将支持c ++ 1x的移动语义(补充复制语义)和rvalue引用作为实验添加。这允许确定参数是否引用临时参数。这将使得这样的添加速度惊人地快,因为上述所有内容将最终出现在一个没有副本的"添加管道"中。
如果它成为瓶颈,你仍然可以做到
1 | std::string(a).append(" :").append(c) ... |
append调用将参数附加到*this,然后返回对自己的引用。因此,那里没有复制临时工。或者,可以使用operator+=,但是您需要使用丑陋的括号来修复优先级。
- 很酷的关于C ++ 1x移动语义
- 我必须检查stdlib实现者真的这样做。 :对于operator+(string const& lhs, string&& rhs),P libstdc++ return std::move(rhs.insert(0, lhs))。然后,如果两者都是临时的,那么operator+(string&& lhs, string&& rhs)如果lhs有足够的可用容量将直接append()。如果lhs没有足够的容量,那么我认为这可能比operator+=慢,那么它会回落到rhs.insert(0, lhs),这不仅必须扩展缓冲区并添加新的内容,如append(),还需要沿着rhs的原始内容移动。
- 与operator+=相比,另一个开销是operator+仍然必须返回一个值,所以它必须move()它附加到哪个操作数。不过,我认为与深度复制整个字符串相比,这是一个相当小的开销(复制几个指针/大小),所以这很好!
对于大多数应用来说,这无关紧要。只需编写代码,幸福地不知道+运算符的工作原理,只有当它成为一个明显的瓶颈时才能自己动手。
- 当然,对于大多数情况来说,这是不值得的,但这并没有真正回答他的问题。
- 是的。我同意只说"配置文件然后优化"可以作为评论问题:)
- 从技术上讲,他问这些是否是"必要的"。他们不是,这回答了这个问题。
- 对,对。全部回来:)
- 很公平,但它肯定是一些应用程序所需要的。因此,在这些应用程序中,答案简化为:"掌握在自己手中"
- 编程世界中存在一种变态的概念,即事情需要尽早地进行优化,而事实恰恰相反。这个问题意味着在这种情况下是否需要优化的不确定性,这意味着答案可能是"不"。
- 很抱歉这么关键。我只是想解释为什么operator +效率不高他需要确定他是否需要这样做。
- 标记为不回答问题
- @Pesto在编程世界中有一种变态的概念,即性能无关紧要,我们可以忽略整个协议,因为计算机的速度越来越快。问题是,这不是人们用C ++编程的原因,而这也不是为什么他们在有关字符串连接的堆栈溢出上发布问题的原因。
- 从一个简单的测试表明append远远优于我,我改造了我的整个代码库来使用它而不是operator+ - 包括将const& args更改为值的hacks,以便我可以附加到它们......然后当我跑真正的编程,要么没有统计差异,要么append在某些运行中稍微慢一点!我猜这是Pesto的观点:除非您的应用几乎完全基于连接,否则您将看不到(可靠的)差异。现在检查master退出并忘记所有这些:P在这种状态下,如果有一个减速,它被operator+的可读性所抵消
与.NET System.Strings不同,C ++的std :: strings是可变的,因此可以通过简单的连接来构建,就像通过其他方法一样快。
- 特别是如果你在启动之前使用reserve()使缓冲区足够大以获得结果。
- operator +返回一个新的字符串对象......
- 我认为他在谈论算子+ =。它也是连接,虽然这是一个堕落的案例。 james是一个vc ++ mvp所以我希望他有一些c ++的线索:p
- 我毫不怀疑他对C ++有广泛的了解,只是对这个问题存在误解。问题是关于operator +的效率,每次调用它时返回新的字符串对象,因此使用新的char缓冲区。
- 是的。但后来他要求案例运算符+很慢,最好的方法是进行连接。在这里,operator + =进入游戏。但我同意詹姆斯的回答有点短。这听起来好像我们都可以使用operator +而且效率最高:p
- 陷阱是有道理的。
- @BrianR.Bondy operator+不必返回新字符串。如果该操作数由rvalue引用传递,则实现者可以返回其操作数之一,如果已修改。例如,libstdc++就是这样做的。所以,当用临时函数调用operator+时,它可以达到相同或几乎同样好的性能 - 这可能是支持默认的另一个论据,除非有基准显示它代表了瓶颈。
也许是std :: stringstream而已?
但我同意这样的观点,即你应该保持它的可维护性和可理解性,然后分析一下你是否确实遇到了问题。
- stringstream很慢,请参阅groups.google.com/d/topic/comp.lang.c++.moderated/aiFIGb6za0w
- @ArtemGr stringstream可能很快,请参阅codeproject.com/Articles/647856/
在Imperfect C ++中,Matthew Wilson提出了一个动态字符串连接器,它预先计算最终字符串的长度,以便在连接所有部分之前只进行一次分配。我们还可以通过使用表达式模板来实现静态连接器。
这种想法已在STLport std :: string实现中实现 - 由于这种精确的黑客攻击而不符合标准。
- 从glibmm绑定到GLib的Glib::ustring::compose()做到:根据提供的格式字符串和varargs估算和reserve() s最终长度,然后在循环中每个(或其格式化的替换)append() s。我希望这是一种非常常见的工作方式。
std::string operator+分配新字符串并每次复制两个操作数字符串。重复多次,它变得昂贵,O(n)。
另一方面,std::string append和operator+=,每次字符串需要增长时,容量会增加50%。这显着减少了内存分配和复制操作的数量,O(log n)。
- 我不太清楚为什么这会被贬低。标准不要求50%的数字,但IIRC或100%是实践中常见的增长指标。这个答案中的其他所有内容似乎都无可非议。
- 几个月后,我想这并不是那么准确,因为它是在C ++ 11首次亮相之后很久写的,并且operator+的重载,其中一个或两个参数通过rvalue引用传递,可以避免通过连接到一起来完全分配一个新的字符串。 其中一个操作数的现有缓冲区(尽管如果它们的容量不足,它们可能必须重新分配)。
与大多数事情一样,做某事比做事更容易。
如果你想输出大串的图形用户界面,它可能是,无论你是输出到可以处理件字符串不是作为一个大的字符串更好(例如,在文本编辑器串联文本 - 通常是他们让行作为独立结构)。
如果要输出到文件,请流式传输数据,而不是创建大型字符串并输出该字符串。
如果我从慢速代码中删除不必要的连接,我从未发现需要更快地进行连接。
对于小弦乐而言并不重要。
如果你有大字符串,你最好将它们存储为矢量或其他集合中的部分。并添加您的算法来处理这样的数据集而不是一个大字符串。
我更喜欢std :: ostringstream用于复杂的连接。
如果在结果字符串中预先分配(保留)空间,则可能是最佳性能。
1 | template<typename... Args> |
用法:
1 | std::string merged = concat("This","is","a","test!"); |
一个简单的字符数组,封装在一个跟踪数组大小和分配字节数的类中是最快的。
诀窍是在开始时只做一个大的分配。
在
https://github.com/pedro-vicente/table-string
基准
对于Visual Studio 2015,x86调试构建,对C ++ std :: string的改进。
1 | | API | Seconds |
- OP对如何有效地连接std::string感兴趣。 他们不是要求替代字符串类。