gpt4 book ai didi

java - 即使字符串池中已有可用的对象,字符串追加也会花费更多时间

转载 作者:行者123 更新时间:2023-12-01 12:38:56 27 4
gpt4 key购买 nike

我尝试过这个例子来发现 StringBuffer、StringBuilder 和 String 的执行时间不同

经过尝试,我发现 StringBuffer 和 StringBuilder 花费的时间更少,因为它没有创建新对象。

作为字符串追加空字符串也不会创建任何对象,因此速度更快。

当我 append 一些字符串时,应该需要更多时间,因为创建对象需要时间。

当我对另一个字符串执行相同的 append 字符串模式时,这也花费了更多时间。在这种情况下,所有对象都已在字符串池中可用。为什么它花费的时间和以前一样?

public class StringComparation {
public static void main(String[] args) {
int N = 100000;
long time;
// String Buffer
StringBuffer sb = new StringBuffer();
time = System.currentTimeMillis();
for (int i = N; i --> 0 ;) {
sb.append("a");
}
System.out.println("String Buffer - " + (System.currentTimeMillis() - time));
// String Builder
StringBuilder sbr = new StringBuilder();
time = System.currentTimeMillis();
for (int i = N; i --> 0 ;) {
sbr.append("a");
}
System.out.println("String Builder - " + (System.currentTimeMillis() - time));
// String Without String pool value
String s2 = new String();
time = System.currentTimeMillis();
for (int i = N; i --> 0 ;) {
s2 = s2 + "";
}
System.out.println("String Without String pool value - "
+ (System.currentTimeMillis() - time));
// String With new String pool Object
String s = new String();
time = System.currentTimeMillis();
for (int i = N; i --> 0 ;) {
s = s + "a";
}
System.out.println("String With new String pool Object - "
+ (System.currentTimeMillis() - time));
// String With already available String pool Object
String s1 = new String();
time = System.currentTimeMillis();
for (int i = N; i --> 0 ;) {
s1 = s1 + "a";
}
System.out.println("String With already available String pool Object - "
+ (System.currentTimeMillis() - time));
}
}

输出:

String Buffer - 43
String Builder - 16
String Without String pool value - 64
String With new String pool Object - 12659
String With already available String pool Object - 14258

如有错误,请指正。

最佳答案

鉴于您的最后两个测试是相同的,因此您实际上只有四个测试。为了方便起见,我将它们重构为单独的方法并删除了基准测试代码,因为没有必要了解这里发生的情况。

public static void stringBuilderTest(int iterations) {
final StringBuilder sb = new StringBuilder();
for (int i = iterations; i-- > 0;) {
sb.append("a");
}
}

public static void stringBufferTest(int iterations) {
final StringBuffer sb = new StringBuffer();
for (int i = iterations; i-- > 0;) {
sb.append("a");
}
}

public static void emptyStringConcatTest(int iterations) {
String s = new String();
for (int i = iterations; i-- > 0;) {
s += "";
}
}

public static void nonEmptyStringConcatTest(int iterations) {
String s = new String();
for (int i = iterations; i-- > 0;) {
s += "a";
}
}

我们已经知道 StringBuilder 版本的代码是四个版本中最快的。 StringBuffer 版本速度较慢,因为它的所有操作都是同步的,这带来了 StringBuilder 没有的不可避免的开销,因为它同步。

所以我们感兴趣的两个方法是 emptyStringConcatTestnonEmptyStringConcatTest 。如果我们检查 emptyStringConcatTest 编译版本的字节码,我们看到以下内容:

  public static void emptyStringConcatTest(int);
flags: ACC_PUBLIC, ACC_STATIC
LineNumberTable:
line 27: 0
line 28: 8
line 29: 17
line 31: 40
Code:
stack=2, locals=3, args_size=1
0: new #14 // class java/lang/String
3: dup
4: invokespecial #15 // Method java/lang/String."<init>":()V
7: astore_1
8: iload_0
9: istore_2
10: iload_2
11: iinc 2, -1
14: ifle 40
17: new #7 // class java/lang/StringBuilder
20: dup
21: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
24: aload_1
25: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: ldc #16 // String
30: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: invokevirtual #17 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: astore_1
37: goto 10
40: return
LineNumberTable:
line 27: 0
line 28: 8
line 29: 17
line 31: 40
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 10
locals = [ class java/lang/String, int ]
frame_type = 250 /* chop */
offset_delta = 29

在底层,这两种方法几乎相同,唯一的区别是这一行:

空字符串:

28: ldc           #9                  // String 

非空字符串(注意微小但重要的区别!):

28: ldc           #9                  // String a

关于字节码,首先要注意的是for的结构。循环体:

10: iload_2
11: iinc 2, -1
14: ifle 40
17: new #7 // class java/lang/StringBuilder
20: dup
21: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
24: aload_1
25: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: ldc #16 // String
30: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: invokevirtual #17 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: astore_1
37: goto 10

我们实际上最终得到的是编译器优化

for (int i = iterations; i-- > 0;) {
s += "";
}

进入:

for (int i = iterations; i-- > 0;) {
s = new StringBuilder().append(s).append("").toString();
}

这不太好。我们在每一次迭代中实例化一个新的临时 StringBuilder 对象,其中有 100,000 次。这是很多对象。

您在 emptyStringConcatTest 之间看到的差异和nonEmptyStringConcatTest如果我们检查StringBuilder#append(String)的源代码可以进一步解释:

public StringBuilder append(String str) {
super.append(str);
return this;
}

StringBuilder的父类(super class)是AbstractStringBuilder,我们来看看它的append(String)的实现:

public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
if (len == 0) return this;
int newCount = count + len;
if (newCount > value.length)
expandCapacity(newCount);
str.getChars(0, len, value, count);
count = newCount;
return this;
}

您会在这里注意到,如果参数的长度 str为零,该方法只是返回而不执行任何进一步的操作,这使得在空字符串的情况下速度相当快。

非空字符串参数触发支持的边界检查 char[] ,可能会导致其大小调整为 expandCapacity(int) ,它将原始数组复制到一个新的、更大的数组中(请注意,StringBuilder 中的后备数组不是 final - 它可以重新分配!)。完成后,我们调用 String#getChars(int, int, char[], int) ,它执行更多数组复制。数组复制的确切实现隐藏在 native 代码中,因此我不会四处寻找它们。

更复杂的是,我们创建然后丢弃的对象的绝对数量可能足以触发 JVM 垃圾收集器的运行,这会带来进一步的开销。

总而言之;相当于 nonEmptyStringConcatTest 的性能大幅下降很大程度上取决于编译器所做的糟糕的“优化”。通过不在循环内进行直接串联来避免这种情况。

关于java - 即使字符串池中已有可用的对象,字符串追加也会花费更多时间,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25305933/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com