Почему логарифм медленнее в Rust, чем в Java?

Если я запустил эти тесты в 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 нс на вызов 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 log более ~ 3.7 раз медленнее (115/31), чем в Java.

Когда я тестирую реализацию гипотенузы (hypot), реализация в Rust в 15,8 раза быстрее, чем в Java.

Я написал плохие тесты или это проблема производительности?

Ответы на вопросы, заданные в комментариях:

  • "," является десятичным разделителем в моей стране.

  • Я запускаю тест Rust с помощью cargo bench, который всегда запускается в режиме выпуска.

  • Базовая среда Java (JMH) создает новый объект для каждого вызова, хотя это класс static и переменная final. Если я добавлю случайное создание в тестируемый метод, я получаю 43 нс/оп.

Ответы

Ответ 1

Ответ был указанный @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

Я собираюсь предоставить вторую половину объяснения, так как я не знаю Ржавчина. Math.log аннотируется с помощью @HotSpotIntrinsicCandidate, что означает, что он будет заменен нативной инструкцией процессора для такой операции: подумайте 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, хотя...