为什么String-casting循环似乎有静态开销?

时间:2013-01-23 03:44:37

标签: java performance casting

背景

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

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

测试

我基本上想要测试向下转换的效果与.toString()方法对String s的对象进行测试,但输入为Object s。所以,我创建了一个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 ******/

2 个答案:

答案 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选项可以做到这一点......)