【问题标题】:Why is my Rust version of "wc" slower than the one from GNU coreutils?为什么我的 Rust 版本的“wc”比 GNU coreutils 中的慢?
【发布时间】:2017-08-02 08:18:05
【问题描述】:

考虑这个程序:

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

fn main() {
    let mut n = 0;
    let stdin = io::stdin();
    for _ in stdin.lock().lines() {
        n += 1;
    }
    println!("{}", n);
}

为什么它比 GNU 版本的 wc 慢 10 倍以上?看看我是如何测量它的:

$ yes | dd count=1000000 | wc -l
256000000
1000000+0 records in
1000000+0 records out
512000000 bytes (512 MB, 488 MiB) copied, 1.16586 s, 439 MB/s
$ yes | dd count=1000000 | ./target/release/wc
1000000+0 records in
1000000+0 records out
512000000 bytes (512 MB, 488 MiB) copied, 41.685 s, 12.3 MB/s
256000000

【问题讨论】:

  • 只是一个猜测,但是:我会说wc 可能不会为每一行分配一个新的动态调整大小的字符串。
  • 用 Rust 为 Redox 编写的 coreutils 最少;你可以查看their implementation of wc
  • 将其描述为“Rust 的 wc 版本”而不是 您的 版本的wc 是不是有点不对劲?
  • @Damien_The_Unbeliever 你可能会争论两种方式——要么我没有很好地优化它(从我在这里得到的答案来看,这并不简单)或者 Rust 没有。

标签: rust benchmarking gnu-coreutils


【解决方案1】:

您的代码比原来的wc 慢的原因有很多。有些东西是你付钱买的,实际上你根本不需要。通过删除这些,您已经可以获得相当可观的速度提升。

堆分配

BufRead::lines() 返回 an iterator 产生 String 元素。由于这种设计,它将(它必须!)为每一行分配内存。 lines() 方法是一种方便的方法,可以轻松编写代码,但不应该在高性能情况下使用。

为避免为每一行分配堆内存,您可以改用BufRead::read_line()。代码有点冗长,但如你所见,我们正在重用s的堆内存:

let mut n = 0;
let mut s = String::new();
let stdin = io::stdin();
let mut lock = stdin.lock();
loop {
    s.clear();
    let res = lock.read_line(&mut s);
    if res.is_err() || res.unwrap() == 0 {
        break;
    }
    n += 1;
}
println!("{}", n);

在我的笔记本上,结果是:

$ yes | dd count=1000000 | wc -l
256000000
1000000+0 records in
1000000+0 records out
512000000 bytes (512 MB, 488 MiB) copied, 0,981827 s, 521 MB/s

$ yes | dd count=1000000 | ./wc 
1000000+0 records in
1000000+0 records out
512000000 bytes (512 MB, 488 MiB) copied, 6,87622 s, 74,5 MB/s
256000000

如您所见,它改进了很多,但仍然不等同。

UTF-8 验证

由于我们正在读取String,因此我们正在验证来自标准输入的原始输入是否为正确的 UTF-8。这需要时间!但是我们只对原始字节感兴趣,因为我们只需要计算换行符(0xA)。我们可以通过使用 Vec<u8>BufRead::read_until() 来摆脱 UTF-8 检查:

let mut n = 0;
let mut v = Vec::new();
let stdin = io::stdin();
let mut lock = stdin.lock();
loop {
    v.clear();
    let res = lock.read_until(0xA, &mut v);
    if res.is_err() || res.unwrap() == 0 {
        break;
    }
    n += 1;
}
println!("{}", n);

这会导致:

1000000+0 records in
1000000+0 records out
512000000 bytes (512 MB, 488 MiB) copied, 4,24162 s, 121 MB/s
256000000

这是 60% 的改进。但原来的 wc 仍然快 3.5 倍!

进一步可能的改进

现在,我们用尽了所有唾手可得的成果来提升性能。为了匹配wcs 的速度,我认为必须进行一些认真的分析。在我们当前的解决方案中,perf 报告以下内容:

  • 大约 11% 的时间花在 memchr 上;我认为这无法改进
  • 大约 18% 用于<StdinLock as std::io::BufRead>::fill_buf()
  • 大约 6% 用于<StdinLock as std::io::BufRead>::consume()

剩余时间的很大一部分直接花费在main 中(由于内联)。从表面上看,我们也为跨平台抽象付出了一些代价。 Mutex 方法和东西花了一些时间。

但在这一点上,我只是猜测,因为我没有时间进一步研究。对不起:

但请注意,wc 是一个旧工具,并且针对其运行的平台和执行的任务进行了高度优化。我想有关 Linux 内部事物的知识会对性能有很大帮助。这真的很专业,所以我不希望能轻易匹配性能。

【讨论】:

  • 我是14.9 MB/s
  • @d33tah:我知道,它仍然不几乎等同。现在,我正在深入研究这一点。原因可能包括 UTF8 验证和类似的东西。当我确定结果时会更新答案。如果我没有发现任何有用的东西,我会删除它。抱歉发布不完整的答案:'(
  • 我只是在文章开头将其命名为“小改进”并保持原样;)
  • 要真正获得这么快,您可能需要读取 16/32 字节的块,然后使用 sse/avx + bmi2 来计算 OxA 的数量。我不知道生锈是否可能,但这应该会加快速度很多:如果您可以从 L1 读取,则没有分支错误并且每个周期大约 32 字节。
  • 我希望能够执行wc 而无需在用户区复制字节。计算各种标记字节 (SSE/AVX) 的单遍应该就足够了,并且会使其成为 I/O 或内存绑定程序。上面的任何东西(使用String/Vec,...)都会增加一些开销。
【解决方案2】:

这是因为您的版本绝不等同于不为字符串分配任何内存的 GNU 版本,而只是移动文件指针并递增不同的计数器。此外,它处理原始字节,而 Rust 的 String 必须是有效的 UTF-8。

GNU wc source

【讨论】:

    【解决方案3】:

    这是我在#rust-beginners IRC 上从 Arnavion 获得的版本:

    use std::io::Read;
    
    fn main() {
        let mut buffer = [0u8; 1024];
        let stdin = ::std::io::stdin();
        let mut stdin = stdin.lock();
        let mut wc = 0usize;
        loop {
            match stdin.read(&mut buffer) {
                Ok(0) => {
                    break;
                },
                Ok(len) => {
                    wc += buffer[0..len].into_iter().filter(|&&b| b == b'\n').count();
                },
                Err(err) => {
                    panic!("{}", err);
                },
            }
        };
        println!("{}", wc);
    }
    

    这使得性能非常接近原始wc 所做的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-12-15
      • 1970-01-01
      • 2021-05-30
      • 2016-08-20
      • 2012-06-20
      • 2014-10-05
      相关资源
      最近更新 更多