我们将尝试使用 Rust 来比较读取文件的各种不同方法。除了 wc -l 之外,我们将使用 criterion 对每个函数运行 10 次,然后取平均值。
以下基准测试的代码存放在 Benchmark code for Linux IO using Rust 。 在以下代码中,BUFFER_SIZE 为 8192,NUM_BUFFERS 为 32。
原文: [# Linux File IO using Rust](Linux File IO using Rust (opdroid.org) ) by opdroid
测试机器细节
Framework 16 笔记本,带有锐龙 7840 HS 处理器和 64 G 内存的电脑。电源已接通并启用了性能模式。(这个笔记本是一个模块化的笔记本)
SSD: WD_BLACK SN850X 4000GB。使用Gnome Disks进行测试显示读取速度为3.6 GB/s(样本大小为1000MB,共100个样本)。
文件系统:btrfs
操作系统版本 (uname 结果):Linux fedora 6.8.8-300. Fc 40. X 86_64 #1 SMP PREEMPT_DYNAMIC Sat Apr 27 17:53:31 UTC 2024 x 86_64 GNU/Linux
文件细节
未压缩大小:22G
行数:200,000,000
使用 btrfs 压缩(zstd)后的压缩大小:5.3G
对于不耐烦的读者:结果概述
读取方法
时间 (单位:秒)
Mmap with AVX512
2.61
Mmap with AVX2
2.64
io_uring with Vectored IO
2.86
Vectored IO
2.89
Mmap
3.43
io_uring
5.26
wc -l (baseline)
8.01
Direct IO
10.56
BufReader without appends
15.94
BufReader with lines().count()
33.50
一个有趣的观察是,AVX512 需要 2.61 秒,文件大小约为 22G,而 SSD 基准测试显示读取速度为 3.6 GB/s。这意味着文件应该在大约 6 秒内被读取完毕。但 AVX512 的实现实际上是以约 8.4 GB/s 的速度读取文件。
这是怎么回事呢?比磁盘的读取速度都快不少?不科学啊?
原来 Fedora 使用了 btrfs 文件系统,它默认启用了 zstd 压缩。实际的磁盘上大小可以使用 compsize 命令来查看。
1 2 3 4 5 6 opdroid@box:~/tmp$ sudo compsize data Processed 1 file, 177437 regular extents (177437 refs), 0 inline. Type Perc Disk Usage Uncompressed Referenced TOTAL 24% 5.3G 21G 21G none 100% 32K 32K 32K zstd 24% 5.3G 21G 21G
感谢这些优秀的人
@alextjensen - 感谢他指导我使用 BufReader 的合理默认值,并编译为本机架构。
@aepau2 - 感谢他发现了 wc 数字中的一个明显错误。我在使用 wc 测量之前忘记了清空缓存。
@rflaherty71 - 感谢他建议我使用更多且更大的缓冲区(64 x 64k)。
@daniel_c0deb0t - 感谢他建议我使用更大的缓冲区。
不使用我们编写的代码作为基线总是一个好主意,这样比较客观。
使用 wc -l 作为基线 1 2 3 4 5 6 opdroid@box:~/tmp$ time wc -l data 200000000 data real 0m8.010s user 0m0.193s sys 0m7.591s
在每个函数的末尾,我们使用以下命令重置文件缓存。我还没有弄清楚如何在 criterion 中使用 teardown 函数,以便这个时间不被计入总耗时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // TODO: move to a teardown function in criterion fn reset_file_caches() { // Execute the command to reset file caches let output = Command::new("sudo") .arg("sh") .arg("-c") .arg("echo 3 > /proc/sys/vm/drop_caches") .output() .expect("Failed to reset file caches"); // Check if the command executed successfully if !output.status.success() { panic!("Failed to reset file caches: {:?}", output); } }
方法1:使用 BufReader 读取文件,并使用 reader.lines().count() 计算行数 1 2 3 4 5 6 7 8 9 fn count_newlines_standard (filename: &str ) -> Result <usize , std::io::Error> { let file = File::open (filename)?; let reader = BufReader::with_capacity (16 * 1024 , file); let newline_count = reader.lines ().count (); reset_file_caches (); Ok (newline_count) }
在我的机器上,这需要大约 36.5 秒的时间。
在 count_newlines_standard 函数中,字符串拼接(String appends)可能是导致性能问题的原因。
方法 2:使用 BufReader 读取文件并避免字符串拼接 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 pub fn count_newlines_standard_non_appending (filename: &str ) -> Result <usize , std::io::Error> { let file = File::open (filename)?; let mut reader = BufReader::with_capacity (64 * 1024 , file); let mut newline_count = 0 ; loop { let len = { let buffer = reader.fill_buf ()?; if buffer.is_empty () { break ; } newline_count += buffer.iter ().filter (|&&b| b == b'\n ' ).count (); buffer.len () }; reader.consume (len); } reset_file_caches (); Ok (newline_count) }
在我的机器上,这大约需要 15.94 秒。这比使用字符串拼接的版本快了一半以上。
当我们查看火焰图时,我们可以确认字符串拼接的操作已经不存在了。
方法 3:使用 Direct I/O 读取文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fn count_newlines_direct_io (filename: &str ) -> Result <usize , Error> { let mut open_options = File::options (); open_options.read (true ).custom_flags (libc::O_DIRECT); let mut file = open_options.open (filename)?; let mut buffer = vec! [0 ; BUFFER_SIZE]; let mut newline_count = 0 ; loop { let bytes_read = file.read (&mut buffer)?; if bytes_read == 0 { break ; } let chunk_newline_count = buffer[..bytes_read].iter ().filter (|&&b| b == b'\n ' ).count (); newline_count += chunk_newline_count; } reset_file_caches (); Ok (newline_count) }
在我的机器上,这大约需要 35.7 秒。
方法 4:使用内存映射(Mmap)读取文件 1 2 3 4 5 6 7 8 fn count_newlines_memmap (filename: &str ) -> Result <usize , Error> { let file = File::open (filename)?; let mmap = unsafe { Mmap::map (&file)? }; let newline_count = mmap.iter ().filter (|&&b| b == b'\n ' ).count (); reset_file_caches (); Ok (newline_count) }
在我的机器上,这大约需要 8.3 秒。
方法 5:使用内存映射(Mmap)和 AVX2 指令集读取文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 unsafe fn count_newlines_memmap_avx2 (filename: &str ) -> Result <usize , Error> { let file = File::open (filename)?; let mmap = unsafe { Mmap::map (&file)? }; let newline_byte = b'\n ' ; let newline_vector = _mm256_set1_epi8(newline_byte as i8 ); let mut newline_count = 0 ; let mut ptr = mmap.as_ptr (); let end_ptr = unsafe { ptr.add (mmap.len ()) }; while ptr <= end_ptr.sub (32 ) { let data = unsafe { _mm256_loadu_si256(ptr as *const __m256i) }; let cmp_result = _mm256_cmpeq_epi8(data, newline_vector); let mask = _mm256_movemask_epi8(cmp_result); newline_count += mask.count_ones () as usize ; ptr = unsafe { ptr.add (32 ) }; } let remaining_bytes = end_ptr as usize - ptr as usize ; newline_count += mmap[mmap.len () - remaining_bytes..].iter ().filter (|&&b| b == newline_byte).count (); reset_file_caches (); Ok (newline_count) }
这个方法在我的机器上大约耗时 2.64 秒。
方法 6:使用内存映射(Mmap)和 AVX-512 指令集读取文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 unsafe fn count_newlines_memmap_avx512 (filename: &str ) -> Result <usize , Error> { let file = File::open (filename)?; let mmap = unsafe { Mmap::map (&file)? }; let newline_byte = b'\n ' ; let newline_vector = _mm512_set1_epi8(newline_byte as i8 ); let mut newline_count = 0 ; let mut ptr = mmap.as_ptr (); let end_ptr = unsafe { ptr.add (mmap.len ()) }; while ptr <= end_ptr.sub (64 ) { let data = unsafe { _mm512_loadu_si512(ptr as *const i32 ) }; let cmp_result = _mm512_cmpeq_epi8_mask(data, newline_vector); newline_count += cmp_result.count_ones () as usize ; ptr = unsafe { ptr.add (64 ) }; } let remaining_bytes = end_ptr as usize - ptr as usize ; newline_count += mmap[mmap.len () - remaining_bytes..].iter ().filter (|&&b| b == newline_byte).count (); reset_file_caches (); Ok (newline_count) }
这个方法在我的机器上大约需要 2.61 秒。
方法 7:使用向量 I/O 读取文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 fn count_newlines_vectored_io (path: &str ) -> Result <usize , Error> { let mut file = File::open (path)?; let mut buffers_ : Vec <_> = (0 ..16 ).map (|_| vec! [0 ; BUFFER_SIZE]).collect (); let mut buffers : Vec <_> = buffers_.iter_mut ().map (|buf| io::IoSliceMut::new (buf)).collect (); let mut newline_count = 0 ; loop { let bytes_read = file.read_vectored (&mut buffers)?; if bytes_read == 0 { break ; } let filled_buffers = bytes_read / BUFFER_SIZE; for buf in &buffers[..filled_buffers] { newline_count += buf.iter ().filter (|&&b| b == b'\n ' ).count (); } if filled_buffers < buffers.len () { let last_buffer = &buffers[filled_buffers]; let end = bytes_read % BUFFER_SIZE; newline_count += last_buffer[..end].iter ().filter (|&&b| b == b'\n ' ).count (); } } Ok (newline_count) }
在我的机器上,这大约需要 7.7 秒。
方法 8:使用 io_uring 读取文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 fn count_lines_io_uring (path: &str ) -> io::Result <usize > { let file = File::open (path)?; let fd = file.as_raw_fd (); let mut ring = IoUring::new (8 )?; let mut line_count = 0 ; let mut offset = 0 ; let mut buf = vec! [0 ; 4096 ]; let mut read_size = buf.len (); loop { let mut sqe = opcode::Read::new (types::Fd (fd), buf.as_mut_ptr (), read_size as _) .offset (offset as _) .build () .user_data (line_count as _); unsafe { ring.submission () .push (&mut sqe) .expect ("submission queue is full" ); } ring.submit_and_wait (1 )?; let cqe = ring.completion ().next ().expect ("completion queue is empty" ); let bytes_read = cqe.result () as usize ; line_count = cqe.user_data () as usize ; if bytes_read == 0 { break ; } let data = &buf[..bytes_read]; line_count += data.iter ().filter (|&&b| b == b'\n ' ).count (); offset += bytes_read as u64 ; read_size = (buf.len () - (offset as usize % buf.len ())) as usize ; } Ok (line_count) }
在我的机器上,这大约需要 10.5 秒。
方法 9:使用带有向量 I/O 的 io_uring 读取文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 fn count_lines_io_uring_vectored (path: &str ) -> io::Result <usize > { let file = File::open (path)?; let fd = file.as_raw_fd (); let mut ring = IoUring::new (NUM_BUFFERS as u32 )?; let mut line_count = 0 ; let mut offset = 0 ; let mut buffers = vec! [vec! [0 ; 8192 ]; NUM_BUFFERS]; let mut iovecs : Vec <iovec> = buffers .iter_mut () .map (|buf| iovec { iov_base: buf.as_mut_ptr () as *mut _, iov_len: buf.len (), }) .collect (); loop { let mut sqe = opcode::Readv::new (types::Fd (fd), iovecs.as_mut_ptr (), iovecs.len () as _) .offset (offset as _) .build () .user_data (0 ); unsafe { ring.submission () .push (&mut sqe) .expect ("submission queue is full" ); } ring.submit_and_wait (1 )?; let cqe = ring.completion ().next ().expect ("completion queue is empty" ); let bytes_read = cqe.result () as usize ; if bytes_read == 0 { break ; } let mut buffer_line_count = 0 ; let mut remaining_bytes = bytes_read; for buf in &buffers[..iovecs.len ()] { let buf_size = buf.len (); let data_size = remaining_bytes.min (buf_size); let data = &buf[..data_size]; buffer_line_count += data.iter ().filter (|&&b| b == b'\n ' ).count (); remaining_bytes -= data_size; if remaining_bytes == 0 { break ; } } line_count += buffer_line_count; offset += bytes_read as u64 ; } Ok (line_count) }
在我的机器上,这大约需要 7.6 秒。