赋值时的字符串串联是否有效?
Is String concatenation on assignment efficient?
我知道使用“+”连接运算符构建字符串效率很低,这就是为什么推荐使用 StringBuilder class,但我想知道这种模式是否效率低下也是吗?
String some = a + "\t" + b + "\t" + c + "\t" + d + "\t" + e;
我猜编译器会不会优化赋值?
此特定示例将由编译器内联:
String a = "a";
String b = "bb";
String c = "ccc";
String some = a + "\t" + b + "\t" + c;
Java 9+ 将使用 invokedynamic with makeConcatWithConstants 内联它以提高效率。根据 javap -v
输出:
Code:
stack=3, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String bb
5: astore_2
6: ldc #4 // String ccc
8: astore_3
9: aload_1
10: aload_2
11: aload_3
12: invokedynamic #5, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
17: astore 4
19: return
但是如果a
b
和c
是编译时常量,编译器会进一步优化代码:
final String a = "a";
final String b = "bb";
final String c = "ccc";
String some = a + "\t" + b + "\t" + c;
和some
将加载一个常量值:
Code:
stack=1, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String bb
5: astore_2
6: ldc #4 // String ccc
8: astore_3
9: ldc #5 // String a\tbb\tccc
11: astore 4
13: return
在其他情况下,例如for
循环编译器可能无法生成优化代码,因此 StringBuilder
可能更快。
在一般情况下,使用 +
和使用 StringBuilder
的字符串连接是绝对正确且有效的。但是在不同的情况下,与 +
的连接效率低于使用 StringBuilder
.
不在循环中的字符串连接 - 高效!!!
这会产生很好的性能,因为 JVM 使用 StringBuilder
.
转换它
String some = a + "\t" + b + "\t" + c + "\t" + d + "\t" + e;
这没问题,因为 JVM 在内部将此代码更改为以下代码:
String some = new StringBuilder().append(a).append('\t').append(c).append('\t')
.append(d).append('\t').append(e).toString();
P.S. StringBuilder
有内部缓冲区 char[]
。如果您知道结果字符串有多长,那么最好在开始时保留整个缓冲区。例如。如果最终字符串将 最多 1024 个字符 ,那么你可以 new StringBuilder(1024)
循环中的字符串连接 - 效率不高!!!
这会导致性能变差,因为 JVM 无法用一个 StringBuilder
包裹 while 循环,如下所示:
StringBuilder buf = new StringBuilder();
for (int i = 0; i < 10; i++)
buf.append(a).append('\t').append(c).append('\t')
.append(d).append('\t').append(e).append('t');
String some = buf.toString();
但 JVM 仍然能够优化每个循环迭代中的所有连接;像这样:
String some = "";
for (int i = 0; i < 10; i++) {
some = new StringBuilder(some).append(a).append('\t').append(c).append('\t')
.append(d).append('\t').append(e).append('t');
}
如您所见,在循环中使用字符串连接有一些缺点。
您的前提“使用“+”连接运算符构建字符串的效率非常低”是不正确的。首先,字符串连接本身并不是一个廉价的操作,因为它意味着创建一个包含所有连接字符串的新字符串,因此需要复制字符内容。但这始终适用,无论您如何做到这一点。
当你使用 +
运算符时,你是在告诉你想做什么,而不是说如何去做。甚至 Java 语言规范也不需要特定的实现策略,除了编译时常量的连接必须在编译时完成。因此对于编译时常量,+
运算符 是 最有效的解决方案¹。
实际上,从 Java 5 到 Java 8 的所有常用编译器都使用 StringBuilder
底层生成代码(在 Java 5 之前,它们使用 StringBuffer
).这适用于像您这样的语句,因此将其替换为手动 StringBuilder
使用不会有太大好处。通过提供合理的初始容量,您可能会比典型的编译器生成的代码稍微好一些,但仅此而已。
从 Java9 开始,编译器生成一条 invokedynamic
指令,允许运行时提供执行串联的实际代码。这可能是一个 StringBuilder
代码,类似于过去使用的代码,但也完全不同。最值得注意的是,运行时提供的代码可以访问应用程序代码无法访问的实现特定功能。所以现在,通过 +
的字符串连接甚至可以比基于 StringBuilder
的代码更快。
由于这仅适用于单个连接表达式,因此在使用多个语句甚至循环执行字符串构造时,在整个构造过程中始终如一地使用 StringBuilder
可能比多个连接操作更快。但是,由于代码在优化环境中运行,JVM 可以识别其中的一些模式,因此甚至不能确定。
现在是记住旧规则的时候了,只有在性能出现实际问题时才尝试优化性能。并始终使用公正的测量工具来验证尝试的优化是否真正提高了性能。关于性能优化技巧,有很多流传甚广的神话,无论是错误的还是过时的。
¹ 除非您有重复的部分并希望减小 class 文件的大小
我知道使用“+”连接运算符构建字符串效率很低,这就是为什么推荐使用 StringBuilder class,但我想知道这种模式是否效率低下也是吗?
String some = a + "\t" + b + "\t" + c + "\t" + d + "\t" + e;
我猜编译器会不会优化赋值?
此特定示例将由编译器内联:
String a = "a";
String b = "bb";
String c = "ccc";
String some = a + "\t" + b + "\t" + c;
Java 9+ 将使用 invokedynamic with makeConcatWithConstants 内联它以提高效率。根据 javap -v
输出:
Code:
stack=3, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String bb
5: astore_2
6: ldc #4 // String ccc
8: astore_3
9: aload_1
10: aload_2
11: aload_3
12: invokedynamic #5, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
17: astore 4
19: return
但是如果a
b
和c
是编译时常量,编译器会进一步优化代码:
final String a = "a";
final String b = "bb";
final String c = "ccc";
String some = a + "\t" + b + "\t" + c;
和some
将加载一个常量值:
Code:
stack=1, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String bb
5: astore_2
6: ldc #4 // String ccc
8: astore_3
9: ldc #5 // String a\tbb\tccc
11: astore 4
13: return
在其他情况下,例如for
循环编译器可能无法生成优化代码,因此 StringBuilder
可能更快。
在一般情况下,使用 +
和使用 StringBuilder
的字符串连接是绝对正确且有效的。但是在不同的情况下,与 +
的连接效率低于使用 StringBuilder
.
不在循环中的字符串连接 - 高效!!!
这会产生很好的性能,因为 JVM 使用 StringBuilder
.
String some = a + "\t" + b + "\t" + c + "\t" + d + "\t" + e;
这没问题,因为 JVM 在内部将此代码更改为以下代码:
String some = new StringBuilder().append(a).append('\t').append(c).append('\t')
.append(d).append('\t').append(e).toString();
P.S. StringBuilder
有内部缓冲区 char[]
。如果您知道结果字符串有多长,那么最好在开始时保留整个缓冲区。例如。如果最终字符串将 最多 1024 个字符 ,那么你可以 new StringBuilder(1024)
循环中的字符串连接 - 效率不高!!!
这会导致性能变差,因为 JVM 无法用一个 StringBuilder
包裹 while 循环,如下所示:
StringBuilder buf = new StringBuilder();
for (int i = 0; i < 10; i++)
buf.append(a).append('\t').append(c).append('\t')
.append(d).append('\t').append(e).append('t');
String some = buf.toString();
但 JVM 仍然能够优化每个循环迭代中的所有连接;像这样:
String some = "";
for (int i = 0; i < 10; i++) {
some = new StringBuilder(some).append(a).append('\t').append(c).append('\t')
.append(d).append('\t').append(e).append('t');
}
如您所见,在循环中使用字符串连接有一些缺点。
您的前提“使用“+”连接运算符构建字符串的效率非常低”是不正确的。首先,字符串连接本身并不是一个廉价的操作,因为它意味着创建一个包含所有连接字符串的新字符串,因此需要复制字符内容。但这始终适用,无论您如何做到这一点。
当你使用 +
运算符时,你是在告诉你想做什么,而不是说如何去做。甚至 Java 语言规范也不需要特定的实现策略,除了编译时常量的连接必须在编译时完成。因此对于编译时常量,+
运算符 是 最有效的解决方案¹。
实际上,从 Java 5 到 Java 8 的所有常用编译器都使用 StringBuilder
底层生成代码(在 Java 5 之前,它们使用 StringBuffer
).这适用于像您这样的语句,因此将其替换为手动 StringBuilder
使用不会有太大好处。通过提供合理的初始容量,您可能会比典型的编译器生成的代码稍微好一些,但仅此而已。
从 Java9 开始,编译器生成一条 invokedynamic
指令,允许运行时提供执行串联的实际代码。这可能是一个 StringBuilder
代码,类似于过去使用的代码,但也完全不同。最值得注意的是,运行时提供的代码可以访问应用程序代码无法访问的实现特定功能。所以现在,通过 +
的字符串连接甚至可以比基于 StringBuilder
的代码更快。
由于这仅适用于单个连接表达式,因此在使用多个语句甚至循环执行字符串构造时,在整个构造过程中始终如一地使用 StringBuilder
可能比多个连接操作更快。但是,由于代码在优化环境中运行,JVM 可以识别其中的一些模式,因此甚至不能确定。
现在是记住旧规则的时候了,只有在性能出现实际问题时才尝试优化性能。并始终使用公正的测量工具来验证尝试的优化是否真正提高了性能。关于性能优化技巧,有很多流传甚广的神话,无论是错误的还是过时的。
¹ 除非您有重复的部分并希望减小 class 文件的大小