在 Rust 与 C 中对 2D 数组性能的迭代



我是 Rust 的新手,最近我一直在搞砸它。与 C 相比,我对在 Rust 中访问带有索引的数组的性能感到好奇。

我做了这两个程序:

fn main() {
    let mut arr: [[i32; 1000]; 1000] = [[0; 1000]; 1000];
    for t in 0..1000 {
        for i in 0..1000 {
            for j in 0..1000 {
                arr[i][j] = (i * j) as i32;
            }
        }
    }
}

在 C 中:

#include <stdlib.h>
#include <string.h>
#define ARRSIZE 1000
int main() {
    int ** arr = malloc(sizeof(int*) * ARRSIZE);
    int i, j, t;
    for (i = 0; i < ARRSIZE; ++i) {
        arr[i] = malloc(sizeof(int) * ARRSIZE);
        memset((void*) arr[i], 0, sizeof(int) * ARRSIZE);
    }
    for (t = 0; t < ARRSIZE; ++t) {
        for (i = 0; i < ARRSIZE; ++i) {
            for (j = 0; j < ARRSIZE; ++j) {
                arr[i][j] = i * j;
            }
        }
    }
    for (i = 0; i < ARRSIZE; ++i) {
        free(arr[i]);
    }
    free(arr);
}

这个想法是创建一个 1000x1000 的 2D 数组,并在每次迭代中对每个元素进行 1000 次迭代,执行简单的算术运算。

两者的性能差距很大(C 版本需要近 3 秒,Rust 版本需要 45 秒)。这是正常的,还是我在 Rust 版本中做错了什么?

编辑:我尝试禁用边界检查并得到相同的结果。

谢谢。

除了DK的答案。

为了以(或多或少)有意义的方式(以及供其他人使用)进行测试,我修改了程序,如下所示:

锈:

#![feature(test)]
extern crate test;
fn main() {
    let mut arr: [[i32; 1000]; 1000] = [[0; 1000]; 1000];
    for _ in 0..1000 {
        for i in 0..1000 {
            for j in 0..1000 {
                arr[i][j] = (i * j) as i32;
            }
        }
    }
    test::black_box(arr);
}

C(使用堆栈,代码主要由DK。

#define ARRSIZE 1000
int main() {
    int arr[ARRSIZE][ARRSIZE] = { { 0 } };
    int i, j, t;
    for (t = 0; t < ARRSIZE; ++t) {
        for (i = 0; i < ARRSIZE; ++i) {
            for (j = 0; j < ARRSIZE; ++j) {
                arr[i][j] = i * j;
            }
        }
    }
    asm ("" : : "r" (arr));
}

black_boxasm(...)用于防止优化程序删除所有代码。但是,我使用clang而不是gcc来完成这项工作。所以相比之下:

$ rustc -O test.rs      |      $ clang -O2 test.c 
$ time ./test           |      $ time ./a.out 
                        |    
real    0m0.537s        |      real    0m0.546s
user    0m0.532s        |      user    0m0.544s
sys     0m0.004s        |      sys     0m0.004s

同一程序的单次执行之间的执行时间变化大于这两个程序在运行时的差异。

我想说的(以及DK已经说过的):差异应该是微不足道的。两者都应该做完全相同的工作;只是 Rust 也进行绑定检查。但在这种情况下,LLVM 优化器可能会删除这些内容。请记住在发布模式下构建;)

首先,我假设您在没有优化的情况下进行编译,因为我无法重现您描述的时间,而无需在调试模式下进行编译,这显然不会积极优化。 在这种情况下,这种差异并不那么令人惊讶,因为 Rust 正在做更多的工作,并且已知在调试模式下生成次优代码。

其次,这两个程序并不等同。 C 代码分配了 1001 个堆数组,Rust 代码没有分配任何堆数组。 因此,一旦你打开优化,Rust 代码就会比 C 代码运行得更快

所以现在我们需要修改 C 程序以分配。 鉴于此:

#define ARRSIZE 1000
int main() {
    int arr[ARRSIZE][ARRSIZE] = { { 0 } };
    int i, j, t;
    for (t = 0; t < ARRSIZE; ++t) {
        for (i = 0; i < ARRSIZE; ++i) {
            for (j = 0; j < ARRSIZE; ++j) {
                arr[i][j] = i * j;
            }
        }
    }
}

我使用 gcc -O(使用 GCC 4.8.4)和rustc -O(使用 Rust 1.7.0)编译的结果是:

$ time ./c-2; time ./rs-1
real    0m0.335s
user    0m0.328s
sys 0m0.000s
real    0m0.002s
user    0m0.000s
sys 0m0.000s

这些都太短了,毫无意义。 但情况变得更糟。 Rust 程序如此之快的原因是程序非常简单,LLVM 完全删除了它。 该程序没有明显的副作用,因此它只是编译为立即退出的空二进制文件。

从这个基准测试中可以收集到任何有意义的东西,除了 Rust 产生缓慢的调试可执行文件(这已经是众所周知的)。

最新更新