一次连接一个字符串效率低下吗?


11

我回想起用C语言进行编程的日子,当两个字符串连接在一起时,操作系统必须为连接的字符串分配内存,然后程序可以将所有字符串文本复制到内存中的新区域,然后旧内存必须手动进行被发布。因此,如果像加入列表一样多次执行此操作,则OS必须不断分配越来越多的内存,只是要在下一个串联后释放它。用C语言执行此操作的更好方法是确定组合字符串的总大小,并为整个连接的字符串列表分配必要的内存。

现在,在现代编程语言(例如C#)中,我通常会看到遍历集合的内容,方法是遍历集合并将所有字符串一次全部添加到单个字符串引用中。即使具有现代计算能力,这也不低效吗?


将其留给编译器和探查器,他们会关心它,您的时间要比字符串串联的时间要昂贵得多。
OZ_ 2012年

7
取决于实现方式–您确实应该检查特定字符串库的文档。可以在O(1)时间内实现通过引用连接的字符串。无论如何,如果需要连接任意长的字符串列表,则应使用为此类事情设计的类或函数。
即将

注意,诸如字符串连接之类的事情通常由库函数而不是操作系统处理。操作系统可能会参与内存分配,但可能不会涉及相对较小的对象(例如字符串)。
Caleb 2012年

@Caleb OS参与所有内存分配。未能遵循此规则是一种内存泄漏。例外是当您在应用程序中使用硬编码的字符串时。那些将作为二进制数据写入生成的程序集中。但是,一旦您操纵(或什至分配)了一个字符串,就需要将其存储在内存中(也就是说,必须分配内存)。
JSideris

4
@Bizorke在典型情况下,像malloc()这样的内存分配器(它是C标准库的一部分,而不是OS的一部分)用于从OS已经分配给进程的内存中分配各种内存块。除非进程运行的内存不足并且需要更多请求,否则操作系统无需参与。如果分配导致页面错误,它也可能处于较低级别。因此,是的,OS最终提供了内存,但是它不一定参与进程内部字符串和其他对象的零碎分配。
Caleb'4

Answers:


21

您至少在我熟悉的语言(C,Java,C#)中对为什么效率低下的解释是准确的,尽管我不同意执行大量字符串连接普遍存在。在C#代码,我的工作,有丰富的使用StringBuilderString.Format等等这些都是记忆保存techiniques避免过度再分配。

因此,要回答您的问题,我们必须提出另一个问题: 如果串联字符串从来就不是真正的问题,为什么类会喜欢StringBuilderStringBuffer存在?为什么甚至在初学者编程书籍和类中都包含此类的使用?为什么看似过早的优化建议如此突出?

如果大多数使用字符串连接的开发人员的答案完全基于经验,那么大多数人会说这永远不会有所作为,并且会避免使用此类工具,而采用“更具可读性”的方法for (int i=0; i<1000; i++) { strA += strB; }但是他们从来没有测量过。

这个问题的真正答案可以在SO答案中找到,它揭示了在一个实例中,当连接50,000个字符串(取决于您的应用程序,这很常见)时,甚至很小的字符串,都可能导致性能提高1000倍

如果性能从字面上根本不代表任何意义,则必须将其连接起来。但是我不同意使用替代方法(StringBuilder)困难或可读性差,因此这是一种合理的编程实践,不应调用“过早优化”防御。

更新:

我认为这可以归结为了解您的平台并遵循其最佳实践,而遗憾的是,这些最佳实践并不普遍。来自两种不同“现代语言”的两个示例:

  1. 另一个SO答案中,在JavaScript中有时发现完全相反的性能特征(array.join vs + =)是正确的。在某些浏览器中,字符串连接似乎是自动优化的,而在其他情况下则不是。因此,建议(至少在该SO问题中)是串联而不用担心。
  2. 在另一种情况下,Java编译器可以用更有效的结构(例如StringBuilder)自动替换串联。但是,正如其他人指出的那样,这是不确定的,不能保证,并且使用StringBuilder不会损害可读性。在这种特殊情况下,我倾向于建议不要将串联用于大型集合,也不要依赖于不确定的Java编译器行为。同样,在.NET中,永远不会执行排序优化

马上就不了解每个平台的每一个细微差别,这并不是一个主要的罪过,但是忽略这样的重要平台问题,几乎就像是从Java到C ++,而不是关心释放内存。


-1:包含主要BS。strA + strB完全相同一样使用一个StringBuilder。它的性能下降了1倍。或0x,具体取决于您的测量方式。有关更多详细信息, codinghorror.com / blog / 2009/01 /…
amara

5
@sparkleshy:我的猜测是SO答案使用Java,而您的链接文章使用C#。我同意那些说“取决于实现”并“针对您的特定环境对其进行度量”的人。
Kai Chan'4

1
@KaiChan:字符串串联在Java和C#中基本相同
amara 2012年

3
@sparkleshy-采取了观点,但是从来没有推荐使用StringBuilder,String.Join等来精确地连接两个字符串。此外,OP的问题专门针对“ 集合的内容连接在一起”,而事实并非如此(其中StringBuilder等非常适用)。无论如何,我将更新我的示例,使之更有意义。
凯文·麦考密克

3
我不在乎这个问题的目的。使用某些语言在后台使用stringbuilder 解释了为什么连接整个字符串列表可能并非没有效率的问题,这回答了我的问题。但是,此答案确实解释了加入列表可能存在危险,并建议使用stringbuilder作为替代方法。我建议在幕后将编译器对stringbuilder的使用添加到您的答案中,以避免可能的信誉损失或误解。
JSideris

2

大致由于您所描述的原因,它效率不高。C#和Java中的字符串是不可变的。与C中不同,对字符串的操作将返回一个单独的实例,而不是修改原始实例。在连接多个字符串时,每个步骤都会创建一个单独的实例。分配和稍后垃圾收集那些未使用的实例可能会导致性能下降。只有这次,垃圾收集器才为您处理内存管理。

C#和Java都引入了StringBuilder类作为专门用于此类任务的可变字符串。C语言中的等效项将使用串联字符串的链接列表,而不是将它们连接到数组中。C#还为字符串提供了一种方便的Join方法,用于加入字符串集合。


1

严格来说,使用CPU周期的效率较低,因此您是对的。但是,开发人员的时间,维护成本等情况如何。如果将时间成本加到等式中,最简单的方法通常总是效率更高,然后在需要时分析和优化慢速位。
“程序优化的第一条规则:不要做。程序优化的第二条规则(仅供专家使用!):还不要做。”


3
我认为规则不是很有效。
OZ_ 2012年

@OZ_:这是Donald Knuth等人广泛使用的引号(Michael A. Jackson)等。然后是这个,我通常不使用“更多以效率为名的计算犯罪(而不是出于任何其他单一原因-包括盲目愚蠢”。
mattnz

2
我要指出的是,迈克尔·A·杰克逊是一个英国人,所以它的优化优化。在某些时候,我确实应该更正Wikipedia页面。* 8')
Mark Booth

我完全同意,您应该纠正这些拼写错误。尽管我的母语是皇后英语,但我发现在公司内部网站上说美国语言会更容易
..

不会有人想到用户的。您可能会使开发人员的创建速度稍快一些,但是您的每一位客户都会为此受苦。为他们而不是为您编写代码。
gbjbaanb 2012年

1

如果没有实际测试,很难说出任何有关性能的信息。最近,我很惊讶地发现,在JavaScript中,单纯的字符串连接通常比推荐的“ make list and join”解决方案要快(在这里进行测试,将t1与t4进行比较)。我仍然对为什么会发生感到困惑。

在对性能进行推理时(尤其是在内存使用方面),您可能会问以下几个问题:1)我的输入有多大?2)我的编译器有多聪明?3)我的运行时如何管理内存?这并不详尽,但这是一个起点。

  1. 我的投入有多大?

    复杂的解决方案通常具有固定的开销,可能以要执行的额外操作的形式出现,或者可能需要额外的内存。由于这些解决方案是为处理大案件而设计的,因此实现者通常不会出现额外费用的问题,因为净收益比微优化代码更为重要。因此,如果您的输入足够小,那么天真的解决方案可能会比复杂的解决方案具有更好的性能,仅是为了避免这种开销。(尽管确定“足够小”是困难的部分)

  2. 我的编译器有多聪明?

    许多编译器非常聪明,可以“优化”写入但从未读取的变量。同样,一个好的编译器也许也可以将一个简单的字符串连接转换为一个(核心)库使用,并且,如果其中许多是在没有任何读取的情况下进行的,那么在这些操作之间就无需将其转换回字符串(即使您的源代码似乎就是这样做的。我无法确定是否有任何编译器可以执行此操作,或者执行的程度如何(AFAIK Java至少将同一表达式中的多个concat替换为一系列StringBuffer操作),但这是有可能的。

  3. 我的运行时如何管理内存?

    在现代CPU中,瓶颈通常不是处理器,而是缓存。如果您的代码在短时间内访问了许多“远程”内存地址,则在高速缓存级别之间移动所有内存所需的时间将超过所使用指令中的大多数优化。这在具有分代垃圾回收器的运行时中尤其重要,因为最近创建的变量(例如,在同一函数范围内)通常位于连续的内存地址中。这些运行时通常还会在方法调用之间来回移动内存。

    它可能会影响字符串连接的一种方式(免责声明:这是一个疯狂的猜测,我还不足以肯定地说),如果是为朴素的用户分配的内存是否接近使用它的其余代码(甚至如果它多次分配和释放它),而为库对象分配的内存却远不及它(因此,在代码计算,库消耗,代码计算更多等过程中,许多上下文都会发生更改,等等)会导致许多缓存未命中。当然对于大输入OTOH,无论如何都会发生高速缓存未命中,因此多重分配的问题变得更加明显。

就是说,我不是在提倡使用这种方法,而只是在测试,性能分析和基准测试之前,应先进行任何有关性能的理论分析,因为当今大多数系统太复杂,以至于没有足够的专业知识就无法完全理解。


是的,我同意,这绝对是编译器从理论上可以意识到您正在尝试将一堆字符串添加在一起,然后像使用字符串生成器一样进行优化的领域。但是,这并不是一件容易的事,而且我不认为它可以在任何现代编译器中实现。您只是给我一个本科研究项目:D的好主意。
JSideris

检查此答案,Java编译器已经StringBuilder在后台使用了,它所要做的就是toString在实际需要该变量之前才调用它。如果我没有记错,这确实是一个单一的表达,我唯一的疑问是它是否适用于同样的方法多条语句。我对.NET内部一无所知,但我相信C#编译器也可能采用类似的策略。
mgibsonbr

0

乔尔(Joel)不久前就这一主题写了一篇很棒的文章。正如其他人指出的那样,它在很大程度上取决于语言。由于在C中实现字符串的方式(零终止,没有长度字段),因此标准的strcat库例程效率很低。乔尔(Joel)提出了一个替代方案,只需稍作改动即可,效率更高。


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.