为什么 Rust 中的对数运算比 Java 慢?

21

如果我在Rust中运行这些基准测试:

#[bench]
fn bench_rnd(b: &mut Bencher) {
    let mut rng = rand::weak_rng();
    b.iter(|| rng.gen_range::<f64>(2.0, 100.0));
}

#[bench]
fn bench_ln(b: &mut Bencher) {
    let mut rng = rand::weak_rng();
    b.iter(|| rng.gen_range::<f64>(2.0, 100.0).ln());
}

结果为:

test tests::bench_ln             ... bench:        121 ns/iter (+/- 2)
test tests::bench_rnd            ... bench:          6 ns/iter (+/- 0)

每次ln调用需要115纳秒。

但是在Java中,相同的基准测试:

@State(Scope.Benchmark)
public static class Rnd {
    final double x = ThreadLocalRandom.current().nextDouble(2, 100);
}

@Benchmark
public double testLog(Rnd rnd) {
    return Math.log(rnd.x);
}
给我:
Benchmark    Mode Cnt  Score  Error Units
Main.testLog avgt  20 31,555 ± 0,234 ns/op

相较于Java,Rust的日志速度慢了大约3.7倍(115/31)。

当我测试勾股定理的实现(hypot)时,Rust的实现比Java快15.8倍。

我写的基准测试有问题还是存在性能问题?

针对评论中提出的问题的回答:

  1. 在我的国家,“,”是小数分隔符。

  2. 我使用 cargo bench 运行Rust基准测试,它始终在发布模式下运行。

  3. Java基准测试框架(JMH)为每个调用创建一个新对象,即使它是静态类和final变量。如果我在测试的方法中添加一个随机创建,那么我得到43 ns/op。


1
Java不是作为基准基础使用的糟糕语言吗?我的意思是,Java很好,但在某些情况下,它太过完美了。 - Wietlol
3
你可能更多地在对比随机数生成器而非对数函数的性能。另外,我相信 Rust 只是使用系统的数学库,所以单纯调用 log 应该与 C 中的一致(Java 就不清楚了)。 - Simon Byrne
5
请ن½؟用RUSTFLAGS='-Ctarget-cpu=native' cargo benché‡چو–°è؟گè،Œوµ‹è¯•م€‚ - kennytm
2
如果 OP 不小心在对随机数生成器进行基准测试,那么 bench_rnd 函数是否会抵消这一点呢?因为该函数仅测试 RNG。这就是为什么 OP 要减去两个 Rust 基准测试时间的原因——以使 ln 函数的基准测试更加纯粹。我同意它应该只调用系统数学库。 - Shepmaster
2
我对Java一无所知,但你确定x已经更新了吗? - Stargateur
2个回答

14

这个答案是由@kennytm提供的:

export RUSTFLAGS='-Ctarget-cpu=native'

问题已得到解决。之后的结果为:

test tests::bench_ln              ... bench:          43 ns/iter (+/- 3)
test tests::bench_rnd             ... bench:           5 ns/iter (+/- 0)

我认为38(±3)已经足够接近于31.555(±0.234)。


10

由于我不了解 Rust,因此我将提供解释的另一半。 Math.log 带有 @HotSpotIntrinsicCandidate 注释,这意味着它将被本地 CPU 指令替换以执行这样的操作:可以考虑 Integer.bitCount ,它要么进行大量移位,要么使用直接的 CPU 指令来更快地执行。

像这样非常简单的程序:

public static void main(String[] args) {
    System.out.println(mathLn(20_000));
}

private static long mathLn(int x) {
    long result = 0L;
    for (int i = 0; i < x; ++i) {
        result = result + ln(i);
    }
    return result;
}

private static final long ln(int x) {
    return (long) Math.log(x);
}

并使用以下命令运行:

 java -XX:+UnlockDiagnosticVMOptions  
      -XX:+PrintInlining 
      -XX:+PrintIntrinsics 
      -XX:CICompilerCount=2 
      -XX:+PrintCompilation  
      package/Classname 

它将生成许多行,但其中之一是:

 @ 2   java.lang.Math::log (5 bytes)   intrinsic

让代码运行极快。

但是我不太清楚在Rust中发生的时间和方式...


12
由于Rust是静态(或者说预先)编译的,因此它必须知道一个要编译到的平台。默认情况下,它会比较保守(例如32位的x86代码可能会针对686处理器进行目标编译)。使用“-Ctarget-cpu=native”标志告诉编译器以编译器正在运行的机器为目标平台;这使得编译器可以使用完整的可用指令集(例如您的popcnt示例)。 - Shepmaster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接