java与rust性能

a64a0gku  于 2021-07-09  发布在  Java
关注(0)|答案(1)|浏览(344)

我在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);
    }
}

生 rust :

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中实现最佳的循环吗?

l7wslrjt

l7wslrjt1#

我调整了你的代码以消除评论中提出的批评。不为生产编译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。我从命令行运行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 在里面。
我用反汇编器检查产生的代码。它肯定涉及最里面的循环imul。我读了这个答案,它说英特尔有一个imul指令只有3个cpu周期的延迟。再加上多个alu和指令并行性,每次迭代1个周期的结果就更可信了。
我发现的另一件有趣的事是,如果我检查一下 c % 137 == 0 但不要打印 c 在铁 rust 里 println! 语句,(仅打印“count可被137整除”),迭代成本降到仅0.26ns。所以,当我没有要求精确的值时,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();
}

现在我得到这个输出:

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

相关问题