什么是读取CSV文件的内存有效方式?
我的程序使用csvcrate into读取 CSV 文件Vec<Vec<String>>,其中外部向量表示行,内部向量将行分成列。
use std::{time, thread::{sleep, park}};
use csv;
fn main() {
different_scope();
println!("Parked");
park();
}
fn different_scope() {
println!("Reading csv");
let _data = read_csv("data.csv");
println!("Sleeping");
sleep(time::Duration::from_secs(4));
println!("Going out of scope");
}
fn read_csv(path: &str) -> Vec<Vec<String>> {
let mut rdr = csv::Reader::from_path(path).unwrap();
return rdr
.records()
.map(|row| {
row
.unwrap()
.iter()
.map(|column| column.to_string())
.collect()
})
.collect();
}
我正在查看 RAM 使用情况,htop这使用 2.5GB 内存来读取 250MB CSV 文件。
以下是内容 cat /proc/<my pid>/status
Name: (name)
Umask: 0002
State: S (sleeping)
Tgid: 18349
Ngid: 0
Pid: 18349
PPid: 18311
TracerPid: 0
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
FDSize: 256
Groups: 4 24 27 30 46 118 128 133 1000
NStgid: 18349
NSpid: 18349
NSpgid: 18349
NSsid: 18311
VmPeak: 2748152 kB
VmSize: 2354932 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 2580156 kB
VmRSS: 2345944 kB
RssAnon: 2343900 kB
RssFile: 2044 kB
RssShmem: 0 kB
VmData: 2343884 kB
VmStk: 136 kB
VmExe: 304 kB
VmLib: 2332 kB
VmPTE: 4648 kB
VmSwap: 0 kB
HugetlbPages: 0 kB
CoreDumping: 0
THP_enabled: 1
Threads: 1
SigQ: 0/127783
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000001000
SigCgt: 0000000180000440
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
NoNewPrivs: 0
Seccomp: 0
Speculation_Store_Bypass: thread vulnerable
Cpus_allowed: ffffffff
Cpus_allowed_list: 0-31
Mems_allowed: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
Mems_allowed_list: 0
voluntary_ctxt_switches: 9
nonvoluntary_ctxt_switches: 293
当我删除变量时,它释放了正确的数量(大约 250MB),但仍然剩下 2.2GB。在我的所有内存都被使用并且进程被终止(cargo打印“Killed”)之前,我无法读取超过 2-3GB 的数据。
如何释放多余的内存,同时该CSV正在读?
我需要处理每一行,但在这种情况下,我不需要一次保存所有这些数据,但如果我这样做了怎么办?
我问了一个相关的问题,有人指出什么是 Rust 策略来取消提交并将内存返回给操作系统?这有助于理解问题,但我不知道如何解决。
我的理解是我应该将我的 crate 切换到不同的内存分配器,但是强制执行我能找到的所有分配器感觉像是一种无知的方法。
回答
对于有关内存的问题,最好开发一种技术来量化内存使用情况。您可以通过检查您的表示来做到这一点。在这种情况下,就是Vec<Vec<String>>. 特别是,如果您有一个 250MB 的 CSV 文件,它表示为一系列字段的序列,那么您不一定只使用 250MB 的内存。您需要考虑表示的开销。
对于 a Vec<Vec<String>>,我们可以忽略外部的开销,Vec<...>因为它(在您的程序中)会在堆栈上而不是堆上。它Vec<String>在堆上的内部。
所以,如果您的CSV文件M记录,每个记录N的字段,那么就会出现M的情况Vec<String>和M * N实例String。aVec<T>和 a的开销String都是3 * sizeof(word),一个字是指向数据的指针,另一个字是长度,另一个是容量。(对于 64 位目标,这是 24 个字节。)因此,64 位目标的总开销为(M * 24) + (M * N * 24).
让我们用实验来测试一下。由于你没有分享你的 CSV 输入(你真的应该在未来分享),我会带上我自己的。这是145MB,具有M=3,173,958与记录N=7每条记录的字段。所以你的表示的总开销是(3173958 * 24) + (3173958 * 7 * 24) = 609,399,936字节,或 609 MB。让我们用一个真实的程序来测试一下:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let input_path = match std::env::args_os().nth(1) {
Some(p) => p,
None => {
eprintln!("Usage: csvmem <path>");
std::process::exit(1);
}
};
let rdr = csv::Reader::from_path(input_path)?;
let mut records: Vec<Vec<String>> = vec![];
for result in rdr.into_records() {
let mut record: Vec<String> = vec![];
for column in result?.iter() {
record.push(column.to_string());
}
records.push(record);
}
println!("{}", records.len());
Ok(())
}
(我在几个地方添加了一些不必要的类型注释以使代码更清晰一些,特别是在我们的表示方面。)所以让我们运行这个程序(它唯一的依赖是csv = "1"在 my 中Cargo.toml):
$ echo $TIMEFMT
real %*E user %*U sys %*S maxmem %M MB faults %F
$ cargo b --release
$ time ./target/release/csvmem /m/sets/csv/pop/worldcitiespop-nice.csv
3173958
real 1.542
user 1.236
sys 0.296
maxmem 1287 MB
faults 0
在time此实用程序报告的峰值内存使用情况,这实际上是比我们可能希望它是高一点:609 + 145 = 754MB。我不太了解分配器,无法完全推断出差异。可能是我使用的系统分配器分配了比实际需要更大的块。让我们通过使用 aBox<str>而不是 来使我们的表示更有效率String。我们牺牲了扩展字符串的能力,但作为交换,我们为每个字段节省了 8 个字节的开销。所以我们新的开销计算是(3173958 * 24) + (3173958 * 7 * 16) = 431,658,288字节或 431MB 的差异609 - 431 = 178MB。所以让我们测试我们的新表示,看看我们的 delta 是多少:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let input_path = match std::env::args_os().nth(1) {
Some(p) => p,
None => {
eprintln!("Usage: csvmem <path>");
std::process::exit(1);
}
};
let rdr = csv::Reader::from_path(input_path)?;
let mut records: Vec<Vec<Box<str>>> = vec![];
for result in rdr.into_records() {
let mut record: Vec<Box<str>> = vec![];
for column in result?.iter() {
record.push(column.to_string().into());
}
records.push(record);
}
println!("{}", records.len());
Ok(())
}
并编译和运行:
$ cargo b --release
$ time ./target/release/csvmem /m/sets/csv/pop/worldcitiespop-nice.csv
3173958
real 1.459
user 1.183
sys 0.266
maxmem 1093 MB
faults 0
总增量为 194MB。这与我们的猜测非常接近。
我们可以使用 a 进一步优化表示Vec<Box<[Box<str>]>>:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let input_path = match std::env::args_os().nth(1) {
Some(p) => p,
None => {
eprintln!("Usage: csvmem <path>");
std::process::exit(1);
}
};
let rdr = csv::Reader::from_path(input_path)?;
let mut records: Vec<Box<[Box<str>]>> = vec![];
for result in rdr.into_records() {
let mut record: Vec<Box<str>> = vec![];
for column in result?.iter() {
record.push(column.to_string().into());
}
records.push(record.into());
}
println!("{}", records.len());
Ok(())
}
这给出了 1069 MB 的峰值内存使用量。所以节省的不多。
但是,我们能做的最好的事情是使用csv::StringRecord:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let input_path = match std::env::args_os().nth(1) {
Some(p) => p,
None => {
eprintln!("Usage: csvmem <path>");
std::process::exit(1);
}
};
let rdr = csv::Reader::from_path(input_path)?;
let mut records = vec![];
for result in rdr.into_records() {
let record = result?;
records.push(record);
}
println!("{}", records.len());
Ok(())
}
这给出了 727MB 的峰值内存使用量。秘诀在于 aStringRecord存储内联字段而没有第二层间接。它最终节省了很多!
当然,如果您不需要一次将所有记录存储在内存中,那么您不应该这样做。CSV 板条箱支持这一点就好了:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let input_path = match std::env::args_os().nth(1) {
Some(p) => p,
None => {
eprintln!("Usage: csvmem <path>");
std::process::exit(1);
}
};
let mut count = 0;
let rdr = csv::Reader::from_path(input_path)?;
for result in rdr.into_records() {
let _ = result?;
count += 1;
}
println!("{}", count);
Ok(())
}
并且该程序的峰值内存使用量仅为 9MB,正如您对流式实现所期望的那样。(从技术上讲,如果您下拉并使用csv-corecrate ,则根本可以不使用堆内存。)