当我有一个String时,我需要将一个char连接到它的结尾,
出于性能原因,我应该更喜欢s = .... + ']'
而不是s = .... + "]"
吗?
我知道数组字符串连接和String构建器, 我并不是要求提供有关如何连接字符串的建议。
我也知道有些人会向我解释过早优化的问题,而且一般来说我不应该为这些小问题烦恼,请不要......
我在问,因为从编码风格偏好来看,我更愿意使用后者, 但是我觉得第一个应该稍微好一点,因为知道附加的内容只是一个char,不需要任何内部循环遍历这个单独的char,就像复制单个字符串时一样。 / p>
更新
正如@Scheintod所写,这确实是一个理论上的问题,并且我希望能够更好地理解java如何工作,而不是任何现实生活“让我们节省另一个微秒”的场景...... 也许我应该更清楚地说出来。
我喜欢理解“幕后工作”的方式而且我发现它可以帮助我创建更好的代码......
事实 - 我根本没有考虑编译器优化......
我不希望JIT为我使用StringBuilder
而不是String
s ...
因为我(可能错误地)将String构建器视为“更重”,一方面是字符串,另一方面是构建和修改字符串的速度更快。所以我假设在某些情况下使用StringBuilder
s甚至比使用stings效率更低......(如果不是这样的话那么整个String类应该将其实现更改为如此一个StringBuilder
并使用一些内部表示来实现不可变的字符串... - 或者是JIT的做法? - 假设对于一般情况,最好不要让开发人员选择.. 。)
如果它确实将我的代码更改到这样的程度,那么也许我的Q应该处于该级别,询问它是否适合JIT做这样的事情,如果使用它会更好。
也许我应该开始查看已编译的字节码... [我需要学习如何在java中这样做...]
作为一个侧面说明以及为什么我会考虑查看字节码的示例 - 看看我的一篇关于Optimizing Actionscript 2.0 - a bytecode perspective - Part I的相当古老的博客文章,它表明知道你的代码编译成什么确实可以帮助你写更好的代码。
答案 0 :(得分:21)
除了剖析这个,我们还有另一种可能获得一些见解的可能性。我想专注于可能的速度差异,而不是关注再次删除它们的事情。
让我们从这个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
}
我们可以看到,涉及两个StringBuilder(第4,22行)。所以我们发现的第一件事是,使用+
连接Strings
实际上与使用StringBuilder相同。
我们在这里可以看到的第二件事是StringBuilders被调用两次。首先附加volatile变量(第10,32行),第二次附加常量部分(第15,37行)
如果A + "B"
append
使用Ljava/lang/String
(字符串)参数调用,而A + 'B'
则使用C
调用({1}}一个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编译器生成的字节码,它几乎完全相同。所以至少这两个编译器的结果并不相同。
EDIT2: 现在有趣的部分
基准。这个结果是通过在预热后运行{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'
比a+"B"
快10% ......
...至少(免责声明)我的系统 我的编译器和我的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循环性能,对象创建性能以及对变量性能的赋值。所以真正的速度差异可能会更大一些。
答案 1 :(得分:3)
当我有一个字符串,我需要将单个字符串连接到它的结尾时,我应该更喜欢s = .... +']'而不是s = .... +“]”出于任何性能原因?< / p>
这里实际上有两个问题:
Q1:是否存在性能差异?
答案:这取决于......
在某些情况下,可能是,取决于JVM和/或字节码编译器。如果字节码编译器生成对StringBuilder.append(char)
而不是StringBuilder.append(String)
的调用,那么您会期望前者更快。但JIT编译器可以将这些方法视为“内在函数”,并使用一个字符(文字)字符串优化对append(String)
的调用。
简而言之,您需要在平台上对此进行基准测试才能确定。
在其他情况下,肯定没有区别。例如,这两个调用将编译相同的字节码序列,因为串联是常量表达式。
System.out.println("234" + "]");
System.out.println("234" + ']');
这是由JLS保证的。
Q2:你喜欢一个版本吗?
答案:
从一般意义上讲,这可能是一个过早的优化。如果您在应用程序级别分析了代码并确定代码段对性能有可测量的影响,那么出于性能原因,您应该只选择一个表单而不是其他 。< / p>
如果您已对代码进行了分析,请使用Q1的答案作为指导。
如果值得尝试优化代码段,那么在优化后重新运行基准测试/分析是至关重要的,看它是否有所不同。你对什么是最快的直觉以及你在互联网上的一篇旧文章中所读到的内容......可能非常错误。
答案 2 :(得分:0)
没有!那是不成熟的优化,完全浪费你的时间。喜欢你喜欢的任何一种,并使用任何一种作品。有时']'不会自动提升为“]”。我希望JIT能够优化你认为你衡量的任何性能差异。在它工作的情况下,它起作用,因为']'被转换为“]”(在你的例子中)。现在,直接使用StringBuilder
和append(char)
可能会稍快一点......但是,这种差异在任何实际应用中都没有意义。
修改强>
我能给你的关于表现的最佳建议是: