当我有一个字符串,我需要连接一个字符到结束,我宁愿s = .... + ']'
过s = .... + "]"
任何性能的原因吗?
我知道数组字符串连接和字符串构建器,我不是要求有关如何连接字符串的建议.
我也知道有些人会有向我解释过早优化的冲动,而且总的来说我不应该为这些小问题烦恼,请不要......
我问,因为从编码风格偏好我更喜欢使用后者,但我觉得第一个应该稍微好一点,因为知道所附加的只是一个字符,不需要任何内部循环复制单个字符串时可能会出现这个单个字符串.
更新
正如@Scheintod写的那样,这确实是一个理论上的问题,并且我希望能够更好地理解java如何工作,而不是任何现实生活"让我们节省另一个微秒"的场景...也许我应该更清楚地说出来.
我喜欢理解"幕后"的工作方式我发现它可以帮助我创建更好的代码...
事实 - 我根本没有考虑编译器优化......
我不希望JIT使用StringBuilder
s而不是String
s ...因为我(可能错误地)将String构建器视为"更重",一方面是字符串,另一方面是构建和修改字符串更快.所以我会假设在某些情况下使用StringBuilder
s会比使用stings效率更低......(如果不是这样的话那么整个String类应该将其实现更改为如a的那样StringBuilder
并使用一些内部实际不可变字符串的表示...... - 或者是JIT有点做什么? - 假设对于一般情况,最好不要让开发人员选择......)
如果它确实将我的代码改为这样的程度,那么也许我的Q应该处于那个级别,询问它是否适合JIT做这样的事情,如果使用它会更好.
也许我应该开始查看已编译的字节码... [我将需要学习如何在java中执行此操作...]
作为一个旁注和我为什么会考虑查看字节码的例子 - 看看我的一篇关于优化Actionscript 2.0的相当古老的博客文章- 一个字节码透视图 - 第一部分它表明知道你的代码编译成确实可以帮助你写了更好的代码.
除了剖析这个,我们还有另一种可能获得一些见解的可能性.我想关注可能的速度差异,而不是关注再次移除它们的东西.
所以让我们从这个Test
类开始:
public class Test { // Do not optimize this public static volatile String A = "A String"; public static void main( String [] args ) throws Exception { String a1 = A + "B"; String a2 = A + 'B'; a1.equals( a2 ); } }
我用javac Test.java编译了这个(使用javac -v:javac 1.7.0_55)
使用javap -c Test.class我们得到:
Compiled from "Test.java" public class Test { public static volatile java.lang.String A; public Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]) throws java.lang.Exception; Code: 0: new #2 // class java/lang/StringBuilder 3: dup 4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V 7: getstatic #4 // Field A:Ljava/lang/String; 10: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 13: ldc #6 // String B 15: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 18: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 21: astore_1 22: new #2 // class java/lang/StringBuilder 25: dup 26: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V 29: getstatic #4 // Field A:Ljava/lang/String; 32: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 35: bipush 66 37: invokevirtual #8 // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder; 40: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 43: astore_2 44: aload_1 45: aload_2 46: invokevirtual #9 // Method java/lang/String.equals:(Ljava/lang/Object;)Z 49: pop 50: return static {}; Code: 0: ldc #10 // String A String 2: putstatic #4 // Field A:Ljava/lang/String; 5: return }
我们可以看到,涉及两个StringBuilders(第4,22行).所以我们发现的第一件事是,使用+
concat Strings
实际上与使用StringBuilder相同.
我们在这里可以看到的第二件事是StringBuilders都被调用了两次.首先附加volatile变量(第10,32行),第二次附加常量部分(第15,37行)
在A + "B"
append
使用Ljava/lang/String
(一个字符串)参数调用的情况下,A + 'B'
如果使用C
(a char)参数调用它.
因此编译不会将String转换为char,而是将其保留为*.
现在查看AbstractStringBuilder
包含我们使用的方法:
public AbstractStringBuilder append(char c) { ensureCapacityInternal(count + 1); value[count++] = c; return this; }
和
public AbstractStringBuilder append(String str) { if (str == null) str = "null"; int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; }
作为实际调用的方法.
这里最昂贵的操作当然ensureCapacity
只是在达到限制的情况下(它将旧的StringBuffers char []的数组副本转换为新的).所以两者都是如此,并没有真正的区别.
正如人们所看到的那样,还有许多其他的操作已经完成,但真正的区别在于value[count++] = c;
和之间str.getChars(0, len, value, count);
如果我们查看getChars,我们会看到,这一切都归结为一个System.arrayCopy
用于将String复制到Buffer的数组中的一个加上一些检查和其他方法调用与一个单一数组访问.
所以,我要说在理论上使用A + "B"
是慢得多比使用A + 'B'
.
我认为在实际执行中它也会变慢.但要确定这一点我们需要进行基准测试.
编辑:原因是这一切都在JIT之前完成它的魔力.请参阅Stephen C的答案.
EDIT2:我一直在研究eclipse编译器生成的字节码,它几乎完全相同.所以至少这两个编译器的结果没有区别.
编辑2: 现在有趣的部分
基准.通过在预热后运行循环0..100M a+'B'
和a+"B"
几次生成此结果:
a+"B": 5096 ms a+'B': 4569 ms a+'B': 4384 ms a+"B": 5502 ms a+"B": 5395 ms a+'B': 4833 ms a+'B': 4601 ms a+"B": 5090 ms a+"B": 4766 ms a+'B': 4362 ms a+'B': 4249 ms a+"B": 5142 ms a+"B": 5022 ms a+'B': 4643 ms a+'B': 5222 ms a+"B": 5322 ms
平均为:
a+'B': 4608ms a+"B": 5167ms
因此,即使在真正的基准知识(hehe)基准世界中,a+'B'
也比... 快10%左右a+"B"
.
...至少(免责声明)在我的系统上使用我的编译器和我的cpu,它在现实世界的程序中真的没有区别/不明显.除了因为你有一段代码,你经常运行,你的所有应用程序性能取决于那.但是,你可能会首先做一些与众不同的事情.
EDIT4:
考虑此事.这是用于基准测试的循环:
start = System.currentTimeMillis(); for( int i=0; i<RUNS; i++ ){ a1 = a + 'B'; } end = System.currentTimeMillis(); System.out.println( "a+'B': " + (end-start) + " ms" );
所以我们真的不仅仅是对我们关心的事情进行基准测试,而且还有java循环性能,对象创建性能以及对变量性能的赋值.所以真正的速度差异甚至可能更大一些.