文档结构  
可译网翻译有奖活动正在进行中,查看详情 现在前往 注册?
原作者:Peter Verhas (2016-09-13)    来源:Dzone [英文]
coyee    计算机    2016-09-14    0评/717阅
翻译进度:59%   参与翻译: KeYIKeYI (6), 凭海临风 (4), lison (1)

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

三年前我写过一篇名叫 《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 积分)

Playing a Bit

In the book Java 9 Programming By Example, one of the examples is the Mastermind game. Chapter 5 is all about solving the game in parallel to speed up the guessing. (If you do not know the game, please read about it on Wikipedia, I do not want to explain it here, but you will need it to understand it.)

The normal guessing is simple. There is a hidden secret. The secret is four pegs of four different colors out of 6 colors. When we guess, we take the possible color variations our of the equation, one after the other, and ask the question the table: "If this selection is the secret, are all answers correct?" In other words: can this guess be hidden, or is there some contradiction in the answers for previous answers? If this guess can be the secret, then we will give it a try, putting the pegs on the table. The answer may be 4/0 (alleluia) or something else. In the latter case, we go on searching. This way the 6 color, 4 columns table can be solved in five steps.

第 11 段(可获 2.38 积分)

For the shake of simplicity and visualization we name the colors with numbers, like 01234456789 (we have ten colors in the JMH benchmark, since 6 colors are just not enough) and 6 pegs. The secret we use is 987654, because this is the last guess as we go from 123456, 123457, and so on.

When I first coded this game in August 1983 on a Swedish school computer (ABC80) in BASIC, each guess took 20 to 30 seconds on the z80 processor running on 40MHz with 6 colors, 4 positions. Today my MacBook Pro can play the whole game using single-thread approximately 7 times in a second using 10 colors and 6 pegs. But that is not enough when I have 4 processors in the machine supporting 8 parallel threads.

第 12 段(可获 1.63 积分)

To speed up the execution, I split up the guess space into equal intervals and I start separate guessers, each spitting guesses into a blocking queue. The main thread reads from the queue and puts the guesses on the table as they come. There is some post processing that may be needed in case some of the threads create a guess that becomes outdated by the time the main thread tries to use it as a guess, but still we expect a huge increase in speed.

Does it really speed up the guessing? That is JMH here for.

To run the benchmark, we need some code that actually executes the game:

@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 积分)

The JMH framework will execute the code several times, measuring the time to run with several parameters. The method playParallel will be executed to run the algorithm for 1, 4, 5, 10, and 32 threads, each with 1, 10, 100, and one million maximum queue length. When the queue is full, the individual guessers stop with their guessing until the main thread pulls at least one guess off the queue.

I suspect if we have many threads and we do not limit the length of the queue, then the worker threads will fill the queue with initial guesses that are just based on an empty table and thus do not deliver much value. What do we see after almost 15 minutes of execution?

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 积分)

(For the score, the greater the value, the better.) It shows that the best performance we get if we start 16 threads and if we somewhat limit the length of the queue. Running the parallel algorithm on one thread (a mater and a worker) is somewhat slower than the single thread implementation. This seems to be okay: we have the overhead of starting a new thread and communication between the threads. The maximum performance we have is around 16 threads. Since we can have 8 cores in this machine we expected a peek of around 8. Why is that?

What happens if we replace the standard secret 987654 (which is boring after a while even for a CPU) with something random?

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 积分)

The performance increases, since we do not need to go though all the possible variations. In the case of one thread, there is a 100% increase. In case of multiple threads, the performance does not improve that much. Note that this does not speed the code itself up, it only measures more realistically using statistical, random secrets. What we can also see is that the gain of 16 threads over 8 threads is not significant anymore. This is significant only when we select a secret that is towards the end of the variations. Why? From what you have seen here and from the source code available in GitHub, you can answer that question yourself.

第 16 段(可获 1.43 积分)

Summary

The book  Java 9 Programming By Example is planned to be released February 2017. But since we are living in an open source world you can get access controlled by the publisher to 1.x.x-SNAPSHOT versions. Now I told you the preliminary GitHub URL that I use while I develop code for the book, you can also preorder the eBook and give feedback to help me to create a better book!

第 17 段(可获 0.91 积分)

文章评论