Rust:读写 CSV 性能

use*_*459 1 csv rust

我正在尝试获得我可以使用 Rust 读取和写入“大”CSV 文件的最大速度的指示性度量。

我有一个包含 1 亿行相同行的测试 CSV 文件:

SomeLongStringForTesting1, SomeLongStringForTesting2

该文件在磁盘上的大小为 4.84GB。

我已经编写了(大部分是复制的!)以下使用csv: 1.1.3crate 的代码:

use std::error::Error;

fn main() {
    read_and_write("C:/Dev/100MillionRows.csv", "C:/Dev/100MillionRowsCopy.csv").unwrap();
}

fn read_and_write(in_file_path: &str, out_file_path: &str) -> Result<(), Box<Error>> {
    let mut rdr = csv::ReaderBuilder::new()
        .has_headers(false)
        .from_path(in_file_path)?;

    let mut wtr = csv::WriterBuilder::new()
        .from_path(out_file_path)?;

    for result in rdr.records() {
        let record = result?;
        wtr.write_record(record.iter())?;
    }

    wtr.flush()?;

    Ok(())
}
Run Code Online (Sandbox Code Playgroud)

以“发布模式”构建,然后使用以下命令运行:

powershell -Command "Measure-Command {.\target\release\csv-performance.exe}"72.79 seconds, 71.01 seconds, 70.77 seconds三个运行的产量。

大致说来,我在 70 秒内看到 10GB(组合读写)的 IO,相当于 142MB/S。这大约是 Windows 在任务管理器中报告的磁盘使用情况。

感觉可能很慢,原因如下:

winsat disk -drive c 产量:

Windows System Assessment Tool
> Running: Feature Enumeration ''
> Run Time 00:00:00.00
> Running: Storage Assessment '-drive c -ran -read'
> Run Time 00:00:01.31
> Running: Storage Assessment '-drive c -seq -read'
> Run Time 00:00:05.36
> Running: Storage Assessment '-drive c -seq -write'
> Run Time 00:00:03.17
> Running: Storage Assessment '-drive c -flush -seq'
> Run Time 00:00:00.80
> Running: Storage Assessment '-drive c -flush -ran'
> Run Time 00:00:00.73
> Dshow Video Encode Time                      0.00000 s
> Dshow Video Decode Time                      0.00000 s
> Media Foundation Decode Time                 0.00000 s
> Disk  Random 16.0 Read                       541.88 MB/s       8.3
> Disk  Sequential 64.0 Read                   1523.74 MB/s      8.8
> Disk  Sequential 64.0 Write                  805.49 MB/s       8.3
> Average Read Time with Sequential Writes     0.219 ms          8.6
> Latency: 95th Percentile                     1.178 ms          8.2
> Latency: Maximum                             7.760 ms          8.2
> Average Read Time with Random Writes         0.199 ms          8.9
Run Code Online (Sandbox Code Playgroud)

这表明我的磁盘(一个相当不错的 SSD)有更多的能力。

如果我只是复制文件:

powershell -Command "Measure-Command {Copy-Item "C:/Dev/100MillionRows.csv" -Destination "C:/Dev/100MillionRowsCopy.csv"}"

需要9.97 seconds, 13.85 seconds, 10.90 seconds三个运行。取平均值11.57 seconds,我看到大约 860 MB/S 的 IO。这更接近于我磁盘的局限性。

显然,在我的代码中读取 CSV 时,我所做的工作比简单的副本要多,但我很惊讶它会比副本慢约 6 倍。

感谢您对为什么会出现这种情况以及如何提高我的 Rust 代码性能的任何想法?我对 Rust 很陌生,所以那里很可能有一些东西!我知道文档https://docs.rs/csv/1.0.0/csv/tutorial/index.html#performance的性能部分,但这些似乎是 50% 的性能改进,而不是百分之几百。

更新 1

在不修改代码的情况下,一些进一步的测试表明速率不一致,因为我改变了 1 亿行行中字符串的大小:

A,B : 18 MB/秒

SomeLongStringForTesting1, SomeLongStringForTesting2 : 142 MB/秒

AAAA...(A repeated 300 times),BBBB...(B repeated 300 times): 279 MB/秒

我将尝试实施记录在案的改进,看看它有什么不同,也可能尝试分析 - 任何关于工具的建议都值得赞赏,否则我只会有谷歌。

Bur*_*hi5 5

通过遵循您链接的教程中的性能提示,您可以获得相当大的改进。特别是,关键是摊销分配并避免 UTF-8 检查,这两种情况都发生在您的代码中。也就是说,您的代码在内存中为 CSV 文件中的每一行分配一条新记录。它还检查每个字段是否有效 UTF-8。这两者都有成本,但它们确实提供了一个相当简单的 API,而且速度相当快。

此外,本教程中未提及的一个技巧是csv::Writer::write_byte_record尽可能使用csv::Writer::write_record. 后者更灵活,但前者对输入的约束更多一些,以便它可以在常见场景中更有效地实现写入。

总的来说,进行这些更改非常容易:

use std::error::Error;

fn main() {
    read_and_write("rows.csv", "rows-copy.csv").unwrap();
}

fn read_and_write(
    in_file_path: &str,
    out_file_path: &str,
) -> Result<(), Box<dyn Error>> {
    let mut rdr = csv::ReaderBuilder::new()
        .has_headers(false)
        .from_path(in_file_path)?;
    let mut wtr = csv::WriterBuilder::new()
        .from_path(out_file_path)?;

    let mut record = csv::ByteRecord::new();
    while rdr.read_byte_record(&mut record)? {
        wtr.write_byte_record(&record)?;
    }
    wtr.flush()?;

    Ok(())
}
Run Code Online (Sandbox Code Playgroud)

这是您在我的 Linux 系统上运行代码的时间:

$ time ./target/release/csvsoperf

real    21.518
user    19.315
sys     2.189
maxmem  6 MB
faults  0
Run Code Online (Sandbox Code Playgroud)

这是我更新代码的时间:

$ time ./target/release/csvsoperf

real    12.057
user    9.924
sys     2.125
maxmem  6 MB
faults  0
Run Code Online (Sandbox Code Playgroud)

分析更快的代码,大约 56% 的时间csv::Reader::read_byte_record花在csv::Writer::write_byte_record. 这对我来说似乎是正确的,并且表明您的程序并没有真正做任何其他次优的事情。除了csv自身之外,没有真正的瓶颈需要优化。

显然,在我的代码中读取 CSV 时,我所做的工作比简单的副本要多,但我很惊讶它会比副本慢约 6 倍。

使用病态或非常受限的输入时很容易感到惊讶。您的示例 CSV 数据非常简单,实际上,如果遵循该格式,则有(显然)更快的方法来解析和写入数据。但是 CSV 解析器不知道这一点,并且必须能够处理完整格式,包括处理转义和引用。该csv解析器有很多做IT优化工作,一般应在存在较快的CSV解析器之一。所以这里更合适的比较应该是另一个CSV解析器。数据的哑副本将比对输入进行非平凡工作的解析器快得多也就不足为奇了。