IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    “测试 Rust 的 I/O 性能”

    smallnest发表于 2024-05-09 23:42:14
    love 0

    我们将尝试使用 Rust 来比较读取文件的各种不同方法。除了 wc -l 之外,我们将使用 criterion 对每个函数运行 10 次,然后取平均值。

    以下基准测试的代码存放在 Benchmark code for Linux IO using Rust。
    在以下代码中,BUFFER_SIZE 为 8192,NUM_BUFFERS 为 32。

    原文: # Linux File IO using Rust](https://opdroid.org/rust-io.html)) by opdroid

    测试机器细节

    1. Framework 16 笔记本,带有锐龙 7840 HS 处理器和 64 G 内存的电脑。电源已接通并启用了性能模式。(这个笔记本是一个模块化的笔记本)
    2. SSD: WD_BLACK SN850X 4000GB。使用Gnome Disks进行测试显示读取速度为3.6 GB/s(样本大小为1000MB,共100个样本)。
    3. 文件系统:btrfs
    4. 操作系统版本 (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) };
    }
    // Count remaining bytes
    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) };
    }
    // Count remaining bytes
    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;
    }
    // Calculate how many buffers were filled
    let filled_buffers = bytes_read / BUFFER_SIZE;
    // Process the fully filled buffers
    for buf in &buffers[..filled_buffers] {
    newline_count += buf.iter().filter(|&&b| b == b'\n').count();
    }
    // Handle the potentially partially filled last buffer
    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 秒。



沪ICP备19023445号-2号
友情链接