首页 > 解决方案 > Java 与 Rust 性能

问题描述

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

爪哇:

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 的表现优于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 中实现循环吗?

标签: javaperformancerust

解决方案


我调整了您的代码以消除评论中提出的批评点。不为生产编译 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);
    }
}

我使用 JDK 16 从 IntelliJ 运行 Java。我从命令行运行 Rust,使用cargo run --release.

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但不打印cRustprintln!语句中的实际值(只打印“计数可被 137 整除”),迭代成本下降到仅 0.26 ns。因此,当我不要求c.


更新

正如@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 的一个关键优势:无需预热即可获得最佳性能。


推荐阅读