【问题标题】:Why is logarithm slower in Rust than in Java?为什么 Rust 中的对数比 Java 中的慢?
【发布时间】:2017-12-15 16:38:18
【问题描述】:

如果我在 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)

121-6 = 115 ns 每次 ln 调用。

但 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

Rust 中的日志比 Java 慢约 3.7 倍 (115/31)。

当我测试斜边实现 (hypot) 时,Rust 中的实现比 Java 快 15.8 倍。

我是否编写了糟糕的基准测试或者这是一个性能问题?

对 cme​​ts 中提出的问题的回答:

  1. "," 是我国的小数分隔符。

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

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

【问题讨论】:

  • Java 不适合用作基准测试吗?我的意思是 java 很好,但在某些情况下,它太好了
  • 您可能比 log 函数更多地对随机数生成器进行基准测试。另外,我相信 Rust 只是使用系统数学库,所以对log 的纯调用应该与它在 C 中的调用相同(不知道 Java)。
  • 你能用RUSTFLAGS='-Ctarget-cpu=native' cargo bench重新运行测试吗?
  • @SimonByrne 如果 OP 不小心对 RNG 进行了基准测试,那会不会被 测试 RNG 的 bench_rnd 函数所抵消?这就是为什么 OP 减去了两个 Rust 基准测试时间——为ln 函数做一个纯粹的基准测试。我同意它应该只是调用系统数学库。
  • 我对java一无所知,但你确定x是更新的吗?

标签: java floating-point rust microbenchmark jmh


【解决方案1】:

答案是given by @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)。

【讨论】:

    【解决方案2】:

    我将提供另一半的解释,因为我不了解 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 何时以及如何发生这种情况......

    【讨论】:

    • 由于 Rust 是静态(或 AOT,如果你愿意)编译的,它必须知道要编译到的单一平台。默认情况下,它会有点保守(例如,32 位 x86 代码可能针对 686 处理器)。 -Ctarget-cpu=native 标志告诉编译器以运行编译器的机器为目标;这允许编译器使用完整的可用指令集(如您的 popcnt 示例)。
    猜你喜欢
    • 2014-10-05
    • 1970-01-01
    • 2012-02-19
    • 1970-01-01
    • 2013-11-19
    • 1970-01-01
    • 2014-12-21
    相关资源
    最近更新 更多