为什么这个Rust程序运行得如此缓慢?我有什么遗漏吗?

18

我阅读了曼哈顿度量下的最小距离,并在Rust中重写了作者的“naive”实现。C++变体如下:

#include <utility>
#include <cstdio>
#include <cstdlib>

std::pair<int, int> pointsA[1000001];
std::pair<int, int> pointsB[1000001];

int main() {
    int n, t;
    unsigned long long dist;

    scanf("%d", &t);

    while(t-->0) {
        dist = 4000000000LL;
        scanf("%d", &n);

        for(int i = 0; i < n; i++) {
            scanf("%d%d", &pointsA[i].first, &pointsA[i].second);
        }

        for(int i = 0; i < n; i++) {
            scanf("%d%d", &pointsB[i].first, &pointsB[i].second);
        }

        for(int i = 0; i < n ;i++) {
            for(int j = 0; j < n ; j++) {
                if(abs(pointsA[i].first - pointsB[j].first) + abs(pointsA[i].second - pointsB[j].second) < dist)
                    dist = abs(pointsA[i].first - pointsB[j].first) + abs(pointsA[i].second - pointsB[j].second);
            }
        }
        printf("%lld\n", dist);
    }
}

这个 Rust 变体是:

use std::io;
use std::io::BufReader;
use std::io::BufRead;

fn read_array(stdin: &mut BufReader<io::Stdin>, array_len: usize, points: &mut Vec<(i32, i32)>) {
    let mut line = String::new();
    for _ in 0..array_len {
        line.clear();
        stdin.read_line(&mut line).unwrap();
        let mut item = line.split_whitespace();
        let x = item.next().unwrap().parse().unwrap();
        let y = item.next().unwrap().parse().unwrap();
        points.push((x, y));
    }
}

fn manhattan_dist(a: &(i32, i32), b: &(i32, i32)) -> u32 {
    ((a.0 - b.0).abs() + (a.1 - b.1).abs()) as u32
}

fn main() {
    let mut line = String::new();
    let mut stdin = BufReader::new(io::stdin());
    stdin.read_line(&mut line).unwrap();
    let n_iters = line.trim_right().parse::<usize>().unwrap();
    let mut points_a = Vec::with_capacity(10000);
    let mut points_b = Vec::with_capacity(10000);
    for _ in 0..n_iters {
        line.clear();
        stdin.read_line(&mut line).unwrap();
        let set_len = line.trim_right().parse::<usize>().unwrap();
        points_a.clear();
        points_b.clear();
        read_array(&mut stdin, set_len, &mut points_a);
        read_array(&mut stdin, set_len, &mut points_b);
        let mut dist = u32::max_value();
        for i in points_a.iter() {
            for j in points_b.iter() {
                dist = std::cmp::min(manhattan_dist(i, j), dist);
            }
        }
        println!("{}", dist);
    }
}

接着,我使用Python脚本生成了数据:

import random

ITER = 100
N = 10000
MAX_INT = 1000000

print("%d" % ITER)

for _ in range(0, ITER):
    print("%d" % N)
    for _ in range(0, N):
        print(random.randrange(-MAX_INT, MAX_INT + 1), random.randrange(1, MAX_INT + 1))
    for _ in range(0, N):
        print(random.randrange(-MAX_INT, MAX_INT + 1), random.randrange(-MAX_INT, 0))

我使用g++ -Ofast -march=native编译了两个变体,分别是C++和Rust。时间如下:

C++

real    0m7.789s
user    0m7.760s
sys     0m0.020s

Rust

real    0m28.589s
user    0m28.570s
sys     0m0.010s

为什么我的Rust代码比C++版本慢四倍? 我使用的是Rust 1.12.0-beta.1。

我添加了时间测量:

let now = SystemTime::now();
line.clear();
stdin.read_line(&mut line).unwrap();
let set_len = line.trim_right().parse::<usize>().unwrap();
points_a.clear();
points_b.clear();
read_array(&mut stdin, set_len, &mut points_a);
read_array(&mut stdin, set_len, &mut points_b);
io_time += now.elapsed().unwrap();

let now = SystemTime::now();
let mut dist = u32::max_value();
for i in points_a.iter() {
    for j in points_b.iter() {
        dist = std::cmp::min(manhattan_dist(i, j), dist);
    }
}
calc_time += now.elapsed().unwrap();

writeln!(&mut std::io::stderr(), "io_time: {}, calc_time: {}", io_time.as_secs(), calc_time.as_secs()).unwrap();则输出io_time: 0, calc_time: 27

我尝试了夜间版的rustc 1.13.0-nightly (e9bc1bac8 2016-08-24)

$ time ./test_rust < data.txt  > test3_res
io_time: 0, calc_time: 19

real    0m19.592s
user    0m19.560s
sys     0m0.020s
$ time ./test1 < data.txt  > test1_res

real    0m7.797s
user    0m7.780s
sys     0m0.010s

所以现在我的Core i7处理器与其他处理器相比有2.7倍的差距。


问题肯定是你的实现都不相等。请将 Rust 代码与 C++ 版本完全相同。在应用程序开始时获取 stdout 和 stdin 的句柄并锁定它们。直接写入 stdout 的缓冲区,而不使用会产生锁定和格式化开销的宏。 - user3704639
1
尝试使用env RUSTFLAGS="-C target-cpu=native" cargo build --release进行构建。Rust编译器拒绝使用各种高端CPU扩展,除非明确启用它们。 - user3704639
1
说句实话,BufReader并不是使用stdin的理想方式;尝试使用stdin.lock()代替它,它为您提供了BufRead,避免了重复锁定。但由于IO在此处的开销并不大,所以这种差异并没有太大意义。 - Veedrac
2
@user1244932:我邀请您阅读Reddit讨论此处,有很多评论我个人认为非常有趣,而且太长了,无法简洁地总结 :) - Matthieu M.
3个回答

46

区别当然在于-march=native……某种程度上是这样。Rust 通过 -C target_cpu=native 实现了这一点,但这并不能带来相同的速度优势。这是因为 LLVM 在这种情况下不愿进行矢量化,而 GCC 则会。您可能会注意到,使用 Clang(一个也使用 LLVM 的 C++ 编译器)也会产生相对较慢的代码。

为了鼓励 LLVM 进行矢量化,可以将主循环移入单独的函数中。或者,您可以使用本地块。如果您仔细编写代码,则可以:

let dist = {
    let mut dist = i32::max_value();
    for &(a, b) in &points_a[..n] {
        for &(c, d) in &points_b[..n] {
            dist = std::cmp::min(((a - c).abs() + (b - d).abs()), dist);
        }
    }
    dist
} as u32;

Rust 和 C++ 之间的差异几乎可以忽略不计(约为 4%)。


27
你在C++中看到的绝大多数性能是由标志-march=native所导致的。
该标志不等同于Rust的--release标志。它使用特定于编译CPU的CPU指令,因此数学计算特别快。
移除该标志会使C++代码变成19秒。
然后,在C++代码中存在不安全因素。没有检查任何输入。Rust代码对其进行了检查,您可以使用.unwrap() —— unwrap有一定的性能开销,其中包括断言,然后是必要的解开代码等。
使用if let而不是原始的unwrap或尽可能忽略结果,会再次降低Rust代码的执行时间。
Rust: 22秒
C++:19秒
为什么会有3秒钟的差距呢?通过一些尝试认为这是由println!printf之间的区别造成的,但我没有C++代码的确切数字。我可以说的是,当我将打印放在基准测试之外时,Rust代码会降至13秒。
简而言之:编译器标志不同,C++代码不安全。

1
-march=native 相当于 -C target-cpu=native。以下是我得到的时间:C++:12.417秒;使用 -march=native 的 C++:9.005秒;Rust:13.971秒;使用 -C target-cpu=native 的 Rust:11.943秒。 - Yohaï-Eliel Berreby
首先,这不是我的 c++ 代码,我在问题中提供了它的来源链接。 - user1244932
我有点怀疑你对println!printf的发现是否正确。我用O(N*log(N))的“快速”变体实现了这个算法,而不是O(N * N),但将输入/输出代码保留为原样,现在结果为0.7秒,类似于fast c ++的表现。因此,输入/输出不能超过0.1秒 - user1244932

2

我没有看到执行时间上的任何差别。在我的机器上,

C++:

real    0m19.672s
user    0m19.636s
sys     0m0.060s

Rust:

real    0m19.047s
user    0m19.028s
sys     0m0.040s

我使用rustc -O test.rs -o ./test编译了Rust代码,使用g++ -Ofast test.cpp -o test编译了C++代码。

我正在运行Linux内核4.6.3-040603-generic的Ubuntu 16.04操作系统。我运行这个程序的笔记本电脑配备有Intel(R) Core(TM) i5-6200U CPU和8GB的RAM,没有什么特别的配置。


在测量时间之前,我也运行了 cd /sys/devices/system/cpu/cpufreq/ && for i in seq 0 7; do echo performance > policy$i/scaling_governor; done,你也这样做了吗? - user1244932
不,我没有改变我的管理器设置,但管理器会在毫秒级别内做出反应,而基准测试需要数十秒钟的时间。最多,我希望管理器设置能够产生几分之几秒的差异。 - coder543
哦,使用“-march=native”运行C++代码将其加速到了6.7秒。 - coder543
我改用 nightly 进行重新运行,而不是 beta,性能差异从 4 倍降至 2.7 倍。 - user1244932
我在使用iccgcc时看到了这样的效果。在简单的矩阵乘法(1000x1000)中,icc的代码加速了10倍,因为它可以进行向量化处理,而gcc则不能。 - user1244932
显示剩余4条评论

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