最近在B站冲浪时发现一个 Rust 和 Go 解析 tsv 文件的视频, 作者需要解析使用get-NetTCPConnection | Format-Table -Property LocalAddress,LocalPort,RemoteAddress,RemotePort,State,OwningProcess获取的本地所有 TCP 连接信息, 文件输出大致如下
LocalAddressLocalPortRemoteAddressRemotePortStateOwningProcess -------------------------------------------------------------- 192.168.1.454339104.210.1.98443Established4504
视频作者使用 regex 正则库处理输出, 发现比 Go 版本慢, 优化后虽然比 Go 快, 但并没有领先多少, 于是我自己尝试使用别的优化方法, 解析耗时能优化使用正则解析的 10% 左右. 下面来看看我的优化过程.
•更快的 tsv 解析[1]
•项目搭建[2]
•regex 解析[3]
•减少内存分配[4]
•使用 ascii 正则[5]
•抛弃 regex[6]
•手写解析状态机[7]
•SIMD 加速?[8]
•总结[9]
项目搭建
进行性能时建议使用criterion[10], 它帮我们解决了性能的内存预加载, 操作耗时, 性能记录, 图表输出等功能.
cargonew--libtsv cdtsv cargoaddcriterion--dev-Fhtml_reports cargoaddregex
然后在 Cargo.toml 里添加如下bench 文件
[[bench]] name="parse" harness=false
//benches/parse.rs #![allow(dead_code)] usecriterion::{black_box,criterion_group,criterion_main,Criterion}; constOUTPUT:&str=include_str!("net.tsv"); fncriterion_benchmark(c:&mutCriterion){ todo!() } criterion_group!(benches,criterion_benchmark); criterion_main!(benches);
测试使用的 tsv 一共 380 行.
regex 解析
使用正则解析的正则表达式很简单, 这里直接给代码, 为了避免重复编译正则表达式和重新分配内存报错结果列表, 这里将她们作为参数传给解析函数.
structOwnedRecord{ local_addr:String, local_port:u16, remote_addr:String, remote_port:u16, state:String, pid:u64, } fnregex_owned(input:&str,re:®ex::Regex,result:&mutVec){ input.lines().for_each(|line|{ ifletSome(item)=re.captures(line).and_then(|captures|{ let(_,[local_addr,local_port,remote_addr,remote_port,state,pid])= captures.extract(); letret=OwnedRecord{ local_addr:local_addr.to_string(), local_port:local_port.parse().ok()?, remote_addr:remote_addr.to_string(), remote_port:remote_port.parse().ok()?, state:state.to_string(), pid:pid.parse().ok()?, }; Some(ret) }){ result.push(item); } }); assert_eq!(result.len(),377); }
parse.rs 文件里要加上使用的正则和提前创建好列表, 并且将函数添加的 bench 目标里
fncriterion_benchmark(c:&mutCriterion){ letre=regex::new(r"(S+)s+(d+)s+(S+)s+(d+)s+(S+)s+(d+)").unwrap(); letmutr1=Vec::with_capacity(400); c.bench_function("regex_owned",|b|{ b.iter(||{ //重置输出vector r1.clear(); regex_owned(black_box(OUTPUT),&re,&mutr1); }) }); }
接着跑cargo bench --bench parse进行测试, 在我的电脑上测得每次运行耗时 450 µs 左右.
减少内存分配
一个最简单的优化是使用&str以减少每次创建String带来的内存分配和数据复制.
structRecord<'a>{ local_addr:&'astr, local_port:u16, remote_addr:&'astr, remote_port:u16, state:&'astr, pid:u64, }
两个函数代码差不多, 所以这里不再列出来, 可以通过gits: tsv 解析[11]获取完整代码.
可惜这次改动带来的优化非常小, 在我的电脑上反复测量, 这个版本耗时在 440 µs 左右.
使用 ascii 正则
rust 的 regex 正则默认使用 unicode, 相比于 ascii 编码, unicode 更复杂, 因此性能也相对较低, 刚好要解析的内容都是ascii字符, 使用 ascii 正则是否能提升解析速度呢? regex 有regex::bytes模块用于 ascii 解析, 但为了适配字段, 这里不得不使用transmute将&[u8]强制转换成&str
fncast(data:&[u8])->&str{ unsafe{std::transmute(data)} } fnregex_ascii<'a>(input:&'astr,re:®ex::Regex,result:&mutVec>){ input.lines().for_each(|line|{ ifletSome(item)=re.captures(line.as_bytes()).and_then(|captures|{ let(_,[local_addr,local_port,remote_addr,remote_port,state,pid])= captures.extract(); letret=Record{ local_addr:cast(local_addr), local_port:cast(local_port).parse().ok()?, remote_addr:cast(remote_addr), remote_port:cast(remote_port).parse().ok()?, state:cast(state), pid:cast(pid).parse().ok()?, }; Some(ret) }){ result.push(item); } }); assert_eq!(result.len(),377); }
添加到 bench 后性能大概多少呢?, 很遗憾, 性能与 regex_borrow 差不多, 在 430 µs 左右.
抛弃 regex
鉴于内容格式比较简单, 如果只使用 rust 内置的 split 等方法解析性能会不会更好呢? 解析思路很简单, 使用lines得到一个逐行迭代器, 然后对每行使用 split 切分空格再逐个解析即可
fnsplit<'a>(input:&'astr,result:&mutVec>){ input .lines() .filter_map(|line|{ letmutiter=line.split(['',' ',' ']).filter(|c|!c.is_empty()); letlocal_addr=iter.next()?; letlocal_port:u16=iter.next()?.parse().ok()?; letremote_addr=iter.next()?; letremote_port:u16=iter.next()?.parse().ok()?; letstate=iter.next()?; letpid:u64=iter.next()?.parse().ok()?; Some(Record{ local_addr, local_port, remote_addr, remote_port, state, pid, }) }) .for_each(|item|result.push(item)); assert_eq!(result.len(),377); }
注意line.split只后还需要过滤不是空白的字符串, 这是因为字符串"a b"split 之后得到["a", "", "b"].
经测试, 这个版本测试耗时大概为 53 µs, 这真是一个巨大提升, rust 的 regex 性能确实有些问题.
每次 split 之后还需要 filter 感觉有些拖沓, 刚好有个split_whitespace[12], 换用这个方法, 将新的解析方法命名为split_whitespace后再测试下性能
letmutiter=line.split_whitespace();
令人意想不到的是性能居然倒退了, 这次耗时大概 60 µs, 仔细研究下来还是 unicode 的问题, 改用 ascii 版本的split_ascii_whitespace之后性能提升到 45 µs.
手写解析状态机
除了上述的方法, 我还尝试将 Record 的 local_addr 和 remote_addr 改成std::IpAddr, 消除next()?.parse().ok()?等其他方法, 但收益几乎没有, 唯一有作用的办法是手写解析状态机.
大致思路是, 对于输出来说, 我们只关系它是以下三种情况
1.换行符 NL
2.除了换行符的空白符 WS
3.非空白字符 CH
只解析 LocalAddr 和 LocalPort 解析状态机如下, 如果要解析更多字段, 按顺序添加即可.
因为代码有些复杂, 所以这里不再贴出来, 完整代码在 gits 上. 手写状态机的版本耗时大概在 32 µs 左右. 这版本主要性能提升来自手写状态机减少了循环内的分支判断.
SIMD 加速?
在上面手写解析的例子里, 处理过程类似与将输出作为一个 vec, 状态机作为另一个 vec, 将两个 vec 进行某种运算后输出结果, 应该能使用 simd 进行加速, 但我还没想出高效实现. 所以这里只给出可能的参考资料
1.zsv[13]使用 simd 加速的 csv 解析库
2.simd base64[14]一篇介绍使用 simd 加速 base64 解析的博客, 非常推荐
总结
rust regex 在某时候确实存在性能问题, 有时候使用简单的 split 的方法手动解析反而更简单性能也更高, 如果情况允许, 使用 ascii 版本能进一步提升性能, 如果你追求更好的性能, 手写一个状态不失为一种选择, 当然我不建议在生产上这么做. 同时我也期待有 simd 加速的例子.
审核编辑:黄飞
-
TCP
+关注
关注
8文章
1353浏览量
79059 -
函数
+关注
关注
3文章
4329浏览量
62578 -
内存分配
+关注
关注
0文章
16浏览量
8301
原文标题:更快的 tsv 解析
文章出处:【微信号:Rust语言中文社区,微信公众号:Rust语言中文社区】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论