5

背景

我一直在运行一个代码(发布在底部)来测量显式 Java 向下转换的性能,并且我遇到了我觉得有点异常的情况……或者可能是两个异常情况。

我已经看过这个关于 Java 转换开销的线程,但它似乎只谈论一般的转换,而不是这种特殊现象。这个线程涵盖了类似的主题,我真的不需要关于过早优化的建议——我正在调整我的应用程序的部分以获得最佳性能,所以这是合乎逻辑的步骤。

测试

我基本上想测试向下转换的性能与对象的.toString()方法,对象是Strings,但类型为Objects。因此,我创建了具有相同内容的 aString a和 an 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解决方案的建议,我再次使用一些编译器输出运行我的基准测试。第一个循环塞满了初始化的东西,所以我放入了一个“垃圾”循环来执行此操作。完成后,结果更接近预期。

这是使用 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 ******/
4

2 回答 2

7

基准是有缺陷的,因为 SO 和其他地方的大多数问题都与基准 Java 代码有关。您测量的东西比您想象的要多得多,例如 JIT 编译方法、HotSpot 优化循环等。

检查http://www.ibm.com/developerworks/java/library/j-jtp02225/index.html

此外,服务器虚拟机和客户端虚拟机的行为不同(JVM 在客户端启动得更快,但运行速度会慢一些,因为它在编译时开始解释字节码,而服务器虚拟机在运行前对其进行编译)等等。

GC 也可能会产生干扰,如果您在基准测试期间获得任何 Full GC(通常 Full GC 会每隔一个线程完全暂停),则更是如此。即使是很小的集合也可能会产生一些影响,因为它们可以使用相当多的 CPU 来清理循环内可能产生的巨大混乱。

要进行适当的基准测试,您应该“预热”JVM,打开 JVM 的大量输出以确保您正在测量什么等。

在 SO 上查看这个问题,它解决了如何在 Java 中编写基准测试,包括我上面提到的主题以及更详细的内容:如何在 Java 中编写正确的微基准测试?

于 2013-01-23T03:47:12.520 回答
2

为什么调用 .toString() 比已有 String 对象更快?

从数字上看,我根本没有看到 Loop 2 始终比 Loop3 快。事实上,在某些情况下它会更慢。测试 B 中明显的显着差异可能只是 GC 在 Loop 3 的情况下比在 Loop 2 的情况下运行了一次。这可能只是基准设计的产物。

无论如何,如果您真的想知道发生了什么(如果有的话),您需要查看 JIT 编译器在每种情况下生成的本机指令。(有 JVM 选项可以做到这一点......)

于 2013-01-23T04:55:37.983 回答