Java 与 Rust 性能比较

Div*_*dir 12 java performance 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

锈:

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());
}

Run Code Online (Sandbox Code Playgroud)

正如预期的那样NUM_ITERS = 100,Rust 的表现优于 Java

Java: 59311348 ns
Rust: 29629242 ns
Run Code Online (Sandbox Code Playgroud)

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

Java: 1585835361  ns
Rust: 28623818145 ns
Run Code Online (Sandbox Code Playgroud)

这可能是什么原因?在这种情况下,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
Run Code Online (Sandbox Code Playgroud)

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

Mar*_*nik 17

我调整了你的代码以消除评论中提出的批评点。不编译 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);
    }
}
Run Code Online (Sandbox Code Playgroud)
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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我从 IntelliJ 运行 Java,使用 JDK 16。我从命令行运行 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
Run Code Online (Sandbox Code Playgroud)

Rust 输出示例:

0.600314 ns per iteration
Run Code Online (Sandbox Code Playgroud)

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

我使用反汇编程序来检查 Rust 生成的代码。最里面的循环肯定涉及IMUL。我读过这个答案,它说英特尔的 IMUL 指令的延迟仅为 3 个 CPU 周期。将其与多个 ALU 和指令并行性相结合,每次迭代 1 个周期的结果变得更加合理。

我发现的另一件有趣的事情是,如果我只是检查c % 137 == 0但不打印cRustprintln!语句中的实际值(仅打印“Count isible by 137”),迭代成本会降至仅 0.26 ns。因此,当我不要求 的确切值时,Rust 能够消除循环中的大量工作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();
}
Run Code Online (Sandbox Code Playgroud)

现在我得到这个输出:

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
Run Code Online (Sandbox Code Playgroud)

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

  • 在我的机器上运行这两个程序,每次迭代的最佳时间(Java 为 0.38 ns,Rust 为 0.10 ns)。考虑添加 rustc 标志 `-C target-cpu=native` 让编译器使用主机的完整指令集。另外,您使用的是哪个 JVM?JIT 编译器可能很好,但我们正在针对 LLVM 的提前编译对其进行测试,预计总体上会更加激进。 (2认同)