csv - Rust:读写 CSV 性能

标签 csv rust

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

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

SomeLongStringForTesting1, SomeLongStringForTesting2

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

我已经编写(主要是复制!)以下使用 csv: 1.1.3 crate 的代码:

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(())
}

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

powershell -Command "Measure-Command {.\target\release\csv-performance.exe}" 三个运行产生 72.79 秒、71.01 秒、70.77 秒 .

大约来说,我在 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

这表明我的磁盘(相当不错的 SSD)的功能更多。

如果我只是复制文件:

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

三次运行需要 9.97 秒、13.85 秒、10.90 秒。取 11.57 秒 的平均值,我看到大约 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/S

SomeLongStringForTesting1, SomeLongStringForTesting2:142 MB/S

AAAA...(A 重复 300 次),BBBB...(B 重复 300 次):279 MB/S

我将着手实现记录在案的改进,看看它有什么不同,并且可能还会尝试分析 - 任何有关工具的建议都会受到赞赏,否则我将只使用 Google。

最佳答案

按照您链接的教程中的性能提示,您可以获得相当大的改进。特别是,关键实际上是摊销分配并避免 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(())
}

这是你的代码在我的 Linux 系统上的时间:

$ time ./target/release/csvsoperf

real    21.518
user    19.315
sys     2.189
maxmem  6 MB
faults  0

这是我更新代码的时间:

$ time ./target/release/csvsoperf

real    12.057
user    9.924
sys     2.125
maxmem  6 MB
faults  0

分析更快的代码,大约 56% 的时间花在 csv::Reader::read_byte_record 上,而大约 29% 的时间花在 csv::作家::write_byte_record。这对我来说似乎是正确的,并且表明您的程序实际上并没有做任何次优的事情。在 csv 本身之外没有真正的瓶颈需要优化。

Clearly when reading the CSV in my code I'm doing more work than a simple copy, but I was surprised that it would be ~6 times slower than the copy.

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

关于csv - Rust:读写 CSV 性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62116183/

相关文章:

rust - 为什么不能在结构定义中省略生命周期?

PHP fgetcsv 和自定义行结束符

ruby - 使用 Ruby CSV 提取一列

php - 使用 php 将 csv 上传到 MySQL 并更新

ios - 将 NSArray 写入计算机上的文件一次

rust - 如何在Substrate中具有不可变的键值映射?

javascript - 如何遍历 javascript 行分隔字符串,检查分号分隔的第一个值,并连接值相等的行?

pattern-matching - 相似但不同的匹配子句

rust - 多个底层节点,但每个节点只有一个对等体

rust - 当特征用作类型但不用作 where 子句中的绑定(bind)时出错