Java理论和实践: 一个有缺陷的微基准的剖析 - 编程入门网
知情的情况下,编译器可能将此操作部分地或者完全地优化掉,使得测试运行起来比预期更快。如果在基准中加入无关的代码 Y,那么现在度量的就是 X+Y 的性能,更糟糕的是,由于 Y 的存在,现在 JIT 优化 X 的方式又发生了变化。如果没有足够的额外填充物和数据流依赖,编译器可能会将整个程序优化至无形,但是如果填充物太多,那么真正需要度量的东西又会迷失在噪音当中,因此要编写一个良好的微基准,就意味着要抓住二者之间微妙的平衡。
因为运行时编译使用概要数据来指导优化,所以 JIT 对测试代码的优化可能不同于对实际代码的优化。对于所有的基准,都存在这样一个很大的风险,即编译器能够优化掉整个基准,因为它将(正确地)认识到基准代码实际上没有做任何事情,或者没有产生任何有用的结果。在编写有效的基准时,要求我们能够“愚弄”编译器,即使它认识到代码没有用处,也不能让它将代码砍掉。在 Incrementer 类中使用计数器变量骗不到编译器,在删除无用代码方面我们对编译器给予了信任,但编译器比我们想象的还要聪明。 此外,还有一个问题是,同步是一种内建的语言特性。JIT 编译器可以随意变动同步锁,以减少它们的性能成本。在某些情况下,同步可能被完全消除,并且在同一个监视器上,同步的邻近同步锁可能被合并。如果我们要度量同步的成本,这些优化实际上害了我们,因为我们不知道有多少同步会被优化掉(在这个例子中,很可能是全军覆没!)。更糟糕的是,JIT 对于 SyncTest.increment() 中不做事的代码的优化与对实际中的程序的优化在方式上有很大的不同。 更糟的还在后面。这个微基准表面上的目的是测试同步与 ReentrantLock 哪个更快。由于同步是内建在语言中的,而 ReentrantLock 是一个普通的 Java 类,编译器对于不做事的同步的优化与对于不做事的 ReentrantLock 的优化在方式上又有不同。这样的优化会使不做事的同步看上去更快些。编译器对此二者的优化方式存在差别,加上对基准和对实际代码的优化方式也是不相同的,因此程序的结果几乎无法告诉我们实际情况下两者在性能上存在的差别。 无用代码的消除 在12 月份的文章中,我讨论了基准中无用代码的消除问题 —— 由于基准常常不做有用的事,因此编译器可能会整块地砍掉基准代码,从而歪曲了对执行时间的度量。基准在很多方面都存在这样的问题。虽然编译器消除无用代码这件事对我们要做的事还不一定会造成致命打击,但这里的问题是,编译器对于两种代码路径可以执行不同程度的优化,这从根本上歪曲了我们的度量。 两个 Incrementer 类的用途是做一些无用的工作(让一个变量递增)。但聪明的 JVM 会发现,这两个计数器变量从来没有被访问过,因此可以消除与使这些变量递增有关的代码。正是这里存在一个严重问题 —— 现在 SyncIncrementer.increment() 方法中的 synchronized 块是空的,编译器可以整个地删除它,而 LockIncrementer.increment() 却仍然包含锁代码,编译器可能会将其完全删除,也可能不会这样做。您可能会想,这部分代码有利于同步 —— 编译器更可能会删除这部分代码 —— 但这样的事情只有在不做事的基准中才如此普遍,而在精心编写的实际代码中就少见得多。 编译器对某种实现比对另一种实现要优化得多一些,但是这种差别只在不做事的基准中才会体现出来,这个问题导致比较同步和 ReentrantLock 的性能是如此之困难。 Java理论和实践: 一个有缺陷的微基准的剖析(3)时间:2011-02-04 IBM Brian Goetz循环展开和锁合并 即使编译器不消除计数器管理,它也仍会以不同的方式优化两个 increment() 方法。标准的优化是循环展开;编译器将展开循环,以减少分支的数量。展开多 |
凌众科技专业提供服务器租用、服务器托管、企业邮局、虚拟主机等服务,公司网站:http://www.lingzhong.cn 为了给广大客户了解更多的技术信息,本技术文章收集来源于网络,凌众科技尊重文章作者的版权,如果有涉及你的版权有必要删除你的文章,请和我们联系。以上信息与文章正文是不可分割的一部分,如果您要转载本文章,请保留以上信息,谢谢! |