问题描述
这是一个比较两个函数的基准,这些函数将多个文件读入一个文件。一个使用read
,另一个使用read_to_end
。我最初的动机是在进程结束时让缓冲区的 capacity
等于 len
。 read_to_end
没有发生这种情况,这很不令人满意。
然而,使用 read
,这是有效的。 assert_eq!(buf.capacity(),buf.len());
的 read_files_into_file2
(使用 read
)不会恐慌。
use criterion::{criterion_group,criterion_main,Criterion};
use std::io::Read;
use std::io::Write;
use std::{
fs,io::{self,Seek},};
fn criterion_benchmark(c: &mut Criterion) {
let mut files = get_test_files().unwrap();
let mut file = fs::File::create("output").unwrap();
c.bench_function("1",|b| {
b.iter(|| {
read_files_into_file1(&mut files,&mut file).unwrap();
})
});
c.bench_function("2",|b| {
b.iter(|| {
read_files_into_file2(&mut files,&mut file).unwrap();
});
});
}
criterion_group!(benches,criterion_benchmark);
criterion_main!(benches);
/// Goes back to the start so that the files can be read again from the start.
fn reset(files: &mut Vec<fs::File>,file: &mut fs::File) {
file.seek(io::SeekFrom::Start(0)).unwrap();
for file in files {
file.seek(io::SeekFrom::Start(0)).unwrap();
}
}
pub fn read_files_into_file1(files: &mut Vec<fs::File>,file: &mut fs::File) -> io::Result<()> {
reset(files,file);
let total_len = files
.iter()
.map(|file| file.Metadata().unwrap().len())
.sum::<u64>() as usize;
let mut buf = Vec::<u8>::with_capacity(total_len);
for file in files {
file.read_to_end(&mut buf)?;
}
file.write_all(&buf)?;
// assert_eq!(buf.capacity(),buf.len());
Ok(())
}
fn read_files_into_file2(files: &mut Vec<fs::File>,file);
let total_len = files
.iter()
.map(|file| file.Metadata().unwrap().len())
.sum::<u64>() as usize;
let mut vec: Vec<u8> = vec![0; total_len];
let mut buf = &mut vec[..];
for file in files {
match file.read(&mut buf) {
Ok(n) => {
buf = &mut buf[n..];
}
Err(err) if err.kind() == io::ErrorKind::Interrupted => {}
Err(err) => return Err(err),}
}
file.write_all(&vec)?;
// assert_eq!(vec.capacity(),vec.len());
Ok(())
}
/// Creates 5 files with content "hello world" 500 times.
fn get_test_files() -> io::Result<Vec<fs::File>> {
let mut files = Vec::<fs::File>::new();
for index in 0..5 {
let mut file = fs::Openoptions::new()
.read(true)
.write(true)
.truncate(true)
.create(true)
.open(&format!("test{}",index))?;
file.write_all("hello world".repeat(500).as_bytes())?;
files.push(file);
}
Ok(files)
}
如果您取消对 assert_eq!
的注释,那么您将看到只有 read_files_into_file1
(使用 read_to_end
)会因此恐慌失败:
thread 'main' panicked at 'assertion Failed: `(left == right)`
left: `55000`,right: `27500`',benches/bench.rs:53:5
read_files_into_file1
分配比需要更多的内存,而 read_files_into_file2
分配最佳数量。
尽管如此,结果表明它们的性能几乎相同(read_files_into_file1
需要 11.439 us,read_files_into_file2
需要 11.098 us):
1 time: [11.417 us 11.439 us 11.463 us]
change: [+3.7987% +3.9997% +4.1984%] (p = 0.00 < 0.05)
Performance has regressed.
Found 1 outliers among 100 measurements (1.00%)
1 (1.00%) high mild
2 time: [11.085 us 11.098 us 11.112 us]
change: [+0.1255% +0.5081% +0.9545%] (p = 0.01 < 0.05)
Change within noise threshold.
Found 4 outliers among 100 measurements (4.00%)
2 (2.00%) high mild
2 (2.00%) high severe
我希望 read_files_into_file2
会快得多,但当我增加文件大小时,它甚至会变慢。为什么 read_files_into_file2
不符合我的期望?有效地将多个文件读入一个的最佳方法是什么?
解决方法
read_to_end
在处理大文件时通常不是一个好主意,因为它会尝试将整个文件读入内存,这可能会导致交换或内存不足错误。
在 linux 上并假设使用 io::copy
单线程执行应该是最快的方法,因为它包含 optimizations for this case。
在其他平台上使用 io::copy
并将写入端包装在用于复制的 BufWriter lets you control the buffer size 中,这将有助于分摊系统调用成本。
如果您可以使用多个线程并且知道文件长度不会改变,那么您可以使用特定于平台的位置读/写方法,例如 read_at 并行读取多个文件并将数据写入目标文件中的正确位置。这是否确实提供了加速取决于许多因素。连接来自网络文件系统的许多小文件时,这可能是最有益的。
除了标准库之外,还有一些 crate 公开特定于平台的复制例程,这可能比简单的用户空间复制方法更快。