我有一个关于使用StringBuilder的性能相关问题。
在一个很长的循环中,我正在操纵StringBuilder
并将其传递给另一个方法:
for (loop condition) {
StringBuilder sb = new StringBuilder();
sb.append("some string");
. . .
sb.append(anotherString);
. . .
passToMethod(sb.toString());
}
在每个循环周期实例化StringBuilder
是一个很好的解决方案吗?并且更好地调用删除,如下所示?
StringBuilder sb = new StringBuilder();
for (loop condition) {
sb.delete(0, sb.length);
sb.append("some string");
. . .
sb.append(anotherString);
. . .
passToMethod(sb.toString());
}
答案 0 :(得分:66)
第二个在我的迷你基准测试中快了大约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
答案 1 :(得分:25)
在编写可靠代码的哲学中,将StringBuilder放在循环中总是更好。这样它就不会超出其预期的代码。
其次,StringBuilder中最大的改进来自给它一个初始大小,以避免它在循环运行时变大;
for (loop condition) {
StringBuilder sb = new StringBuilder(4096);
}
答案 2 :(得分:23)
更快:
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,系统的视角都没有区别。由于在循环之外声明它更快,并且它不会使代码更复杂,因此重用该对象而不是重新实现它。
即使代码更复杂,并且您确实知道对象实例化是瓶颈,请对其进行评论。
有三个回答:
$ java ScratchPad
1567
$ java ScratchPad
1569
$ java ScratchPad
1570
另外三个回答:
$ java ScratchPad2
1663
2231
$ java ScratchPad2
1656
2233
$ java ScratchPad2
1658
2242
虽然不重要,但设置StringBuilder
的初始缓冲区大小会产生很小的收益。
答案 3 :(得分:12)
好的,我现在明白发生了什么,而且确实有意义。
我的印象是toString
刚刚将基础char[]
传递给了一个没有获取副本的String构造函数。然后将在下一次“写入”操作(例如delete
)上进行复制。我相信在以前的某个版本中, 就是StringBuffer
的情况。 (现在不是。)但是没有 - toString
只是将数组(以及索引和长度)传递给需要副本的公共String
构造函数。
因此,在“重用StringBuilder
”的情况下,我们真正为每个字符串创建一个数据副本,一直使用缓冲区中的相同char数组。显然每次都创建一个新的StringBuilder
创建一个新的底层缓冲区 - 然后在创建一个新字符串时复制该缓冲区(在我们的特定情况下,有些无意义,但出于安全原因)。
这一切导致第二个版本肯定更有效率 - 但与此同时我仍然会说这是更丑陋的代码。
答案 4 :(得分:9)
由于我不认为它已被指出,因为Sun Java编译器内置了优化,当它看到String连接时自动创建StringBuilders(StringBuffers pre-J2SE 5.0),问题中的第一个示例是等效的到:
for (loop condition) {
String s = "some string";
. . .
s += anotherString;
. . .
passToMethod(s);
}
哪个更具可读性,IMO,更好的方法。您尝试优化可能会导致某些平台获益,但可能会损失其他平台。
但如果你真的遇到了性能问题,那么请确保优化。我首先明确指定StringBuilder的缓冲区大小,每个Jon Skeet。
答案 5 :(得分:4)
现代JVM对这样的东西非常聪明。我不会再猜测它并做一些不太可维护/可读的hacky ......除非你用生产数据做适当的基准测试,以验证非平凡的性能改进(并记录它;)
答案 6 :(得分:4)
根据我在Windows上开发软件的经验,我会说在循环期间清除StringBuilder比每次迭代实例化StringBuilder有更好的性能。清除它可以立即释放内存,无需额外分配。我对Java垃圾收集器不够熟悉,但我认为释放和不重新分配(除非你的下一个字符串增长了StringBuilder)比实例化更有益。
(我的意见与其他人的建议相反。嗯。是时候对它进行基准测试了。)
答案 7 :(得分:1)
执行'setLength'或'delete'改善性能的原因主要是代码'学习'正确的缓冲区大小,而不是进行内存分配。通常,I recommend letting the compiler do the string optimizations。但是,如果性能很关键,我会经常预先计算缓冲区的预期大小。默认的StringBuilder大小为16个字符。如果你超越了那个,那么它必须调整大小。调整大小是性能丢失的地方。这是另一个迷你基准,说明了这一点:
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();
}
}
结果显示重用该对象比创建预期大小的缓冲区快10%。
答案 8 :(得分:1)
LOL,我第一次看到人们通过在StringBuilder中组合字符串来比较性能。为此,如果你使用“+”,它可能会更快; D。使用StringBuilder加速检索整个字符串作为“locality”的概念的目的。
在频繁检索不需要频繁更改的String值的场景中,Stringbuilder允许更高的字符串检索性能。这就是使用Stringbuilder的目的..请不要MIS-测试那个的核心目的..
有人说,飞机飞得更快。因此,我用我的自行车测试它,发现飞机移动得更慢。你知道我如何设置实验设置; D答案 9 :(得分:1)
没有明显更快,但从我的测试中,它使用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 );
答案 10 :(得分:1)
最快的方法是使用“setLength”。它不涉及复制操作。 创建一个新的StringBuilder的方法应该是完全。 StringBuilder.delete(int start,int end)的速度很慢,因为它会为调整大小部分再次复制数组。
System.arraycopy(value, start+len, value, start, count-end);
之后,StringBuilder.delete()会将StringBuilder.count更新为新大小。而StringBuilder.setLength()只是简化了将 StringBuilder.count 更新为新的大小。
答案 11 :(得分:0)
唉,我正常使用C#,我认为清除StringBuilder的重量超过了实现新的权重。
答案 12 :(得分:0)
第一种对人类更好。如果在某些JVM的某些版本上第二个更快一点,那么呢?
如果性能非常重要,请绕过StringBuilder并编写自己的。如果您是一名优秀的程序员,并考虑到您的应用程序如何使用此功能,您应该能够更快地完成它。值得吗?可能不是。
为什么这个问题被视为“最喜欢的问题”?因为性能优化非常有趣,无论它是否实用。
答案 13 :(得分:0)
我认为尝试这样优化性能没有意义。 今天(2019年),这两个陈述在我的I5笔记本电脑上以100.000.000循环运行了大约11秒:
String a;
StringBuilder sb = new StringBuilder();
long time = 0;
System.gc();
time = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
StringBuilder sb3 = new StringBuilder();
sb3.append("someString");
sb3.append("someString2");
sb3.append("someStrin4g");
sb3.append("someStr5ing");
sb3.append("someSt7ring");
a = sb3.toString();
}
System.out.println(System.currentTimeMillis() - time);
System.gc();
time = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
sb.setLength(0);
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);
==> 11000毫秒(循环内声明)和8236毫秒(循环外声明)
即使我正在运行带有数十亿循环的地址去重复的程序 相差2秒。 1亿个循环没有任何区别,因为程序运行了几个小时。 另外请注意,如果只有一个append语句,情况会有所不同:
System.gc();
time = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
StringBuilder sb3 = new StringBuilder();
sb3.append("someString");
a = sb3.toString();
}
System.out.println(System.currentTimeMillis() - time);
System.gc();
time = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
sb.setLength(0);
sb.delete(0, sb.length());
sb.append("someString");
a = sb.toString();
}
System.out.println(System.currentTimeMillis() - time);
==> 3416毫秒(内部循环),3555毫秒(外部循环) 在这种情况下,在循环内创建StringBuilder的第一条语句更快。 而且,如果您更改执行顺序,则速度会更快:
System.gc();
time = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
sb.setLength(0);
sb.delete(0, sb.length());
sb.append("someString");
a = sb.toString();
}
System.out.println(System.currentTimeMillis() - time);
System.gc();
time = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
StringBuilder sb3 = new StringBuilder();
sb3.append("someString");
a = sb3.toString();
}
System.out.println(System.currentTimeMillis() - time);
==> 3638毫秒(外部循环),2908毫秒(内部循环)
关于, 乌尔里希
答案 14 :(得分:-2)
声明一次,并分配每次。与优化相比,它是一个更实用,更可重用的概念。