JMH:对环境的奇怪依赖

JMH: strange dependency on the environment

在尝试使用 JMH 对我的 class 进行基准测试时,我遇到了一个让我感到困惑的行为,我想在继续之前澄清这个问题。

让我困惑的情况:
当我 运行 基准测试而 CPU 被无关进程加载(78%-80%)时,JMH 显示的结果看起来相当合理和稳定:

Benchmark                                  Mode  Cnt    Score   Error  Units
ArrayOperations.a_bigDecimalAddition       avgt    5  264,703 ± 2,800  ns/op
ArrayOperations.b_quadrupleAddition        avgt    5   44,290 ± 0,769  ns/op
ArrayOperations.c_bigDecimalSubtraction    avgt    5  286,266 ± 2,454  ns/op
ArrayOperations.d_quadrupleSubtraction     avgt    5   46,966 ± 0,629  ns/op
ArrayOperations.e_bigDecimalMultiplcation  avgt    5  546,535 ± 4,988  ns/op
ArrayOperations.f_quadrupleMultiplcation   avgt    5   85,056 ± 1,820  ns/op
ArrayOperations.g_bigDecimalDivision       avgt    5  612,814 ± 5,943  ns/op
ArrayOperations.h_quadrupleDivision        avgt    5  631,127 ± 4,172  ns/op

误差比较大是因为我现在只需要一个粗略的估计,我故意用精度换取速度。

但是在处理器上没有额外负载的情况下获得的结果对我来说似乎很惊人:

Benchmark                                  Mode  Cnt    Score     Error  Units
ArrayOperations.a_bigDecimalAddition       avgt    5  684,035 ± 370,722  ns/op
ArrayOperations.b_quadrupleAddition        avgt    5   83,743 ±  25,762  ns/op
ArrayOperations.c_bigDecimalSubtraction    avgt    5  531,430 ± 184,980  ns/op
ArrayOperations.d_quadrupleSubtraction     avgt    5   85,937 ± 103,351  ns/op
ArrayOperations.e_bigDecimalMultiplcation  avgt    5  641,953 ± 288,545  ns/op
ArrayOperations.f_quadrupleMultiplcation   avgt    5  102,692 ±  31,625  ns/op
ArrayOperations.g_bigDecimalDivision       avgt    5  733,727 ± 161,827  ns/op
ArrayOperations.h_quadrupleDivision        avgt    5  820,388 ± 546,990  ns/op

一切似乎都慢了近两倍,迭代时间非常不稳定(在相邻迭代中可能从 500 到 1300 ns/op 不等)并且误差分别大得令人无法接受。

第一组结果是用一堆应用程序 运行ning 获得的,包括 Folding@home 分布式计算客户端 (FahCore_a7.exe),它占用了 CPU 时间的 75%,积极使用磁盘的 BitTorrent 客户端、浏览器中的十几个选项卡、电子邮件客户端等。平均 CPU 负载约为 85%。在基准测试执行期间,FahCore 减少负载,因此 Java 占 25%,总负载为 100%。

第二组结果是在停止所有不必要的进程时获取的,CPU 实际上是空闲的,只有 Java 占 25%,还有几个百分比用于系统需求。

我的 CPU 是 Intel i5-4460,4 内核,3.2 GHz,RAM 32 GB,OS Windows Server 2008 R2。
java 版本“1.8.0_231”
Java(TM) SE 运行时环境(构建 1.8。0_231-b11)
Java HotSpot(TM) 64 位服务器 VM(内部版本 25.231-b11,混合模式)

问题是:

  1. 当加载机器的唯一任务时,为什么基准测试显示更糟糕和不稳定的结果?
  2. 当第一组结果如此依赖于环境时,我可以认为它们或多或少可靠吗?
  3. 我是否应该以某种方式设置环境以消除这种依赖性?
  4. 或者这是我的代码造成的?

代码:

package com.mvohm.quadruple.benchmarks;

// Required imports here

import com.mvohm.quadruple.Quadruple; // The class under tests

@State(value = Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(java.util.concurrent.TimeUnit.NANOSECONDS)
@Fork(value = 1)
@Warmup(iterations = 3, time = 7)
@Measurement(iterations = 5, time = 10)
public class ArrayOperations {

  // To do BigDecimal arithmetic with the precision close to this of Quadruple
  private static final MathContext MC_38 = new MathContext(38, RoundingMode.HALF_EVEN);

  private static final int DATA_SIZE = 0x1_0000;        // 65536
  private static final int INDEX_MASK = DATA_SIZE - 1;  // 0xFFFF

  private static final double RAND_SCALE = 1e39; // To provide a sensible range of operands,
                                                 // so that the actual calculations don't get bypassed

  private final BigDecimal[]      // Data to apply operations to
      bdOp1     = new BigDecimal[DATA_SIZE],  // BigDecimals 
      bdOp2     = new BigDecimal[DATA_SIZE],
      bdResult  = new BigDecimal[DATA_SIZE];
  private final Quadruple[]
      qOp1      = new Quadruple[DATA_SIZE],   // Quadruples
      qOp2      = new Quadruple[DATA_SIZE],
      qResult   = new Quadruple[DATA_SIZE];

  private int index = 0;

  @Setup
  public void initData() {
    final Random rand = new Random(12345); // for reproducibility
    for (int i = 0; i < DATA_SIZE; i++) {
      bdOp1[i] = randomBigDecimal(rand);
      bdOp2[i] = randomBigDecimal(rand);
      qOp1[i] = randomQuadruple(rand);
      qOp2[i] = randomQuadruple(rand);
    }
  }

  private static Quadruple randomQuadruple(Random rand) {
    return Quadruple.nextNormalRandom(rand).multiply(RAND_SCALE); // ranged 0 .. 9.99e38
  }

  private static BigDecimal randomBigDecimal(Random rand) {
    return Quadruple.nextNormalRandom(rand).multiply(RAND_SCALE).bigDecimalValue();
  }

  @Benchmark
  public void a_bigDecimalAddition() {
    bdResult[index] = bdOp1[index].add(bdOp2[index], MC_38);
    index = ++index & INDEX_MASK;
  }

  @Benchmark
  public void b_quadrupleAddition() {
    // semantically the same as above 
    qResult[index] = Quadruple.add(qOp1[index], qOp2[index]); 
    index = ++index & INDEX_MASK;
  }

  // Other methods are similar 

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

}

道理很简单,我应该马上就明白了。在 OS 中启用了省电模式,这降低了 CPU 在低负载下的时钟频率。原则是,在进行基准测试时始终禁用省电功能!