为什么在Rust中更新大型静态浮点数组的程序比在C中慢?

我写了一个简单的程序来比较rust和c的性能。
铁锈版:

use std::time::Instant;

const STREAM_ARRAY_SIZE: usize = 10000000;
static mut A: [f64; STREAM_ARRAY_SIZE] = [1.0; STREAM_ARRAY_SIZE];

fn main() {
    let now = Instant::now();

    unsafe {
        for i in 0..STREAM_ARRAY_SIZE {
            A[i] = 2.0E0 * A[i];
        }
    }

    let duration = now.elapsed();
    println!("{}", (duration.as_secs() * 1_000_000_000 + duration.subsec_nanos() as u64) / 1000);
}

在调试和释放模式下运行:
$ ./target/debug/calc
472046 us.
$ ./target/release/calc
62860 us.

与调试相比,发行版的性能有了很大的提高。
C版本执行相同的操作,并在同一服务器上运行:
#include <sys/time.h>
#include <stdio.h>

#define STREAM_ARRAY_SIZE   10000000

static double A[STREAM_ARRAY_SIZE];
int mysecond(void)
{
        struct timeval tp;
        struct timezone tzp;
        int i;

        i = gettimeofday(&tp,&tzp);
        return (tp.tv_sec * 1000000 + tp.tv_usec);
}

int main(void)
{
    int j = 0;
    for (j = 0; j < STREAM_ARRAY_SIZE; j++)
    {
        A[j] = 1.0;
    }

    int t = mysecond();
    for (j = 0; j < STREAM_ARRAY_SIZE; j++)
    {
        A[j] = 2.0E0 * A[j];
    }
    printf("%d us.\n", mysecond() - t);
    return 0;
}

使用-O0-O2编译并运行它:
$ gcc test.c
$ ./a.out
41626 us.
$ gcc -O2 test.c
$ ./a.out
13499 us.

Rust优化版本仅与gcc -O0相比,并且与gcc -O2相比非常弱。这合理吗?如何提高铁锈版的性能?


最佳答案:

rust将循环编译为:

.LBB0_1:
    movupd  xmm0, xmmword ptr [rcx + 8*rax - 48]
    movupd  xmm1, xmmword ptr [rcx + 8*rax - 32]
    addpd   xmm0, xmm0
    addpd   xmm1, xmm1
    movupd  xmmword ptr [rcx + 8*rax - 48], xmm0
    movupd  xmmword ptr [rcx + 8*rax - 32], xmm1
    movupd  xmm0, xmmword ptr [rcx + 8*rax - 16]
    movupd  xmm1, xmmword ptr [rcx + 8*rax]
    addpd   xmm0, xmm0
    addpd   xmm1, xmm1
    movupd  xmmword ptr [rcx + 8*rax - 16], xmm0
    movupd  xmmword ptr [rcx + 8*rax], xmm1
    add rax, 8
    cmp rax, 100006
    jne .LBB0_1

GCC 7.1.0编译为:
L6:
    movsd   (%rbx), %xmm0
    addq    $8, %rbx
    addsd   %xmm0, %xmm0
    movsd   %xmm0, -8(%rbx)
    cmpq    %rbp, %rbx
    jne     L6

rust将数组放在一个数据段中,而c实际上将(memset和pattern)写入内存。这意味着运行应用程序的操作系统可能只是映射范围,并依赖虚拟内存来做正确的事情。
如果将代码更改为在度量之前运行同一个循环,则运行时将大大减少。它实际上比我机器上的C版本快。(可能是因为循环展开)
unsafe {
    for i in 0..STREAM_ARRAY_SIZE {
        A[i] = 2.0E0 * A[i];
    }
}

let now = Instant::now();

unsafe {
    for i in 0..STREAM_ARRAY_SIZE {
        A[i] = 2.0E0 * A[i];
    }
}

let duration = now.elapsed();