Java vs Rust performance



我在Java和Rust上运行了一个相同的小型基准测试。

Java:

public class Main {
private static final int NUM_ITERS = 100;
public static void main(String[] args) {
long tInit = System.nanoTime();
int c = 0;
for (int i = 0; i < NUM_ITERS; ++i) {
for (int j = 0; j < NUM_ITERS; ++j) {
for (int k = 0; k < NUM_ITERS; ++k) {
if (i*i + j*j == k*k) {
++c;
System.out.println(i + " " + j + " " + k);
}
}
}
}
System.out.println(c);
System.out.println(System.nanoTime() - tInit);
}
}

锈蚀:

use std::time::SystemTime;
const NUM_ITERS: i32 = 100;
fn main() {
let t_init = SystemTime::now();
let mut c = 0;
for i in 0..NUM_ITERS {
for j in 0..NUM_ITERS {
for k in 0..NUM_ITERS {
if i*i + j*j == k*k {
c += 1;
println!("{} {} {}", i, j, k);
}
}
}
}
println!("{}", c);
println!("{}", t_init.elapsed().unwrap().as_nanos());
}

如预期的那样,当NUM_ITERS = 100时,Rust-out执行Java

Java: 59311348 ns
Rust: 29629242 ns

但对于NUM_ITERS = 1000,我发现Rust花费的时间要长得多,Java的速度要快得多

Java: 1585835361  ns
Rust: 28623818145 ns

这可能是什么原因?在这种情况下,Rust的性能不应该也比Java好吗?还是因为我在执行过程中犯了一些错误?

更新

我从代码中删除了行System.out.println(i + " " + j + " " + k);println!("{} {} {}", i, j, k);。以下是的输出

NUM_ITERS = 100
Java: 3843114  ns
Rust: 29072345 ns

NUM_ITERS = 1000
Java: 1014829974  ns
Rust: 28402166953 ns

因此,如果没有println语句,Java在这两种情况下的性能都比Rust好。我只是想知道为什么会这样。Java运行垃圾回收器和其他开销。我没有以最佳方式实现Rust中的循环吗?

我调整了您的代码,以消除评论中提出的批评。没有为生产编译Rust是最大的问题,这会带来50倍的开销。除此之外,我取消了测量时的打印,并对Java代码进行了适当的预热。

我想说的是,在这些变化之后,Java和Rust不相上下,它们之间的差距在2倍以内,每次迭代的成本都非常低(只有几分之一纳秒(。

这是我的代码:

public class Testing {
private static final int NUM_ITERS = 1_000;
private static final int MEASURE_TIMES = 7;
public static void main(String[] args) {
for (int i = 0; i < MEASURE_TIMES; i++) {
System.out.format("%.2f ns per iteration%n", benchmark());
}
}
private static double benchmark() {
long tInit = System.nanoTime();
int c = 0;
for (int i = 0; i < NUM_ITERS; ++i) {
for (int j = 0; j < NUM_ITERS; ++j) {
for (int k = 0; k < NUM_ITERS; ++k) {
if (i*i + j*j == k*k) {
++c;
}
}
}
}
if (c % 137 == 0) {
// Use c so its computation can't be elided
System.out.println("Count is divisible by 13: " + c);
}
long tookNanos = System.nanoTime() - tInit;
return tookNanos / ((double) NUM_ITERS * NUM_ITERS * NUM_ITERS);
}
}
use std::time::SystemTime;
const NUM_ITERS: i32 = 1000;
fn main() {
let mut c = 0;
let t_init = SystemTime::now();
for i in 0..NUM_ITERS {
for j in 0..NUM_ITERS {
for k in 0..NUM_ITERS {
if i*i + j*j == k*k {
c += 1;
}
}
}
}
let took_ns = t_init.elapsed().unwrap().as_nanos() as f64;
let iters = NUM_ITERS as f64;
println!("{} ns per iteration", took_ns / (iters * iters * iters));
// Use c to ensure its computation can't be elided by the optimizer
if c % 137 == 0 {
println!("Count is divisible by 137: {}", c);
}
}

我使用JDK16从IntelliJ运行Java。我使用cargo run --release从命令行运行Rust。

Java输出示例:

0.98 ns per iteration
0.93 ns per iteration
0.32 ns per iteration
0.34 ns per iteration
0.32 ns per iteration
0.33 ns per iteration
0.32 ns per iteration

Rust输出示例:

0.600314 ns per iteration

虽然看到Java给出更好的结果我并不一定感到惊讶(它的JIT编译器已经优化了20年,没有对象分配,所以没有GC(,但我对迭代的总体低成本感到困惑。我们可以假设表达式i*i + j*j被提升出内循环,这只留下k*k在里面

我使用了一个反汇编程序来检查Rust生成的代码。它肯定将IMUL包含在最内部的循环中。我读到这个答案,它说英特尔的IMUL指令只有3个CPU周期的延迟。将其与多个ALU和指令并行性相结合,每次迭代1个周期的结果变得更加合理。

我发现的另一个有趣的事情是,如果我只检查c % 137 == 0,但不在Rustprintln!语句中打印c的实际值(只打印"Count可被137整除"(,迭代成本将降至0.26ns。因此,当我没有要求c的确切值时,Rust能够从循环中消除很多工作。


更新

正如在与@trentci的评论中所讨论的,我更完整地模仿了Java代码,添加了一个重复测量的外循环,现在它在一个单独的函数中:

use std::time::SystemTime;
const NUM_ITERS: i32 = 1000;
const MEASURE_TIMES: i32 = 7;
fn main() {
let total_iters: f64 = NUM_ITERS as f64 * NUM_ITERS as f64 * NUM_ITERS as f64;
for _ in 0..MEASURE_TIMES {
let took_ns = benchmark() as f64;
println!("{} ns per iteration", took_ns / total_iters);
}
}
fn benchmark() -> u128 {
let mut c = 0;
let t_init = SystemTime::now();
for i in 0..NUM_ITERS {
for j in 0..NUM_ITERS {
for k in 0..NUM_ITERS {
if i*i + j*j == k*k {
c += 1;
}
}
}
}
// Use c to ensure its computation can't be elided by the optimizer
if c % 137 == 0 {
println!("Count is divisible by 137: {}", c);
}
return t_init.elapsed().unwrap().as_nanos();
}

现在我得到这个输出:

0.781475 ns per iteration
0.760657 ns per iteration
0.783821 ns per iteration
0.777313 ns per iteration
0.766473 ns per iteration
0.774042 ns per iteration
0.766718 ns per iteration

代码的另一个细微变化导致了性能的显著变化。然而,它也显示了Rust相对于Java的一个关键优势:不需要预热即可获得最佳性能。

Java虚拟机可能会在内部循环超过一定的迭代次数后及时将其编译为本机汇编代码。我不是JVM专家,但这可能解释了为什么大量的迭代会神奇地使java代码运行得更快。

这背后的技术被称为HotSpot;客户端";以及";服务器";虚拟机。它还取决于JVM的供应商是否可用以及它的行为方式。

https://en.wikipedia.org/wiki/HotSpot_(虚拟机(

最新更新