文档结构  
翻译进度:已翻译     翻译赏金:10 元 (?)    ¥ 我要打赏

我已经好几个月没在这里写文章了,并且不出意外的话以后也会这样。因为我打算明年三月再重新开始写作。我将在本文最后解释为什么。等等! 并不一定是最后,因为你可以直接滚动到最后。反正是在文章底部前面的某个地方,别管它了,开始阅读吧!

三年前我写过一篇名叫 《Java编译器如何优化执行代码》  的文章. 更确切的说, 是为什么javac不做这些工作,但同时,JIT却优化了代码.于是我做了一些基础测试, 一些 Esko Luontola 提到的非常严苛的测试. 这些测试表明,JIT在执行代码之前就已经做了优化,它甚至可以收集有关代码的执行显著效果的统计数据。

第 1 段(可获 1.64 积分)

这篇文章是2013年一月份写的, 第一次源代码提交到JMH项目 (Java Microbenchmark Harness) 是在两个月以后。 从那时起, harness 进行了大量开发工作, 明年他将成为下一个Java发行版的一部分. 我也有了写一本关于Java 9 的稿约, 这本书的第五章将会涵盖Java 9微基准测试套件以及其他的内容.同时也有了一个深入研究JMH项目的好理由。

在进入到如何使用 JMH以及他的优势的细节之前, 让我们先聊一点关于微基准测试套件的话题吧.

第 2 段(可获 1.33 积分)

微基准测试标准

微基准测试标准是衡量一小段代码性能的标准. 他很少被用到, 在我们开始对一段真实生产环境的代码进行微基准测试之前, 我们需要考虑再三。请记住:过早的优化是万恶之源. 一些开发人员创造了一个更广泛的类似说法,说优化本身是一切罪恶的根源,这可能是正确的,特别是我们提到的微基准测试.

微基准测试标准是一个在我们不知道这段代码是否需要调优的诱人的调优小工具.。当我们在一个庞大的应用程序中有几个模块运行在多个服务器上的时候,我们怎样才能确保调优一些特殊的应用能够极大提高部分整体应用性能吗? 这是否会陷入通过增加对性能和开发投入就一定能获得增加收入、提高收益的误区? 坦率的说你并不清楚,因为这个说法太过宽泛。当然基于统计学的理论,毫无疑问通过调优,包括微基准测试,可以节省很多时间。但是实际情况让我们还是很受伤。你可能还没注意到,或者还没有尝试这样去做,但是实际情况往往和你的想象相差甚远。

第 3 段(可获 2.49 积分)

何时我们会用到微基准测试?我所能预见到的至少涵盖以下三个领域:

  1. 如果你想去写一篇关于微基准测试标准文章的时候。
  2. 你能够识别应用中那些消耗系统资源巨大的代码片段,并且通过有效的微基准测试可以有效改善这种情况的时候。
  3. 和上面的情况有所不同,你不能识别这些代码片段,但是你对应用微基准测试的实际效果有所怀疑的时候。

哈哈,这只是个玩笑. 当你能够玩转微基准测试标准,并理解它是如何工作的,也能够清楚Java代码是如何工作的时候,它就不是个笑话了。去年, Takipi 发表了一篇文章,是描述关于他们如何衡量lambdas表达式的性能。通过阅读它, 我认为这是个很好的文章,它很好的证明了博客比出版打印的图书优势到底在哪里。读者通过添加评论及时纠正了文章中出现的一些错误。

第 4 段(可获 1.81 积分)

第二部分是讲通常的例子.好吧,在读者纠正我之前,让我说得更清楚些:第二部分应该是讲通常的例子.第三部分是讲当你开发一个库你却无法知道使用这个库的所有应用时你该怎么做.在那种情况下,你可能会试着凭借你的想象和对应用的怀疑对某个部分进行优化.但哪怕就是在那种情况下,做一些示例应用仍是一个更好的注意.

缺点

微基准测试的缺点是什么?它目前仍然作为实验特性.我编写的第一个程序是TI计算器,我只能数程序用了多少步来对两个大素数(当时是10位数)求公因数.但即使是那个时候,我也使用了一个很老的俄罗斯码表测量了运行时间,因为我实在太懒了,懒得计算这个步数.实验和测量确实更轻松.

第 5 段(可获 1.9 积分)

现在你不可能计算CPU运行的步数.这里面有太多程序员无法掌控的微小因素可能影响应用的性能,因此,计算步数是不可能的.

那么测量的最大问题是什么呢/假设我们对某个东西X感兴趣,但我们通常测量不了它.所以我们测量Y作为替代并希望Y的值和X具有可比性.我们想测量的是房间的长度,但我们会测量激光束从房间一端到另一端所花的时间作为替代.在这个例子中,长度X和时间Y是紧密关联的.大部分时候,X和Y是或多或少相关的.大部分时候,当人们去测量值的时候,X和Y根本又互相没有关联.然而人们仍然为这些测量得出的结果花费他们的钱以及制定更多的决策.你可以以政治选举为例.

第 6 段(可获 2.19 积分)

微基准测试(与政治选举)并无不同.几乎从没做好过.如果你对具体细节和微基准测试可能的缺点感兴趣,Aleksey Shipilev有个非常好的一小时的视频.微基准测试的首要问题是如何测量运行时间.代码片段在很短的时间内运行完,在测量开始和结束时System.currentTimeMillis() 可能会返回相同的值,因为我们仍然在同一微秒内.即使运行时间是10ms,测量的误差仍然至少为10%,这只是因为我们测量花费时间的量化.幸运的是,我们有方法System.nanoTime().我们真的很开心,文森特?

第 7 段(可获 1.28 积分)

现在还真不是高兴的时候.如文档描述的那样,nanoTime()以纳秒的格式返回正在运行的Java虚拟机的高精度时间源的当前值.什么是"当前值"?调用是什么时候发生的?或者它什么时候返回的?或者某个时候两者都是什么时候发生的?上面任何一个问题,你都很可能不能回答.那个当前值在过去的1000ns之内可能都是一样的,因为这就是Java实现所能保证的极限.

在使用NanoTime()之前,文档中还给出了一个警告:"连续调用之间的差值跨度超过大约292年(263纳秒)时由于数字溢出将不能正确地计算经过的时间."292年?真的假的?

第 8 段(可获 1.3 积分)

这儿还有些其他问题.当你启动Java代码时,刚开始的几千次代码运行是解释执行或运行在没有运行时优化情况下.JIT对比像Swift,C,C++和等静态编译型语言的编译器有其优势----JIT可以从代码运行中收集运行时信息.当JIT通过最近的运行时统计发现上次进行的编译可以被优化,它就再次编译这些代码(编译为机器码).垃圾收集也做了同样的优化----垃圾收集也尝试使用统计数据来调整它的操作参数.因此,那些写的很好的服务器应用会随着时间推移得到很高的性能.它们启动时候有一点慢,然后变得越来越快.如果你重启服务器,整个由慢到快的周期会重新开始.

第 9 段(可获 1.68 积分)

如果你进行微基准测试,你要小心这个行为.你希望测量应用在热身期间的性能或者它实际是如何运行的?

这些问题的解决方案是微基准测试工具,该工具试图考虑到所有这些影响性能的因素.JMH是在Java 9中将引入的微基准测试工具.

JMH是什么?

"JMH是一个Java工具,用于构建,运行和分析纳秒级/微秒级/毫秒级/宏观级的以Java或其他以JVM为目标的语言写成的基准测试."(来自官网)

你可以将JMH作为一个单独的项目独立于你测试的真实项目运行,或者你可以只把测试代码放在一个单独的目录.微基准测试工具将会针对产品的类文件编译并执行基准测试.对我来说,完成这步最简单的方法是使用Gradle插件执行JMH.使用这个方法,你要把测试代码存放在一个名为jmh的目录(与main和test同一层级)并创建一个可以运行基准测试的主类.

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.io.IOException;

public class MicroBenchmark {

    public static void main(String... args) throws IOException, RunnerException {
        Options opt = new OptionsBuilder()
                .include(MicroBenchmark.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    }

上面的代码使用了一个用于配置的构造器接口(这使得代码优雅)和一个可以执行基准测试的运行类.

第 10 段(可获 2.38 积分)

稍作解释

在“Java 9 编程实例”一书中,其中一个例子是珠玑妙算游戏第 5 章是关于平行解读游戏以加快猜测。 (如果你不知道这个游戏,请阅读维基百科,我在此不做详细解释,你可以自己去了解)

普通猜测很简单。但这个游戏有一个隐藏破译,那就是在六个棋子中有四个颜色互不相同。当我们进行猜测时, 我们将可能的颜色从公式中选出,每一次选择可以向庄提问一次:“如果这(颜色)个是对的,是不是所有的答案都正确?“换句话说,这个猜测可以作为隐藏破译吗?还是这个猜测与前面的答案相悖了?如果这一个猜测对了,我们可以继续尝试,把这个棋子放在桌上。答案可能是 4/0(上帝保佑吧)或其他。如果是后者,我们可以继续搜寻答案。这样,6 色 4 列的局可以分五步解答。

第 11 段(可获 2.38 积分)

为了简单形象,我们用数字命名颜色, 即0123456789(我们在JMH基准中使用十种颜色, 因为6种颜色是不够的)和6个位置。我们使用的秘密是987654,因为这是我们从123456开始,到123457等之后所做出的选择。

当我在1983年8月在瑞典学校的一台计算机(ABC80)上使用BASIC语言编写这个游戏时, 在以40mhz的频率运行的Z80处理器上使用6种颜色和4个位置,每一种猜测都要花费20到30秒的时间。现在,我的MacBook Pro使用10种颜色和6个位置,在单线程情况下,可以在一秒钟内完成游戏七次。 但当我有支持8个并行线程的4核处理器时,这就不够了。

第 12 段(可获 1.63 积分)

为了加快执行,我将猜测范围等分到两个阻塞队列,分别进行猜测。主线程从队列读取一个数值并原样放入表中。还有一些后期过程需要处理,因为这些线程创建的测试过期后主线程仍然会使用它作用猜测,但我们仍然期望速度能大幅度提升。

提升猜测速度真有可能吗?这里有 JMH。

为了运行参照标准,我们需要一些实际游戏的代码:

@State(Scope.Benchmark)
public static class ThreadsAndQueueSizes {
    @Param(value = {"1", "4", "8", "16", "32"})
    String nrThreads;
    @Param(value = { "1", "10", "100", "1000000"})
    String queueSize;

}

@Benchmark
@Fork(1)
public void playParallel(ThreadsAndQueueSizes t3qs) throws InterruptedException {
    int nrThreads = Integer.valueOf(t3qs.nrThreads);
    int queueSize = Integer.valueOf(t3qs.queueSize);
    new ParallelGamePlayer(nrThreads, queueSize).play();
}

@Benchmark
@Fork(1)
public void playSimple(){
    new SimpleGamePlayer().play();
}

 

第 13 段(可获 1.39 积分)

JMH 框架会多次执行代码,测量跑几个参数的时间。playParallel 方法会在1、4、5、10 和 32 个线程中[译者注:看下表应该是1、4、8、16、32个线程才对] 执行算法,各自又分别处理了 1、10、100 和百万长度的队列。当队列排满的时候,相应的猜测者程序停止猜测,直到主线程至少从队列中取出一个猜测。

我怀疑如果我们有很多线程,而且没有限制队列的长度,那么工作线程会仅仅根据空表进行初始的猜测来填充队列,这就不会填充太多的值。我们运行 15 分钟来看看?

Benchmark                    (nrThreads)  (queueSize)   Mode  Cnt   Score   Error  Units
MicroBenchmark.playParallel            1            1  thrpt   20   6.871 ± 0.720  ops/s
MicroBenchmark.playParallel            1           10  thrpt   20   7.481 ± 0.463  ops/s
MicroBenchmark.playParallel            1          100  thrpt   20   7.491 ± 0.577  ops/s
MicroBenchmark.playParallel            1      1000000  thrpt   20   7.667 ± 0.110  ops/s
MicroBenchmark.playParallel            4            1  thrpt   20  13.786 ± 0.260  ops/s
MicroBenchmark.playParallel            4           10  thrpt   20  13.407 ± 0.517  ops/s
MicroBenchmark.playParallel            4          100  thrpt   20  13.251 ± 0.296  ops/s
MicroBenchmark.playParallel            4      1000000  thrpt   20  11.829 ± 0.232  ops/s
MicroBenchmark.playParallel            8            1  thrpt   20  14.030 ± 0.252  ops/s
MicroBenchmark.playParallel            8           10  thrpt   20  13.565 ± 0.345  ops/s
MicroBenchmark.playParallel            8          100  thrpt   20  12.944 ± 0.265  ops/s
MicroBenchmark.playParallel            8      1000000  thrpt   20  10.870 ± 0.388  ops/s
MicroBenchmark.playParallel           16            1  thrpt   20  16.698 ± 0.364  ops/s
MicroBenchmark.playParallel           16           10  thrpt   20  16.726 ± 0.288  ops/s
MicroBenchmark.playParallel           16          100  thrpt   20  16.662 ± 0.202  ops/s
MicroBenchmark.playParallel           16      1000000  thrpt   20  10.139 ± 0.783  ops/s
MicroBenchmark.playParallel           32            1  thrpt   20  16.109 ± 0.472  ops/s
MicroBenchmark.playParallel           32           10  thrpt   20  16.598 ± 0.415  ops/s
MicroBenchmark.playParallel           32          100  thrpt   20  15.883 ± 0.454  ops/s
MicroBenchmark.playParallel           32      1000000  thrpt   20   6.103 ± 0.867  ops/s
MicroBenchmark.playSimple            N/A          N/A  thrpt   20   6.354 ± 0.200  ops/s

 

第 14 段(可获 1.54 积分)

(分数值越大越好)。它显示了如果我们启动16个线程,并且稍微限制一下队列的长度,我们能够得到的最好的性能。 在一个线程(一个master和一个worker)上运行并行算法比单线程实现要慢一点。 这似乎没什么问题:我们有启动新线程和线程之间通信的开销。 我们拥有的最大性能大约是16个线程。 由于我们在这台机器上有8个内核,我们预计会看到大约8个线程。这是为什么呢?

如果我们用随机数来替换标准的987654(即使对于CPU来说,一段时间后也会无聊),会发生什么呢?

Benchmark                    (nrThreads)  (queueSize)   Mode  Cnt   Score   Error  Units
MicroBenchmark.playParallel            1            1  thrpt   20  12.141 ± 1.385  ops/s
MicroBenchmark.playParallel            1           10  thrpt   20  12.522 ± 1.496  ops/s
MicroBenchmark.playParallel            1          100  thrpt   20  12.516 ± 1.712  ops/s
MicroBenchmark.playParallel            1      1000000  thrpt   20  11.930 ± 1.188  ops/s
MicroBenchmark.playParallel            4            1  thrpt   20  19.412 ± 0.877  ops/s
MicroBenchmark.playParallel            4           10  thrpt   20  17.989 ± 1.248  ops/s
MicroBenchmark.playParallel            4          100  thrpt   20  16.826 ± 1.703  ops/s
MicroBenchmark.playParallel            4      1000000  thrpt   20  15.814 ± 0.697  ops/s
MicroBenchmark.playParallel            8            1  thrpt   20  19.733 ± 0.687  ops/s
MicroBenchmark.playParallel            8           10  thrpt   20  19.356 ± 1.004  ops/s
MicroBenchmark.playParallel            8          100  thrpt   20  19.571 ± 0.542  ops/s
MicroBenchmark.playParallel            8      1000000  thrpt   20  12.640 ± 0.694  ops/s
MicroBenchmark.playParallel           16            1  thrpt   20  16.527 ± 0.372  ops/s
MicroBenchmark.playParallel           16           10  thrpt   20  19.021 ± 0.475  ops/s
MicroBenchmark.playParallel           16          100  thrpt   20  18.465 ± 0.504  ops/s
MicroBenchmark.playParallel           16      1000000  thrpt   20  10.220 ± 1.043  ops/s
MicroBenchmark.playParallel           32            1  thrpt   20  17.816 ± 0.468  ops/s
MicroBenchmark.playParallel           32           10  thrpt   20  17.555 ± 0.465  ops/s
MicroBenchmark.playParallel           32          100  thrpt   20  17.236 ± 0.605  ops/s
MicroBenchmark.playParallel           32      1000000  thrpt   20   6.861 ± 1.017  ops/s

 

第 15 段(可获 1.51 积分)

因为我们不需要去考虑所有可能的变化,性能得到了提升。 在单线程的情况下,性能提升了100%。 在多线程的情况下,性能并没有提高那么多。 请注意,这并不会使代码本身加速,它只是使用统计,随机破译来更实际地度量。 我们还可以看到,16个线程比起8个线程的增益不再显著。 只有当我们选择一个接近变化结束的破译时,这一点才是重要的。 为什么? 从您在这里看到的和GitHub中提供的源代码中,您可以自己找到这个问题的答案。

第 16 段(可获 1.43 积分)

小结

Java 9 编程实例一书计划在 2017 年 2 月出版。不过既然我们都生活在开源时代,你可以访问由出版商控制的 1.x.x-SNAPSHOT 版本。现在告诉你, 一开始的 Github URL 是我为了写这本书开发的代码,你可以购买电子书并进行反馈,帮助我把书写得更好!

 

第 17 段(可获 0.91 积分)

文章评论