注意:截至 2023 年 4 月 23 日,此问题的修复已在rust-lang/rust:master
. 您很快就可以File::read_to_end
不用再担心这些问题了。
我正在解决一个非常具体的问题,需要我读取数十万个文件,从几个字节到几百兆字节不等。由于大部分操作包括枚举文件和从磁盘移动数据,因此我重新使用Vec
缓冲区来读取文件,希望避免一些内存管理。
就在那时,我遇到了意想不到的情况:file.read_to_end(&mut buffer)?
缓冲区容量越大,速度就越慢。先读取 300MB 文件,然后再读取一千个 1KB 文件比相反读取要慢得多(只要我们不截断缓冲区)。
令人困惑的是,如果我将文件包装在 aTake
或 use中read_exact()
,则不会出现速度减慢的情况。
有谁知道那是关于什么的吗?是否有可能在每次调用时(重新)初始化整个缓冲区?这是 Windows 特有的怪癖吗?在处理此类问题时,您会推荐哪些(基于 Windows 的)分析工具?
下面是一个简单的复制,展示了这些方法之间巨大的性能差异(在本机上为 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]),
}
}
Run Code Online (Sandbox Code Playgroud)
尝试了几种--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");
Run Code Online (Sandbox Code Playgroud)
运行时间几乎与它成线性比例。
使用cargo flamegraph
可以提供清晰的图片:NtReadFile
需要 95% 的时间。
版本中只占10% exact
。换句话说,你的 Rust 代码没有问题。
Windows 文档没有对缓冲区的长度提出任何建议,但从阅读 rust 标准库来看,确实NtReadFile
给出了 的全部备用容量,Vec
并且从基准测试中可以明显看出,它在每个缓冲区上NtReadFile
都做了一些事情缓冲区中的字节。
我相信exact
这里的方法是最好的。std::fs::read
还在读取之前查询文件的长度,尽管它始终具有正确大小的缓冲区,因为它创建了Vec
. 它还仍然使用read_to_end
,以便即使长度之间发生变化,它也会返回更正确的文件。如果您想重用Vec
,则需要以其他方式执行此操作。
确保您选择的任何内容都比Vec
每次重新创建更快,我尝试了一下并获得了与exact
. 释放未使用的内存会带来性能优势,因此是否能让程序更快取决于具体情况。
您还可以分离短文件和长文件的代码路径。
最后,确保您需要整个文件。BufReader
如果您可以一次使用 和fill_buf
进行大块的处理consume
,则可以完全避免此问题。
归档时间: |
|
查看次数: |
722 次 |
最近记录: |