【发布时间】:2013-01-23 03:44:36
【问题描述】:
背景
我一直在运行代码(发布在底部)来测量显式 Java 向下转换的性能,我遇到了我觉得有点异常的情况……或者可能是两个异常情况。
我已经看过 this thread 关于 Java 转换开销,但它似乎只谈论一般的转换,而不是这种特殊现象。 This thread 涵盖了类似的主题,我真的不需要关于过早优化的建议——我正在调整我的应用程序的某些部分以获得最佳性能,所以这是合乎逻辑的步骤。
测试
我基本上想测试向下转换与.toString() 方法的性能,对象是Strings,但类型为Objects。所以,我创建了一个 String a 和一个 Object b 具有相同的内容,运行三个循环,并为它们计时。
- 循环 1 是
((String) b).toLowerCase(); - 循环 2 是
b.toString().toLowerCase(); - 循环 3 是
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 ******/
【问题讨论】:
-
尝试以不同的顺序运行循环和测试。有什么不同吗?例如,如果您将循环 1 放在测试 D 的最后会发生什么?
-
Java 非常擅长优化,编译后的循环行为可能看起来完全不同。
-
@DWright 很棒的电话。在第一个循环中肯定会发生一些事情。
-
这个输出听起来像
-XX:+PrintCompilation和-verbose:gc,你可以看到很多 GC 活动(因为字符串是不可变的,并且循环内的许多调用会产生将被丢弃的新字符串 - - 虽然它运行得很快!) -
很高兴它成功了。在我离开之前,只是一个可能有帮助的提示:通常 HotSpot 会在调用 20k(或 10k,取决于客户端/服务器 vm)次后优化方法。因此,要预热,通常复制和粘贴您想要在循环中进行基准测试的代码,该循环调用您的方法 20k 次正是您所需要的。 :)
标签: java performance casting