如何在Java中基准测试主机多线程CPU性能?

3
我需要创建一个简单的Java应用程序,只返回一个数字:CPU性能估计。例如,当我在拥有4个核心的机器上运行它时,得到的数字大约是使用2个核心的情况下的两倍。该应用程序应该使用100% CPU运行几秒钟来测量。实际精度不太重要。
令我惊讶的是,我找不到任何已经执行此操作的Java库。当然,其他语言中也有类似的工具,但是在我的环境中,只有Java被批准。
我的当前想法是在我的代码中使用SciMark 2.0中的类并从多个线程运行它,但是这个工具看起来非常混乱(例如,类名以小写字母开头),我需要编写自定义代码来运行这些线程并组合结果。
是否有更好的解决方法?

CPU性能在做什么?实际上您尝试测量的内容可能很重要。通常的方法是测量完成任务所需的总时间。 - markspace
如果您使用的是Linux系统,只需从/proc/cpuinfo中读取bogomips值即可。 - rkosegi
@markspace 我不在意。就像我说的,准确性对我来说一点也不重要,只需要大致的数字即可。理想情况下,我正在寻找一个已经准备好的解决方案,无论做出什么样的假设。因为这些是 Jenkins 代理,所以将有各种任务需要执行。 - Michal Kordas
@rkosegi 我不能使用 /proc/cpuinfo,因为这个基准测试必须按需运行(虚拟机性能可能在不重启的情况下发生变化)。 - Michal Kordas
然后我会对手头的任务进行基准测试,并记录其性能。如果该性能随时间变化,您可以在那时调查变化。这样做更好,因为它测量的是实际任务的时间,而不是某个任意基准。 - markspace
显示剩余4条评论
4个回答

3

这是一段我想要的最简单的代码。它尝试通过计算连续整数的平方根之和来估算多线程的CPU性能。变量iterations可以调整以增加/减少基准测试的长度。在我的机器上,使用默认值大约需要7秒钟。

import static java.util.stream.IntStream.rangeClosed;

class Benchmark {
    public static void main(String[] args) {
        final int iterations = 100_000_000;
        long start = System.currentTimeMillis();
        rangeClosed(1, 50).parallel()
                .forEach(i -> rangeClosed(1, iterations).mapToDouble(Math::sqrt).sum());
        System.out.println(System.currentTimeMillis() - start);
    }
}

很好,提供了一个有用的回答,没有冗长的修辞。 - tsquared
1
所以你正在测量FP开方吞吐量。这是非常特定的,与大多数FP工作负载不高度相关。例如,您将看到在相同时钟速度下从Haswell与Broadwell和Skylake(如2倍)之间获得非常大的加速,而mul/add/FMA吞吐量并没有改变。BDW引入了更高的基数除法/平方根单元。https://agner.org/optimize/ instruction tables;查看sqrtsd吞吐量(从8到14,而4到8),假设它不使用SIMD进行矢量化。如果它可以向量化,Skylake将为128位和256位宽向量提供另一个巨大的加速。 - Peter Cordes
@PeterCordes 我不是用它来衡量哪个虚拟机更好或更快。我只需要它来进行健全性检查,以确保我的机器的性能大致与一段时间前相同,并且仍然从裸金属获得类似数量的CPU周期。 - Michal Kordas
好的,是的,它应该在相同的硬件上每次以相同的方式运行。(如果JVM找到自动矢量化或甚至优化掉您没有任何分配的总和,那么可能会更快。)随着线程数的增加,使用超线程可能不会有帮助;单个硬件线程运行此代码(如果在任何地方JIT得足够高效)可能可以饱和sqrt单元。 - Peter Cordes
@PeterCordes 没错,在这种情况下,使用硬编码的Java版本的Docker容器似乎是确保结果稳定的解决方案。 - Michal Kordas
@MichalKordas:这取决于你想测试什么!如果你想看看你的简单基准测试技术是否仍然有效,或者你的JVM是否有所改进,那么就使用它。或者添加对最终总和的打印,这样它就不能轻易地被优化掉,但是仍然可以通过SIMD加速,所以你可能会在某个时候得到一个愉快的惊喜。 - Peter Cordes

2
如果我理解正确,您的目标是衡量系统性能而不是应用程序性能。
问题在于,系统性能不能简化为单个有意义的数字。实际上,系统性能...甚至CPU性能都是多维的。
例如,一个内存密集型的应用程序将在不同的机器上表现不同,这取决于CPU芯片的内存缓存大小和设计...以及内存速度。但是,如果应用程序是计算密集型的,则性能将更多地取决于时钟速率和核心数。
然后还有像NUMA单元和线程固定等问题,当核心数很高和/或您有多个CPU芯片时会出现这些问题。
这些和类似的问题是为什么试图独立于应用程序测量原始CPU性能的基准测试已经大多不受欢迎的原因。(MIPS最初意味着每秒百万(硬件)指令。现在它通常被称为神话般的每秒指令...暗示了这种测量作为真实应用程序性能预测器的虚假性)

完全同意。正如我所强调的,我需要一个非常粗略的数字。我不关心细节。我只需要检测出由于某些原因(例如物理服务器被过度分配)导致这个特定虚拟机的性能下降。而且我只关心数量级的变化,例如,这个虚拟机昨天能够在1分钟内计算1M位数的圆周率,但今天却需要10分钟,所以肯定有问题。 - Michal Kordas
1
好的...如果你正在寻找一个随意无意义的指标,那么在N个并行线程中计算Pi的前D位,并使用以下其中之一来测量时钟时间或CPU时间:https://dev59.com/DGs05IYBdhLWcg3wFN4h#7467299 - Stephen C
但如果您的目标是测量虚拟机的性能,而您怀疑由于超额提交而导致性能下降,则衡量单个或多个线程的CPU性能的基准测试是不足够的。为什么?因为您还需要考虑RAM超额提交和I/O或设备饱和度。对于典型的Java应用程序,这些因素对性能的影响可能比简单的CPU<->VCPU超额提交更严重。 - Stephen C

0

Java Microbenchmark Harness (JMH)是一个实现Java代码基准测试的工具包。

它可以测量吞吐量或平均时间; 您可以使用它来估计CPU周期。

基本上,您需要使用@Benchmark注释要进行基准测试的方法。这个方法

他们的存储库中有一些JMH使用示例

在运行基准测试时,始终建议让计算机独自运行,并关闭所有其他应用程序(如果可能)。 如果您的计算机正在运行其他应用程序,则这些应用程序可能会从CPU中获取时间并提供不正确(较低)的性能数字。

如果您想深入了解CPU性能(周期、缓存使用、指令等),您可能需要使用Linux perf

我不需要测量代码的性能。我正在寻找一个Java库(或者如何编写这样的库的想法),它将触发一些CPU密集型任务,以配置的时间为单位使用所有可用线程,并且作为结果,我将得到一个大致表示此VM当前CPU性能的数字。 - Michal Kordas
JMH的Blackhole类有一个consumeCPU方法,它只是消耗CPU,避免JIT优化。 - Gonzalo Matheu
好的,但这仍然是单线程的。我更想要consumeAllCpus(long tokens)方法,否则就完美了。 - Michal Kordas

0

Michal,谢谢你的回答,我借鉴了一些线程技术来帮助我诊断客户AIX机器上的虚拟CPU性能问题。

import static java.util.stream.IntStream.rangeClosed;

public class Main {

    public static void main(String[] args) {
        if (args.length < 2) {
            System.out.println("Usage: benchmark [million iterations] [maxThreads]");
            return;
        }

        final int MILLION = 1_000_000;
        final int iterations = Integer.parseInt(args[0]);
        final int maxThreads = Integer.parseInt(args[1]);

        for (int threads = 1; threads < maxThreads; threads++) {
            long start = System.currentTimeMillis();
            int count = iterations * MILLION / threads;
            rangeClosed(1, threads).parallel()
                .forEach(i -> rangeClosed(1, count).mapToDouble(Math::sqrt).sum());

            System.out.println(String.format("Benchmark of %d M iterations on %d thread(s): %d ms", iterations, threads, System.currentTimeMillis() - start));
        }

    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接