【问题标题】:Java vs Rust performanceJava 与 Rust 性能
【发布时间】:2021-04-22 10:02:23
【问题描述】:

我在 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 的表现如预期的那样优于 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?
  • 不存在每次迭代产生输出的基准。您测量的是 I/O,而不是 CPU 时间。
  • 虽然在测量过程中打印任何东西绝对不好,但我认为上面的评论者忽略了这样一个事实,即在 100 万个测试组合中,只有 299 次满足条件 a2 + b2 = c2(使用 NUM_ITERS = 100)。
  • @DivyanshuPundir,在 IntelliJ 中,使用cargo run --release 运行以运行优化代码。
  • 我刚刚使用了一个改进的版本(如果重新打开,我很乐意分享一个答案),优化 Rust 构建的结果是每次迭代 0.6 ns,对于 Java 是 0.32 ns每次迭代。我不觉得这是一个令人惊讶的结果,这里没有分配所以 GC 无关紧要,Java 的 JIT 编译器非常擅长优化这样的简单代码。

标签: java performance rust


【解决方案1】:

我调整了您的代码以消除 cmets 中列出的批评点。不为生产编译 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。我使用 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。我读了this 的答案,它说英特尔的 IMUL 指令延迟只有 3 个 CPU 周期。将其与多个 ALU 和指令并行性相结合,每次迭代 1 个周期的结果变得更加合理。

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


更新

正如在 @trentci 的 cmets 中所讨论的,我更完整地模仿了 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 的一个关键优势:无需预热即可获得最佳性能。

【讨论】:

  • 在我的机器上运行这两个程序会产生 0.38 ns 的 Java 和 0.10 ns 的 Rust 作为每次迭代的最佳时间。考虑添加 rustc 标志 -C target-cpu=native 以让编译器使用主机的完整指令集。另外,您使用的是哪个 JVM? JIT 编译器可能不错,但我们正在针对 LLVM 的提前编译对其进行测试,预计总体上它会更具侵略性。
  • @E_net4thecopycat 我发现了一个令人惊讶的结果:我实际上在最后一刻更改了 Rust 中的 print 语句,而没有重新运行测试。从 println!("Count is divisible by 137") 更改为 println("Count is divisible by 137: {}", count) 导致 0.26 ns 与发布的 0.60 ns 的差异!
  • @MarkoTopolnik 是的,结果可能有点令人惊讶。对于 Rust 中的微基准测试,人们通常会使用统计驱动的工具,例如 Criterion.rs,它提供了 black_box() 的实现,这是一种旨在避免优化值的构造(否则,std 库中的 black_box 需要目前的夜间工具链)。
  • @E_net4thecopycat 我玩过目标CPU标志,结果比较意外。打印 c 时,性能会变差(0.67 ns),但不打印时,它甚至比您发布的更好:每次迭代仅 0.076 ns!编译器一定有一些捷径。
  • @DivyanshuPundir 是的,这就是我在代码中的评论所解释的:Use c to ensure its computation can't be elided by the optimizer
猜你喜欢
  • 2021-11-18
  • 1970-01-01
  • 1970-01-01
  • 2012-08-06
  • 1970-01-01
  • 2019-09-04
  • 2016-06-23
  • 2011-10-02
  • 2013-12-01
相关资源
最近更新 更多