我当时正在处理一个非常具体的问题,它要求我读取数十万个从几个字节到几百兆字节的文件.由于大部分操作包括枚举文件和从磁盘移动数据,我求助于重用Vec个缓冲区来读取文件,希望避免一些内存管理.

这就是我遇到意外的时候:缓冲区的容量越大,file.read_to_end(&mut buffer)?的速度就越慢.首先读取300MB的文件,然后读取file.read_to_end(&mut buffer)?0个1KB的文件,这比反过来读取要慢得多(只要我们不截断缓冲区).

令人困惑的是,如果我将文件包装在Takeread_exact()中,则不会发生减慢.

有人知道这是怎么回事吗?是否有可能在每次调用时都(重新)初始化整个缓冲区?这是Windows特有的怪癖吗?在处理此类问题时,您会推荐哪些(基于Windows的)分析工具?

以下是一个简单的复制品,它演示了在不考虑磁盘速度的情况下,这两种方法之间的huge(在这台机器上是50倍以上)性能差异:

use std::io::Read;
use std::fs::File;

// with a smaller buffer, there's basically no difference between the methods...
// const BUFFER_SIZE: usize = 2 * 1024;

// ...but the larger the Vec, the bigger the discrepancy.
// for simplicity's sake, let's assume this is a hard upper limit.
const BUFFER_SIZE: usize = 300 * 1024 * 1024;


fn naive() {
    let mut buffer = Vec::with_capacity(BUFFER_SIZE);

    for _ in 0..100 {
        let mut file = File::open("some_1kb_file.txt").expect("opening file");

        let metadata = file.metadata().expect("reading metadata");
        let len = metadata.len();
        assert!(len <= BUFFER_SIZE as u64);

        buffer.clear();
        file.read_to_end(&mut buffer).expect("reading file");

        // do "stuff" with buffer
        let check = buffer.iter().fold(0usize, |acc, x| acc.wrapping_add(*x as usize));

        println!("length: {len}, check: {check}");
    }
}

fn take() {
    let mut buffer = Vec::with_capacity(BUFFER_SIZE);

    for _ in 0..100 {
        let file = File::open("some_1kb_file.txt").expect("opening file");

        let metadata = file.metadata().expect("reading metadata");
        let len = metadata.len();
        assert!(len <= BUFFER_SIZE as u64);

        buffer.clear();
        file.take(len).read_to_end(&mut buffer).expect("reading file");

        // this also behaves like the straight `read_to_end` with a significant slowdown:
        // file.take(BUFFER_SIZE as u64).read_to_end(&mut buffer).expect("reading file");

        // do "stuff" with buffer
        let check = buffer.iter().fold(0usize, |acc, x| acc.wrapping_add(*x as usize));

        println!("length: {len}, check: {check}");
    }
}

fn exact() {
    let mut buffer = vec![0u8; BUFFER_SIZE];

    for _ in 0..100 {
        let mut file = File::open("some_1kb_file.txt").expect("opening file");

        let metadata = file.metadata().expect("reading metadata");
        let len = metadata.len() as usize;
        assert!(len <= BUFFER_SIZE);

        // SAFETY: initialized by `vec!` and within capacity by `assert!`
        unsafe { buffer.set_len(len); }
        file.read_exact(&mut buffer[0..len]).expect("reading file");

        // do "stuff" with buffer
        let check = buffer.iter().fold(0usize, |acc, x| acc.wrapping_add(*x as usize));

        println!("length: {len}, check: {check}");
    }
}

fn main() {
    let args: Vec<String> = std::env::args().collect();

    if args.len() < 2 {
        println!("usage: {} <method>", args[0]);
        return;
    }

    match args[1].as_str() {
        "naive" => naive(),
        "take" => take(),
        "exact" => exact(),
        _ => println!("Unknown method: {}", args[1]),
    }
}

try 了在--release模式的几种组合中,LTO甚至+crt-static都没有明显的差别.

推荐答案

我试着用数字递增的take:

// Run with different values of `take` from 10_000_000 to 300_000_000
file.take(take)
    .read_to_end(&mut buffer)
    .expect("reading file");

并且运行时几乎完全线性地随之扩展.

graph of time vs. take showing a linear correlation

使用cargo flamegraph可以清楚地看到:NtReadFile占95%的时间.

flamegraph of the executable

exact版本中只需10%.换句话说,您的防 rust 代码不是错误.

Windows文档没有提出任何关于缓冲区长度的建议,但从阅读Ruust标准库可以看出,NtReadFile似乎得到了Vec的全部空闲容量,从基准测试中可以明显看出,NtReadFile对缓冲区中的每个字节都是something.

我相信exact方法在这里是最好的.std::fs::read还会在读取之前查询文件的长度,尽管它总是有一个大小合适的缓冲区,因为它创建了Vec.它仍然使用read_to_end,因此即使长度在两者之间发生变化,它也会返回更正确的文件.如果您想要重复使用Vec,则需要通过其他方式来实现.

确保无论你 Select 什么,都比每次重新创建Vec更快,我try 了一下,得到了几乎与exact相同的性能.释放未使用的内存对性能有好处,因此它是否会使您的程序更快将取决于情况.

您还可以分隔短文件和长文件的代码路径.

最后,确保您需要整个文件.如果您可以一次处理BufReader个块、fill_buf个和consume个块,您就可以完全避免这个问题.

Rust相关问答推荐

如何从polars DataFrame中获取一个列作为Option String?<>

我怎样才能从一个Rust 的日期中go 掉3年?

Rust:跨多个线程使用hashmap Arc和rwlock

我如何在Rust中使用传递依赖中的特征?

有没有更好的方法从HashMap的条目初始化 struct ?

如何go 除多余的(0..)在迭代中,当它不被使用时?

写入引用会更新基础值,但引用会打印意外的值

Rust编译器似乎被结果类型与anyhow混淆

允许 rust 迹 struct 条目具有多种类型

由于生存期原因,返回引用的闭包未编译

Rust 重写函数参数

如何在 Rust 中打印 let-else 语句中的错误?

在没有任何同步的情况下以非原子方式更新由宽松原子操作 Select 的值是否安全?

sha256 摘要仅适用于 &*

如何为整数切片定义一个带有额外函数的特性别名?

如何在Rust中使用Serde创建一个自定义的反序列化器来处理带有内部标记的枚举

Rust typestate 模式:实现多个状态?

错误:将自定义 proc_macro 与用Rust 的宝贝编写的属性一起使用时,无法在此范围内找到属性

为什么我可以同时传递可变和不可变引用?

当我在 struct 中存储异步函数时,为什么它需要生命周期