在 JVM 中分析块的正确方法?

Correct way to profile a block in JVM?

我看到很多帖子建议使用类似于下面的代码来分析 Scala 中的方法:

  def timer[A](block: => A): A = {
    val startTime = System.currentTimeMillis()
    val ret = block
    println(s"${System.currentTimeMillis() - startTime}ms")
    ret
  }

理由是 pass-by-nameblock。但是,当我尝试代码片段时,我发现结果不可靠。

object Euler006 extends App {
  def f1(n: Int): Long = {
    val numbers = 1 to n
    def square(n: Int) = n.toLong * n
    square(numbers.sum) - numbers.map(square).sum
  }

  def f2(n: Int): Long = (n.toLong - 1) * n / 2 * (n + 1) / 3 * (3 * n + 2) / 2

  {
    val r2 = timer(f2(10000))
    val r1 = timer(f1(10000))
    println(s"$r1 $r2")
  }

  System.gc()
  System.runFinalization()

  {
    val r1 = timer(f1(10000))
    val r2 = timer(f2(10000))
    println(s"$r1 $r2")
  }

}

输出:

57ms // line 1
19ms // line 2
2500166641665000 2500166641665000
7ms  // line 4
0ms  // line 5
2500166641665000 2500166641665000

显然 f2 应该花很少的时间来执行,但是第 1 行输出 57ms。我想这可能是因为 JVM 初始化。 OTOH,尽管我尝试了垃圾收集,但第 2 行和第 4 行也有所不同(我们不能保证,因为 JVM 有一些不确定性,但这就是我能想到的)。

这个例子非常简单,我应该多次 运行 结果以实际分析它们(就像 Python 中的 timeit 模块)。但是,我不确定如何将正确的计时器写入 eliminate/mitigate 输出中显示的潜在影响。

更新:

JVM 初始化应该包括在内,因为如果我在开始之前添加类似 timer({}) 的内容,第 1 行 时间成本很快就会变成 0ms(表示它花费的时间很少)。

Obviously f2 should take little time to execute,

这可能需要一些时间,但不一定需要一毫秒。您的计算可能只有几分之一毫秒。事实上,由于没有使用结果,代码可以被丢弃。

我建议你使用 System.nanoTime() 并确保使用结果。

guess perhaps it's because of the JVM initialization.

您第一次调用代码时,必须加载它,很可能这就是您所安排的时间

line 2 and line 4 are also different although I tried garbage collection

代码现已加载。注意:如果您复制了代码和 运行 另一个做同样事情的方法,您可能会得到相似的时间。

I should run the results for many times to actually profile them

我会忽略 运行 的前 2 秒确保代码已经预热,或者使用像 JMH 这样的微基准框架 http://openjdk.java.net/projects/code-tools/jmh/

static int n = 10000;

public static void main(String[] args) throws RunnerException, IOException {
    long time = 2;
    Options opt = new OptionsBuilder()
            .include(CalcBenchmark.class.getSimpleName())
            .warmupIterations(6)
            .forks(1)
            .measurementTime(TimeValue.seconds(time))
            .timeUnit(TimeUnit.NANOSECONDS)
            .build();

    new Runner(opt).run();
}
@Benchmark
public long calc() {
    return (n - 1L) * n / 2 * (n + 1) / 3 * (3 * n + 2) / 2;
}

打印

# JMH 1.11.2 (released 164 days ago, please consider updating!)
# VM version: JDK 1.8.0_45, VM 25.45-b02
# VM invoker: /mnt/opt/jdk1.8.0_45/jre/bin/java
# VM options: -Didea.launcher.port=7534 -Didea.launcher.bin.path=/mnt/opt/idea-IC-143.1821.5/bin -Dfile.encoding=UTF-8
# Warmup: 6 iterations, 1 s each
# Measurement: 20 iterations, 2 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: vanilla.java.jmh.CalcBenchmark.calc

# Run progress: 0.00% complete, ETA 00:00:46
# Fork: 1 of 1
# Warmup Iteration   1: 0.105 ops/ns
# Warmup Iteration   2: 0.156 ops/ns
# Warmup Iteration   3: 0.169 ops/ns
# Warmup Iteration   4: 0.167 ops/ns
# Warmup Iteration   5: 0.166 ops/ns
# Warmup Iteration   6: 0.165 ops/ns
Iteration   1: 0.169 ops/ns
Iteration   2: 0.166 ops/ns
Iteration   3: 0.165 ops/ns
Iteration   4: 0.168 ops/ns
Iteration   5: 0.163 ops/ns
Iteration   6: 0.159 ops/ns
Iteration   7: 0.162 ops/ns
Iteration   8: 0.166 ops/ns
Iteration   9: 0.169 ops/ns
Iteration  10: 0.166 ops/ns
Iteration  11: 0.169 ops/ns
Iteration  12: 0.162 ops/ns
Iteration  13: 0.166 ops/ns
Iteration  14: 0.167 ops/ns
Iteration  15: 0.166 ops/ns
Iteration  16: 0.169 ops/ns
Iteration  17: 0.166 ops/ns
Iteration  18: 0.165 ops/ns
Iteration  19: 0.170 ops/ns
Iteration  20: 0.164 ops/ns


Result "calc":
  0.166 ±(99.9%) 0.002 ops/ns [Average]
  (min, avg, max) = (0.159, 0.166, 0.170), stdev = 0.003
  CI (99.9%): [0.163, 0.168] (assumes normal distribution)


# Run complete. Total time: 00:00:47

Benchmark            Mode  Cnt  Score   Error   Units
CalcBenchmark.calc  thrpt   20  0.166 ± 0.002  ops/ns

简而言之,您的操作一旦预热,大约需要 6 ns 或 0.000006 ms


没有 JMH 的更简单的基准测试可能如下所示。注:我更相信JMH的数字。

public class SimpleCalcBenchmark {
    static int n = 10000;
    static final AtomicLong blackHole = new AtomicLong();

    public static void main(String[] args) throws RunnerException, IOException {
        for (int i = 0; i < 5; i++) {
            long start = System.nanoTime();
            long counter = 0;
            while (System.nanoTime() - start < 2e9) {
                for (int j = 0; j < 100; j++) {
                    blackHole.lazySet(calc());
                }
                counter += 100;
            }
            long time = System.nanoTime() - start;
            System.out.printf("Took an average of %.1f ns%n", (double) time/counter);
        }
    }

    public static long calc() {
        return (n - 1L) * n / 2 * (n + 1) / 3 * (3 * n + 2) / 2;
    }
}

打印

Took an average of 10.2 ns
Took an average of 6.7 ns
Took an average of 4.7 ns
Took an average of 4.7 ns
Took an average of 4.6 ns