簡體   English   中英

Java 與 Rust 性能對比

[英]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);
    }
}

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 的表現優於 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);
    }
}

我使用 JDK 16 從 IntelliJ 運行 Java。我使用cargo run --release

Java output 示例:

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 output 示例:

0.600314 ns per iteration

雖然看到 Java 給出了更好的結果,我並不一定感到驚訝(它的 JIT 編譯器已經優化了 20 年,沒有 object 分配,所以沒有 GC),我對迭代的整體低成本感到困惑。 我們可以假設表達式i*i + j*j被提升到內部循環之外,它只留下k*k在里面。

我使用反匯編程序檢查了生成的代碼 Rust。 它肯定在最內層循環中涉及 IMUL。 我讀了這個答案,它說英特爾的 IMUL 指令延遲只有 3 個 CPU 周期。 將其與多個 ALU 和指令並行性相結合,每次迭代 1 個周期的結果變得更加合理。

我發現的另一個有趣的事情是,如果我只檢查c % 137 == 0但不打印println!的實際值c打印聲明,(僅打印“計數可被 137 整除”),迭代成本降至僅 0.26 ns。 因此,當我沒有詢問 c 的確切值時, c能夠消除循環中的大量工作。


更新

正如@trentci 的評論中所討論的,我更完整地模仿了 Java 代碼,添加了一個重復測量的外部循環,現在它位於一個單獨的 function 中:

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

現在我得到這個 output:

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 的關鍵優勢:無需預熱即可獲得最佳性能。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM