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,混合模式)
问题是:
- 当加载机器的唯一任务时,为什么基准测试显示更糟糕和不稳定的结果?
- 当第一组结果如此依赖于环境时,我可以认为它们或多或少可靠吗?
- 我是否应该以某种方式设置环境以消除这种依赖性?
- 或者这是我的代码造成的?
代码:
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 在低负载下的时钟频率。原则是,在进行基准测试时始终禁用省电功能!
在尝试使用 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,混合模式)
问题是:
- 当加载机器的唯一任务时,为什么基准测试显示更糟糕和不稳定的结果?
- 当第一组结果如此依赖于环境时,我可以认为它们或多或少可靠吗?
- 我是否应该以某种方式设置环境以消除这种依赖性?
- 或者这是我的代码造成的?
代码:
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 在低负载下的时钟频率。原则是,在进行基准测试时始终禁用省电功能!