我一直在运行一个代码(在底部发布)来测量显式Java向下转换的性能,而且我遇到了我觉得有点异常......或者可能是两个异常。
我已经看过this thread关于Java构建开销的问题,但它似乎只讨论了一般的转换,而不是这种特殊现象。 This thread涵盖了类似的主题,我并不需要关于过早优化的建议 - 我正在调整应用程序的一部分以获得最佳性能,因此这是合乎逻辑的步骤。
我基本上想要测试向下转换的效果与.toString()
方法对String
s的对象进行测试,但输入为Object
s。所以,我创建了一个String a
和一个Object b
,内容相同,运行了三个循环,然后计时。
((String) b).toLowerCase()
; b.toString().toLowerCase()
; a.toLowerCase()
。(以毫秒为单位的测量值。)
iters | Test Round | Loop 1 | Loop 2 | Loop 3
-----------|--------------|----------|----------|----------
50,000,000 | 1 | 3367 | 3166 | 3186
Test A | 2 | 3543 | 3158 | 3156
| 3 | 3365 | 3155 | 3169
-----------|--------------|----------|----------|----------
5,000,000 | 1 | 373 | 348 | 369
Test B | 2 | 373 | 348 | 370
| 3 | 399 | 334 | 371
-----------|--------------|----------|----------|----------
500,000 | 1 | 66 | 36 | 33
Test C | 2 | 71 | 36 | 41
| 3 | 66 | 35 | 34
-----------|--------------|----------|----------|----------
50,000 | 1 | 27 | 5 | 5
Test D | 2 | 27 | 6 | 5
| 3 | 26 | 5 | 5
-----------|--------------|----------|----------|----------
long t, iters = ...;
String a = "String", c;
Object b = "String";
t = System.currentTimeMillis();
for (int i = 0; i < iters; i++) {
c = ((String) b).toLowerCase();
}
System.out.println(System.currentTimeMillis() - t);
t = System.currentTimeMillis();
for (int i = 0; i < iters; i++) {
c = b.toString().toLowerCase();
}
System.out.println(System.currentTimeMillis() - t);
t = System.currentTimeMillis();
for (int i = 0; i < iters; i++) {
c = a.toLowerCase();
}
System.out.println(System.currentTimeMillis() - t);
我觉得最令人着迷的是循环2(.toString()
)似乎表现出三者中最好的(特别是在测试B中) - 这没有直观意义。 为什么调用.toString()
比拥有String
对象更快?
困扰我的另一件事是它不会扩展。如果我们比较测试A和D,当它们相互比较时它们会偏离9倍(27 * 1000 = 27000,而不是3000); 为什么迭代次数会出现这种巨大的差异?
有人能解释为什么这两个异常被证明是真的吗?
更新:根据Bruno Reis solution的建议,我再次使用一些编译器输出运行基准测试。第一个循环塞满了初始化的东西,所以我放入一个“垃圾”循环来做到这一点。一旦完成,结果就更接近预期。
这是控制台的完整输出,使用5,000,000次迭代(由我评论):
50 1 java.lang.String::toLowerCase (472 bytes)
50 2 java.lang.CharacterData::of (120 bytes)
53 3 java.lang.CharacterDataLatin1::getProperties (11 bytes)
53 4 java.lang.Character::toLowerCase (9 bytes)
54 5 java.lang.CharacterDataLatin1::toLowerCase (39 bytes)
67 6 n java.lang.System::arraycopy (0 bytes) (static)
68 7 java.lang.Math::min (11 bytes)
68 8 java.util.Arrays::copyOfRange (63 bytes)
69 9 java.lang.String::toLowerCase (8 bytes)
69 10 java.util.Locale::getDefault (13 bytes)
70 1 % Main::main @ 14 (175 bytes)
[GC 49088K->360K(188032K), 0.0007670 secs]
[GC 49448K->360K(188032K), 0.0024814 secs]
[GC 49448K->328K(188032K), 0.0005422 secs]
[GC 49416K->328K(237120K), 0.0007519 secs]
[GC 98504K->352K(237120K), 0.0122388 secs]
[GC 98528K->352K(327552K), 0.0005734 secs]
595 1 % Main::main @ -2 (175 bytes) made not entrant
548 /****** Junk Loop ******/
597 2 % Main::main @ 61 (175 bytes)
[GC 196704K->356K(327552K), 0.0008460 secs]
[GC 196708K->388K(523968K), 0.0005100 secs]
343 /****** Loop 1 ******/
939 2 % Main::main @ -2 (175 bytes) made not entrant
940 11 java.lang.String::toString (2 bytes)
940 3 % Main::main @ 103 (175 bytes)
[GC 393092K->356K(523968K), 0.0036496 secs]
377 /****** Loop 2 ******/
1316 3 % Main::main @ -2 (175 bytes) made not entrant
1317 4 % Main::main @ 145 (175 bytes)
[GC 393060K->332K(759680K), 0.0008326 secs]
320 /****** Loop 3 ******/
答案 0 :(得分:7)
基准测试存在缺陷,因为SO和其他地方的大多数问题都与Java代码基准测试有关。您测量的内容远远超出您的想象,例如JIT编译方法,HotSpot优化循环等。
检查http://www.ibm.com/developerworks/java/library/j-jtp02225/index.html。
此外,服务器VM和客户端VM的行为也不同(JVM在客户端启动速度更快,但运行速度慢一段时间,因为它在编译时开始解释字节码,而服务器VM在运行之前编译它)等。
GC可能也会产生干扰,如果您在基准测试期间获得任何Full GC(通常是Full GCs完全暂停其他所有线程),则更是如此。即使是次要的集合也可能会产生一些影响,因为它们可以使用相当多的CPU来清理循环中可能存在的巨大混乱。
要做一个适当的基准测试,你应该“预热”JVM,打开JVM的大量输出,以确定你正在测量的内容等。
在SO上查看这个问题,该问题涉及如何用Java编写基准,包括我上面提到的主题以及更详细的内容:How do I write a correct micro-benchmark in Java?
答案 1 :(得分:2)
为什么调用.toString()比已经拥有String对象更快?
看看数字,我没有看到Loop 2总是比Loop3快。实际上,在某些情况下它更慢。测试B中明显的显着差异可能只是GC在Loop 3情况下再次运行,而不是Loop 2情况。这可能只是基准设计的人工制品。
无论如何,如果你真的想知道发生了什么(如果有的话),你需要查看JIT编译器在每种情况下生成的本机指令。 (有JVM选项可以做到这一点......)