在循环中重用StringBuilder会更好吗?
我有一个性能相关的问题关于使用StringBuilder。 在一个非常长的循环中,我操纵一个StringBuilder
并将其传递给另一个方法,如下所示:
for (loop condition) { StringBuilder sb = new StringBuilder(); sb.append("some string"); . . . sb.append(anotherString); . . . passToMethod(sb.toString()); }
在每个循环周期实例化StringBuilder
是一个很好的解决scheme吗? 而是更好地调用删除,如下所示?
StringBuilder sb = new StringBuilder(); for (loop condition) { sb.delete(0, sb.length); sb.append("some string"); . . . sb.append(anotherString); . . . passToMethod(sb.toString()); }
第二个是在我的迷你基准testing中速度提高了25%。
public class ScratchPad { static String a; public static void main( String[] args ) throws Exception { long time = System.currentTimeMillis(); for( int i = 0; i < 10000000; i++ ) { StringBuilder sb = new StringBuilder(); sb.append( "someString" ); sb.append( "someString2"+i ); sb.append( "someStrin4g"+i ); sb.append( "someStr5ing"+i ); sb.append( "someSt7ring"+i ); a = sb.toString(); } System.out.println( System.currentTimeMillis()-time ); time = System.currentTimeMillis(); StringBuilder sb = new StringBuilder(); for( int i = 0; i < 10000000; i++ ) { sb.delete( 0, sb.length() ); sb.append( "someString" ); sb.append( "someString2"+i ); sb.append( "someStrin4g"+i ); sb.append( "someStr5ing"+i ); sb.append( "someSt7ring"+i ); a = sb.toString(); } System.out.println( System.currentTimeMillis()-time ); } }
结果:
25265 17969
请注意,这是JRE 1.6.0_07。
基于Jon Skeet的编辑思路,这里是第2版。
public class ScratchPad { static String a; public static void main( String[] args ) throws Exception { long time = System.currentTimeMillis(); StringBuilder sb = new StringBuilder(); for( int i = 0; i < 10000000; i++ ) { sb.delete( 0, sb.length() ); sb.append( "someString" ); sb.append( "someString2" ); sb.append( "someStrin4g" ); sb.append( "someStr5ing" ); sb.append( "someSt7ring" ); a = sb.toString(); } System.out.println( System.currentTimeMillis()-time ); time = System.currentTimeMillis(); for( int i = 0; i < 10000000; i++ ) { StringBuilder sb2 = new StringBuilder(); sb2.append( "someString" ); sb2.append( "someString2" ); sb2.append( "someStrin4g" ); sb2.append( "someStr5ing" ); sb2.append( "someSt7ring" ); a = sb2.toString(); } System.out.println( System.currentTimeMillis()-time ); } }
结果:
5016 7516
在编写可靠代码的哲学中,总是把你的StringBuilder放到循环中。 这样它就不会超出其预期的代码。
其次,StringBuilder中最大的改进来自于给它一个初始大小,以避免它在循环运行时变大
for (loop condition) { StringBuilder sb = new StringBuilder(4096); }
更快:
public class ScratchPad { private static String a; public static void main( String[] args ) throws Exception { long time = System.currentTimeMillis(); StringBuilder sb = new StringBuilder( 128 ); for( int i = 0; i < 10000000; i++ ) { // Resetting the string is faster than creating a new object. // Since this is a critical loop, every instruction counts. // sb.setLength( 0 ); sb.append( "someString" ); sb.append( "someString2" ); sb.append( "someStrin4g" ); sb.append( "someStr5ing" ); sb.append( "someSt7ring" ); setA( sb.toString() ); } System.out.println( System.currentTimeMillis()-time ); } private static void setA( String aString ) { a = aString; } }
在编写可靠代码的哲学中,方法的内部运作应该隐藏在使用方法的对象之外。 因此,无论是在循环内还是在循环外部重新声明StringBuilder,系统的angular度都没有什么不同。 由于在循环之外声明它更快,并且不会使代码更复杂,因此重用该对象而不是重新实例化它。
即使代码更复杂,并且您确定对象实例化是瓶颈,请对其进行评论。
三个运行这个答案:
$ java ScratchPad 1567 $ java ScratchPad 1569 $ java ScratchPad 1570
三个运行与其他答案:
$ java ScratchPad2 1663 2231 $ java ScratchPad2 1656 2233 $ java ScratchPad2 1658 2242
虽然不重要,但设置StringBuilder
的初始缓冲区大小将会带来一点收益。
好吧,我现在明白发生了什么事情,这是有道理的。
我的印象是, toString
只是将底层的char[]
传递给一个String构造函数,它没有拷贝。 然后复制下一个“写入”操作(例如delete
)。 我相信在之前版本中, StringBuffer
就是这种情况。 (现在不是。)但是, toString
只是将数组(以及索引和长度)传递给需要拷贝的公共String
构造函数。
所以在“重用StringBuilder
”的情况下,我们真正地创build了每个string的数据的一个拷贝,在缓冲区中一直使用相同的字符数组。 显然每次创build一个新的StringBuilder
会创build一个新的底层缓冲区 – 然后在创build一个新的string的时候,这个缓冲区被复制(在我们的特殊情况下,有点毫无意义,但是出于安全原因)。
所有这一切导致第二个版本肯定更有效 – 但同时我仍然会说这是更丑陋的代码。
由于我不认为它已经被指出了,因为Sun Java编译器内置的优化function,当它看到string连接时会自动创buildStringBuilders(StringBuffers pre-J2SE 5.0),问题的第一个例子等同于:
for (loop condition) { String s = "some string"; . . . s += anotherString; . . . passToMethod(s); }
哪个更具可读性,IMO更好的方法。 您尝试优化可能会在某些平台上获得收益,但可能会损失其他平台。
但是,如果你真的遇到了性能问题,那么一定要优化一下。 不过,按照Jon Skeet的说法,我会先明确指定StringBuilder的缓冲区大小。
现代的JVM对于这样的东西真的很聪明。 我不会再猜测它,并做一些不太可维护/可读的黑客行为,除非你用生产数据做适当的基准testing,以validation不重要的性能改进(并logging下来)
根据我在Windows上开发软件的经验,我可以说在循环期间清理StringBuilder比每次迭代实例化一个StringBuilder具有更好的性能。 清除它可以使内存立即被覆盖,而不需要额外的分配。 我对Java垃圾收集器不够熟悉,但是我认为释放和不重新分配(除非你的下一个string增长了StringBuilder)比实例化更有利。
(我的看法与其他人所暗示的是相反的,呃,时间来衡量它。)
为什么做setLength或者delete会提高性能,主要是代码学习缓冲区的大小,而不是内存分配。 通常, 我build议让编译器进行string优化 。 但是,如果性能很关键,我经常会预先计算缓冲区的预期大小。 默认的StringBuilder大小是16个字符。 如果你超出这个范围,那么它必须resize。 resize是性能正在丧失的地方。 这是另一个微型基准,它说明了这一点:
private void clear() throws Exception { long time = System.currentTimeMillis(); int maxLength = 0; StringBuilder sb = new StringBuilder(); for( int i = 0; i < 10000000; i++ ) { // Resetting the string is faster than creating a new object. // Since this is a critical loop, every instruction counts. // sb.setLength( 0 ); sb.append( "someString" ); sb.append( "someString2" ).append( i ); sb.append( "someStrin4g" ).append( i ); sb.append( "someStr5ing" ).append( i ); sb.append( "someSt7ring" ).append( i ); maxLength = Math.max(maxLength, sb.toString().length()); } System.out.println(maxLength); System.out.println("Clear buffer: " + (System.currentTimeMillis()-time) ); } private void preAllocate() throws Exception { long time = System.currentTimeMillis(); int maxLength = 0; for( int i = 0; i < 10000000; i++ ) { StringBuilder sb = new StringBuilder(82); sb.append( "someString" ); sb.append( "someString2" ).append( i ); sb.append( "someStrin4g" ).append( i ); sb.append( "someStr5ing" ).append( i ); sb.append( "someSt7ring" ).append( i ); maxLength = Math.max(maxLength, sb.toString().length()); } System.out.println(maxLength); System.out.println("Pre allocate: " + (System.currentTimeMillis()-time) ); } public void testBoth() throws Exception { for(int i = 0; i < 5; i++) { clear(); preAllocate(); } }
结果显示,重复使用该对象的速度比创build预期大小的缓冲大约快10%。
大声笑,我第一次看到人们通过在StringBuilder中组合string来比较性能。 为此,如果使用“+”,则可能更快; D。 使用StringBuilder加速检索整个string的目的是“locality”的概念。
在频繁检索不需要频繁更改的String值的情况下,Stringbuilder允许更高的string检索性能。 这是使用Stringbuilder的目的..请不要MIS – testing的核心目的..
有人说,飞机飞得更快。 所以,我用自行车testing了一下,发现飞机移动速度较慢。 你知道我如何设置实验设置; D
没有明显更快,但是从我的testing中可以看出,使用1.6.0_45 64位的平均速度是几毫秒:使用StringBuilder.setLength(0)代替StringBuilder.delete():
time = System.currentTimeMillis(); StringBuilder sb2 = new StringBuilder(); for (int i = 0; i < 10000000; i++) { sb2.append( "someString" ); sb2.append( "someString2"+i ); sb2.append( "someStrin4g"+i ); sb2.append( "someStr5ing"+i ); sb2.append( "someSt7ring"+i ); a = sb2.toString(); sb2.setLength(0); } System.out.println( System.currentTimeMillis()-time );
最快的方法是使用“setLength”。 这将不涉及复制操作。 创build一个新的StringBuilder的方式应该是完全的 。 StringBuilder.delete(int start,int end)的缓慢是因为它会再次为resize的部分复制数组。
System.arraycopy(value, start+len, value, start, count-end);
之后,StringBuilder.delete()会将StringBuilder.count更新为新的大小。 而StringBuilder.setLength()只是简化了将StringBuilder.count更新为新的大小。
唉,我通常在C#上,我认为清理一个StringBuilder的权重比实例化一个新的更重要。
第一个对人类更好。 如果在某些JVM的某些版本中,第二个版本稍微快一点,那又如何呢?
如果性能是至关重要的,绕过StringBuilder并写自己的。 如果你是一名优秀的程序员,并考虑到你的应用程序如何使用这个function,你应该能够更快。 值得吗? 可能不会。
为什么这个问题被视为“最喜欢的问题”? 因为性能优化非常有趣,不pipe是否实用。
申报一次,并分配每一次。 与优化相比,这是一个更实用,更可重用的概念。