提问人:ducktherapy 提问时间:4/19/2023 最后编辑:Sven Marnachducktherapy 更新时间:4/25/2023 访问量:688
为什么缓冲区容量越大,“File::read_to_end”越慢?
Why does `File::read_to_end` get slower the larger the buffer capacity?
问:
通知:截至 2023 年 4 月 23 日,此问题的修复程序落在 rust-lang/rust:master
上。您很快就可以使用File::read_to_end
而不必担心这些。
我正在研究一个非常具体的问题,需要我读取数十万个文件,从几个字节到几百兆字节不等。由于大部分操作包括枚举文件和从磁盘移动数据,因此我求助于重用缓冲区进行文件读取,以期避免一些内存管理。Vec
就在这时,我遇到了意想不到的问题:缓冲区的容量越大,速度就越慢。首先读取 300MB 文件,然后读取一千个 1KB 文件比反之慢得多(只要我们不截断缓冲区)。file.read_to_end(&mut buffer)?
令人困惑的是,如果我将文件包装在 or use 中,则不会发生减速。Take
read_exact()
有谁知道这是怎么回事?它是否有可能在每次调用时(重新)初始化整个缓冲区?这是特定于 Windows 的怪癖吗?在处理此类问题时,您会推荐哪些(基于 Windows)的分析工具?
下面是一个简单的复制品,它演示了这些方法之间的巨大(在这台机器上是 50x+)性能差异,忽略磁盘速度:
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]),
}
}
尝试了几种模式组合,甚至没有显着差异。--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");
运行时几乎完全线性地缩放。
使用货物火焰图
可以清楚地显示:NtReadFile
需要 95% 的时间。
在版本中只需要 10%。换句话说,你的 Rust 代码没有错。exact
Windows 文档没有对缓冲区的长度提出任何建议,但从阅读 rust 标准库来看,它确实给出了 的全部备用容量,并且从基准测试中可以明显看出,它正在对缓冲区中的每个字节做一些事情。NtReadFile
Vec
NtReadFile
我相信这种方法在这里是最好的。std::fs::read
还会在读取之前查询文件的长度,尽管它始终具有适当大小的缓冲区,因为它创建了 .它仍然使用,以便即使长度在两者之间发生了变化,它也会返回更正确的文件。如果你想重用 ,你需要以其他方式执行此操作。exact
Vec
read_to_end
Vec
确保你选择的任何内容都比每次重新创建都快,我尝试了一下,得到了几乎相同的性能。释放未使用的内存对性能有好处,因此它是否使您的程序更快将取决于具体情况。Vec
exact
您还可以将短文件和长文件的代码路径分开。
最后,确保你需要整个文件。如果可以一次使用 BufReader
块进行处理,则使用 fill_buf
和 consume
,则可以完全避免此问题。
评论
Vec
read_exact
naive vs take
naive
take
exact
naive
take
take
take
take(BUFFER_SIZE)
评论
rustc 1.68.2 (9eb3afe9e 2023-03-27)
x86_64-pc-windows-msvc
hyperfine 'binary naive' 'binary take' 'binary exact'
unsafe
exact
vec!
exact
take
naive
vec!
exact
set_len