我正在尝试对一些 Java 代码进行基准测试,我注意到它的性能差异很大。
作为示例,我在下面的代码片段中对函数 copyElements 的性能进行了基准测试:
public class Main {
static class Wrapper {
public int x;
public Wrapper(int x) {
this.x = x;
}
}
private static <T> void shuffle(T[] array) {
int index;
T temp;
Random random = new Random(420);
for (int i = array.length - 1; i > 0; i--) {
index = random.nextInt(i + 1);
temp = array[index];
array[index] = array[i];
array[i] = temp;
}
}
static public void copyElements(Wrapper[] arr1, Wrapper[] arr2) {
for (int i=0; i < arr1.length; i++) {
arr2[i].x = arr1[i].x;
}
}
public static void main(String[] args) {
Wrapper[] arr1 = new Wrapper[100_000_000];
Wrapper[] arr2 = new Wrapper[100_000_000];
for (int i=0; i < arr1.length; i++) {
arr1[i] = new Wrapper(i);
arr2[i] = new Wrapper(i);
}
shuffle(arr1);
shuffle(arr2);
// Run copyElements and measure performance.
}
}
我如何衡量 Java 性能:
我正在使用 OpenJDK 17。我已禁用 CPU 的 Turbo Boost。我使用 numactl -N 0 -m 0 来确保性能不会因为某些 NUMA 问题(64 核服务器)而发生变化。在此期间没有用户运行任何东西。数组被打乱以使内存访问模式随机。
另外,我在C++中重复了这个实验。代码使用带有标志 -std=c++20 -O3 的 g++ (GCC) 11.4.1 20230605 (Red Hat 11.4.1-2) 进行编译。也重复了64次。
#include <vector>
#include <algorithm>
#include <memory>
#include <chrono>
#include <iostream>
void copyElements(std::vector<int *> &arr1, std::vector<int *> &arr2) {
for (int i = 0; i < arr1.size(); i++) {
*arr2[i] = *arr1[i];
}
}
int main() {
std::vector<int *> arr1(1e8);
std::vector<int *> arr2(1e8);
for (int i = 0; i < arr1.size(); ++i) {
arr1[i] = new int(i);
arr2[i] = new int(i);
}
std::random_shuffle(arr1.begin(), arr1.end());
std::random_shuffle(arr2.begin(), arr2.end());
auto start = std::chrono::system_clock::now();
copyElements(arr1, arr2);
auto end = std::chrono::system_clock::now();
auto elapsed_millis = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
std::cout << elapsed_millis << std::endl;
for (int i = 0; i < arr1.size(); ++i) {
delete arr1[i];
delete arr2[i];
}
}
结果如下:
平均值:1518.1782848671876ms 标准:61.13827595338004ms std/平均值 = 4%
平均值:2883.3695373437504ms 标准:8.444136374009128ms std/平均值 = 0.3%
我们可以看到,C++ 的性能更加稳定。如何减少java性能的方差?
Java 性能的差异可能受到多种因素的影响,包括 Java 虚拟机 (JVM) 优化、垃圾回收和运行时预热效果。以下是一些有助于减少 Java 性能差异的建议:
热身阶段: 确保您的 Java 代码在实际基准测试之前经过充分的预热。 JVM 需要时间来执行优化。在预热阶段多次运行代码有助于 JVM 达到稳定状态。
垃圾收集: 监视和管理垃圾收集。频繁的垃圾收集会带来可变性。您可以尝试不同的垃圾收集策略或调整堆大小,看看是否会影响性能。
JIT编译: Java 使用即时 (JIT) 编译,这意味着代码在运行时编译为本机机器代码。此编译可能会引入可变性。考虑使用 JVM 选项来控制 JIT 编译行为,例如
-XX:+PrintCompilation
和 -XX:CompileThreshold
。
JVM 选项: 尝试使用 JVM 选项来针对您的特定用例进行优化。例如,您可以尝试调整堆大小(
-Xms
和 -Xmx
)、线程堆栈大小 (-Xss
) 以及其他相关选项。
基准测试工具: 考虑使用专门的 Java 基准测试工具,例如 JMH(Java Microbenchmarking Harness)。 JMH 旨在产生可靠的基准测试结果,并解决微基准测试中的许多常见陷阱。
剖析器: 使用分析工具来识别性能瓶颈。探查器可以帮助您了解 JVM 如何优化和执行代码。这可以指导您进行优化。
CPU 亲和力: 试验 CPU 关联性设置,以确保您的 Java 进程始终在特定 CPU 内核上运行。这有助于减轻 CPU 调度带来的可变性。
线程亲和力: 如果适用,请探索线程关联设置。确保线程在特定核心上一致运行可以减少可变性。
代码分析: 分析生成的字节码和机器代码以了解 JVM 如何优化您的代码。这种见解可能会指导您做出调整以获得更好的性能。
请记住,基准测试可能很敏感,结果可能会根据环境和特定 JVM 实现的不同而有所不同。必须仔细控制和监控这些因素,以获得可靠且可重复的性能测量结果。