【译】无缓冲 I/O 会让你的 Rust 程序变慢

2023-11-09

  • 文章标题:【译】无缓冲 I/O 会让你的 Rust 程序变慢(Unbuffered I/O Can Make Your Rust Programs Much Slower 译文)
  • 原文链接 https://era.co/blog/unbuffered-io-slows-rust-programs
  • 译文来自:https://github.com/suhanyujie/article-transfer-rs/
  • 译者:suhanyujie
  • ps:水平有限,如有不当之处,还请指正。
  • 标签:Rust,无缓冲 I/O

在这篇文章中,我们将看看 Rust 代码中性能糟糕的常见原因,即使是经验丰富的开发人员也会遇到这种情况,以及当这种情况发生在你的程序中时该如何优化。

Rust 作为一种能够让开发人员编写快速和安全代码的语言,这种特性已经确立。每天,像Mozilla、微软、Dropbox 和亚马逊(仅举几例)这样的大型组织都会使用 Rust 为他们的客户提供一流的性能服务,同时避免了许多用 C 或 C++ 编写的程序的安全问题,这些语言在传统上更适用于高性能工作。

在 Era 软件公司,我们重视性能,我们相信通过制造高效的产品,可以帮助我们的客户从他们的数据中获得更多,同时降低他们每月的基础设施成本。性能是 Rust 成为我们的首选语言的一个主要原因。然而,仅仅用 Rust 编写代码并不能保证高性能。Rust 很好,但它不是魔术。它是一种工具,和任何工具一样,我们必须有效地使用它来获得最佳的结果。

在这篇文章中,我们将看看 Rust 代码中一个常见的低性能的原因,它甚至会阻碍资深的开发者。即,默认情况下,文件的读写是没有缓冲的。

Syscalls

程序不能直接读取或写入磁盘上的文件,而是通过系统调用(syscall)来请求操作系统协助实现。例如,在 Linux 中,内核提供了 write() 系统调用,用于将数据从一个程序传输到一个文件。我们的程序可以通过调用带有三个参数的 write() 来向文件写入数据:一个参数是文件描述符,一个是指向我们想要写入的字节的指针,以及要写入的字节数。

Linux 系统调用的一个特点是,它们的调用速度要比普通函数慢。这是因为它们的执行必须从用户模式下切换到内核模式,而这种切换是有代价的。为了确保良好的性能,我们的程序应该避免进行过多的系统调用。

I/O with and without buffering

下面是一个简单的 Rust 程序,它向一个文件写了几行字。这个程序没有使用缓冲区,这意味着三次调用 f.write() 方法会产生一次 write() 系统调用。

use std::fs;
use std::io::{self, Write};

fn main() -> io::Result<()> {
    let mut f = fs::File::create("/tmp/unbuffered.txt")?;
    f.write(b"foo")?;
    f.write(b"\n")?;
    f.write(b"bar\nbaz\n")?;
    return Ok(());
}

在 strace 中运行这个程序,我们可以看到确实有三个 write() 系统调用。

$ strace --trace=write ./target/release/01_unbuffered
write(3, "foo", 3)                      = 3
write(3, "\n", 1)                       = 1
write(3, "bar\nbaz\n", 8)               = 8

在这个小例子中,对性能的影响是微乎其微的,但如果在一个处理大数据文件的真实程序中,则有数百万甚至数十亿的无意义的系统调用,会导致程序变慢,且让用户失望。

幸运的是,我们可以改进我们的程序。在我们打开文件后,我们可以把它包装在一个 BufWriter 对象里面。

use std::fs;
use std::io::{self, BufWriter, Write};

fn main() -> io::Result<()> {
    let mut f = BufWriter::new(fs::File::create("x.txt")?);
    f.write(b"foo")?;
    f.write(b"\n")?;
    f.write(b"bar\nbaz\n")?;
    return Ok(());
}

现在,当我们调用 f.write() 时,我们实际上并没有执行 write() 系统调用,我们只是将字节追加到缓冲包装器内的数组中。这完全是在用户模式下发生的,所以它开销很低。只有当缓冲区满了,或者当我们关闭文件时,才会进行系统调用,将字节传送到磁盘。我们可以用 strace 来确认这个过程。

$ strace --trace=write ./target/release/02_buffered
write(3, "foo\nbar\nbaz\n", 12)         = 12

缓冲区的作用其实就是摊销。我们必须使用系统调用将数据写入磁盘,并付出调用它们的开销;然而,我们可以聪明一点,发出更少的系统调用,一次传递更多的数据。

Deserialization and buffering

当不使用缓冲时,问题很容易在 Rust 程序中显现出来。以 serde_json 为例,它是一个以简单易用的接口来读写 JSON 数据的库。它的 from_reader() 函数接受任何实现 Read trait 的对象,并将字节解码成 JSON 树。File 类型实现了 Read trait,所以我们可以非常容易地解码磁盘上的文件。下面是一个简单的程序,它的 File 故意没有被 BufReader 对象所包裹。

use std::fs;
use std::io;

fn main() -> io::Result<()> {
    let mut f = fs::File::open("sample.json")?;
    let v: serde_json::Value = serde_json::from_reader(&mut f).unwrap();
    println!("{}", v.is_object());
    return Ok(());
}

我们可以用 perf 来计算在这个程序在执行过程中进行了多少次 read()系统调用。

$ sudo perf stat -e syscalls:sys_enter_read ./target/release/04_unbuffered_json
 Performance counter stats for './target/release/04_unbuffered_json':
         2,009,119      syscalls:sys_enter_read

sample.json 文件有 2,009,108 字节。为了反序列化该文件,serde_json 对每个字节进行了一次系统调用!(额外的 11 次 read() 系统调用发生在程序开始时,用于加载 libc)我们可靠的 strace 证实了这一点。

$ strace --trace=read ./target/release/04_unbuffered_json
...
read(3, "{", 1)                         = 1
read(3, "\"", 1)                        = 1
read(3, "t", 1)                         = 1
read(3, "y", 1)                         = 1
read(3, "p", 1)                         = 1
read(3, "e", 1)                         = 1
read(3, "\"", 1)                        = 1
read(3, ":", 1)                         = 1
...

当我们通过将 file 包裹在一“个缓冲器”内来修复这个程序时,结果令人吃惊。我们将系统调用减少了近 8000 次 – 这很有意义,因为我们每次读取 8192 个字节,而非一个 – 这使得程序运行速度提高了 11 倍。

$ sudo perf stat -e syscalls:sys_enter_read ./target/release/05_buffered_json
 Performance counter stats for './target/release/05_buffered_json':
               257      syscalls:sys_enter_read


$ strace --trace=read ./target/release/05_buffered_json
...
read(3, "{\"type\":\"FeatureCollection\",\"crs"..., 8192) = 8192
read(3, "6200000000001}},{\"type\":\"Feature"..., 8192) = 8192
read(3, "egion\":\"AK\",\"category\":\"In-betwe"..., 8192) = 8192
read(3, "01}},{\"type\":\"Feature\",\"id\":95,\""..., 8192) = 8192
...

$ hyperfine -w 5 -m 30 \
    ./target/release/04_unbuffered_json \
    ./target/release/05_buffered_json
Benchmark #1: ./target/release/04_unbuffered_json
  Time (mean ± σ):     326.3 ms ±   8.1 ms    [User: 70.2 ms, System: 256.0 ms]
  Range (min … max):   312.2 ms … 346.8 ms    30 runs

Benchmark #2: ./target/release/05_buffered_json
  Time (mean ± σ):      28.5 ms ±   1.4 ms    [User: 22.9 ms, System: 5.6 ms]
  Range (min … max):    26.2 ms …  33.2 ms    106 runs

Summary
  './target/release/05_buffered_json' ran
   11.43 ± 0.63 times faster than './target/release/04_unbuffered_json'

我们最近在 Era 软件公司的一个产品中发现了这个问题。我们在排查一个无关的问题时,发现一个 600 兆字节的文件需要 30 多秒才能被反序列化。因为一般序列化文件只需要一秒钟,这显然是有问题的。我们进行了排查,并很快发现了我们在这篇文章中所讨论的问题 – 我们打开了文件并反序列化了它,但没有将文件包裹在 BufReader 中。我们调整了代码,正如我们在这篇文章中所展示的那样,通过优化将反序列化的时间缩短到了一秒钟。

我想强调的是,尽管最初的代码是由一位高级开发人员编写的,并由多位了解这些东西的高级开发人员进行了 review,但这个性能错误仍然逃脱了他们的警惕,摇身一变合到了我们的主分支。正如我在介绍中所说,即使是老手也很容易错过这个问题!

如何发现这类问题

好了,我们已经知道无缓冲 I/O 的危害,但我们也知道它也容易被有经验的程序员的忽视,那么我们能做什么呢?目前,Rust 没有自动提示你这些问题的方法 – 编译器不会发出警告,Clippy 也没有提示你 I/O 是未缓冲的 lint。

然而,我们看到 strace 工具很有用 – 它告诉我们 read()write() 系统调用只处理了一个字节。因此,让我们看看如何使用 strace(和一些 awk 的帮助信息)来(1)排查我们是否有大量的单字节读或写,以及(2)使用 strace 的堆栈跟踪功能来看看程序中的单字节读或写发生在哪里。

我们继续使用上一节中的无缓冲程序读取一个 JSON 文件为例。为了使输出更加可读,我们将使用这个小小的 JSON 装载数据:{"id":42}。在 strace 中运行该程序! 并使用 --trace 选项只保留对 read() 的调用,并使用 awk 截取展示读取一个字节的系统调用(即以 =1 结尾的行)。strace 选项 --decode-fds=path 告诉我们描述符(3)指的是哪个文件(在本例中是 /tmp/simple.json),这对了解程序的哪个部分有问题有帮助。

$ strace --decode-fds=path \
         --trace=read \
         /tmp/04_unbuffered_json 2>&1 |
    awk '/= 1$/'
read(3</tmp/simple.json>, "{", 1)       = 1
read(3</tmp/simple.json>, "\"", 1)      = 1
read(3</tmp/simple.json>, "i", 1)       = 1
read(3</tmp/simple.json>, "d", 1)       = 1
read(3</tmp/simple.json>, "\"", 1)      = 1
read(3</tmp/simple.json>, ":", 1)       = 1
read(3</tmp/simple.json>, "4", 1)       = 1
read(3</tmp/simple.json>, "2", 1)       = 1
read(3</tmp/simple.json>, "}", 1)       = 1
read(3</tmp/simple.json>, "\n", 1)      = 1

我们可以从所有以 =1 结尾的行中看到,程序确实在进行单字节的读取,而且数据是 JSON 内容的一部分。所以我们已经成功地用 strace 验证了问题(1)!

现在,我们如何找到这些读的地方呢?幸运的是,strace 有一个非常有用的标志 --stack-traces,可以显示进行系统调用的代码。现在我们调整 awk 程序执行,以显示与单字节读取相关的堆栈跟踪。下面是对其工作原理的解释。

  • 如果当前行以 =1 结束,我们将变量 show 设为 1(true)。
  • 如果当前行以等号结尾,后面不是数字 1(也就是说,一个读数返回多个字节),我们将变量 show 设为 0(false)。
  • show == 1 时,我们显示当前行。

下面是读取文件中尖括号的系统调用输出。

$ strace --stack-traces \
         --decode-fds=path \
         --trace=read \
         /tmp/04_unbuffered_json 2>&1 |
    awk '/= 1$/ { show = 1 } /= [^1]$/ { show = 0 } show'
read(3</tmp/simple.json>, "{", 1)       = 1
 > /usr/lib/x86_64-linux-gnu/libpthread-2.33.so(read+0x12) [0x13152]
 > /tmp/04_unbuffered_json(<std::fs::File as std::io::Read>::read+0x23) [0x1fbd3]
 > /tmp/04_unbuffered_json(<std::io::Bytes<R> as core::iter::traits::iterator::Iterator>::next+0x33) [0xca53]
 > /tmp/04_unbuffered_json(_ZN10serde_json5value2de77_$LT$impl$u20$serde..de..Deserialize$u20$for$u20$serde_json..value..Value$GT$11deserialize17h23bf1ff9e8286bd9E.llvm.16551263557485243796+0x8ec) [0xb39c]
 > /tmp/04_unbuffered_json(serde_json::de::from_reader+0x48) [0x9fb8]
 > /tmp/04_unbuffered_json(_04_unbuffered_json::main+0x78) [0xc7d8]
 > /tmp/04_unbuffered_json(std::sys_common::backtrace::__rust_begin_short_backtrace+0x3) [0xc583]
 > /tmp/04_unbuffered_json(_ZN3std2rt10lang_start28_$u7b$$u7b$closure$u7d$$u7d$17h3d55d3d7814f859cE.llvm.14713913286507758235+0x9) [0xc929]
 > /tmp/04_unbuffered_json(std::rt::lang_start_internal+0x30a) [0x2650a]
 > /tmp/04_unbuffered_json(main+0x22) [0xc912]
 > /usr/lib/x86_64-linux-gnu/libc-2.33.so(__libc_start_main+0xd5) [0x28565]
 > /tmp/04_unbuffered_json(_start+0x2e) [0x8e3e]

我们看到 read() 系统调用是在我们在 main() 函数中反序列化 JSON(serde_json::de::from_reader)时调用的。有了文件名和函数,这应该能缩小我们的搜索范围,帮助我们找到需要添加缓冲区的地方。这样就解决了问题(2)!

结语

在这篇文章中,我们可以知道:

  • Linux 中的系统调用比普通函数要慢
  • 触发过多的系统调用会对运行时性能产生非常不利的影响
  • 使用 BufReader 和 BufWriter,我们可以降低系统调用的成本。
  • 即使是有经验的程序员也可能忽略这些问题
  • 我们可以使用 strace 和 awk 来查找程序中是否有未缓冲的 I/O 发生,以及在哪儿发生。

最后我想说,如果这种类型的问题发生在你自己的程序中,不要为此感到难过 – 我们都只是人类,我们无法避免犯错。而当错误发生时,要把它们发挥到极致。用它们作为例子来提醒你团队中的开发者,Rust 中的文件默认是没有缓冲的,这与他们可能熟悉的语言不同,比如 Python。如果你的团队中有初级开发人员,请从你的日程安排中抽出一些时间来帮助他们理解这个问题。帮助人们获得一种“spidey sense”,当他们看到 File 这个词而旁边没有 BufReader 或 BufWriter 这两个词时,就会感到刺痛。这虽不一定能防止这类问题的再次发生,但会有更多的人注意到。

在 Era,我们努力使我们的文化对技术友好以及增强技术上的创新。我们正在组建一个专家团队,在分布式系统、机器学习和数据库工程方面进行创新。如果你对我们的工作感兴趣,请查看我们的公司和工作相关页面

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【译】无缓冲 I/O 会让你的 Rust 程序变慢 的相关文章

随机推荐

  • vue中下载文件导出保存到本地

    vue中下载文件导出保存到本地 先分析如何下载 先有一个链接地址 然后使用 location href或window open 下载到本地 看看返回数据 res config url 中是下载链接地址 res data 中是返回的二进制数据
  • 结构体指针变量使用方法举例

    include
  • 报表设计

    最近在做任务报表方面的工作 之前一直以为查询和报表是一样东西 虽说 报表是查询的一种展示模式 但是做分析时 还是应该将以区别 报表 多样的格式 动态的数据 报表是我们想要数据展示的一种形态 就像是各种图表 例如 你想查询小明的名字 查询可以
  • 使用Nginx实现多个网站代理[多端口监听][django][资源服务器]

    使用Nginx实现多个网站代理 导航 原文链接 使用Nginx实现多个网站代理 导言 nginx是网站开发后期一个不可缺少的应用 nginx的作用是 请求代理 监听请求并转发给对应端口的进程处理 资源代理 使用nginx直接将服务器资源共享
  • IntelliJ IDEA部署tomcat时出现No artifacts marked for deployment

    这种错误主要是因为没有设置导出包 解决方法 File gt Project Structure gt Artifacts 然后点击ok 然后记得apply ok 此时再回到问题所在 点击Fix即可 或者如下 启动 打开浏览器复制下面网址 h
  • 数学建模--决策树的预测模型的Python实现

    目录 1 算法流程简介 2 算法核心代码 3 算法效果展示 1 算法流程简介 决策树的应用 对泰坦尼克号数据集成员进行预测生死 算法流程还是比较简单的 简单学习一下决策树跟着注释写即可 文章参考 https zhuanlan zhihu c
  • GROUP BY分组单个和多个字段不同条件所查询出来的结果不同

    第一次查询 代码如下 select from test 结果中 按照b列来分 则是 5个a 3个b 按照c列来分 则是 4个甲 4个乙 第二次 查询按照 b列来分组 代码如下 select count a b from test group
  • cordova-plugin-file-transfer cordova plugin下载不到问题解决记录

    环境 cordova 11 1 0 node 16 前提 安卓项目需要支持一些功能 升级到androidX所以一些插件需要更新 使用github方式可能会报ssh需要publicKey相关问题 不想用私人账号进行git github com
  • c++ uint32转为int_轻松实现C/C++各种常见进制相互转换

    这篇文章主要介绍了轻松实现C C 各种常见进制相互转换 文中通过示例代码介绍的非常详细 对大家的学习或者工作具有一定的参考学习价值 需要的朋友们下面随着小编来一起学习学习吧 adsbygoogle window adsbygoogle pu
  • 融云「百幄」之视频会议和直播,让办公桌无限延伸

    2020 年 为避免人员流动造成聚集性感染 全世界各地不少企业开始允许员工居家办公 跨地域 跨终端协同办公行业迎来井喷式增长 视频会议 疫 外爆发 关注 融云 RongCloud 了解协同办公平台更多干货 2022 年 全球疫情仍在蔓延 对
  • Qt,QEvent

    QEvent Class The QEvent class is the base class of all event classes Event objects contain event parameters More include
  • Gabor滤波器与特征提取

    一 Gabor滤波器 Gabor滤波器 最主要使用优势体现在对物体纹理特征的提取上 二维Gabor基函数能够很好地描述哺乳动物初级视觉系统中一对简单视觉神经元的感受野特性 随着小波变换和神经生理学的发展 Gabor变换逐渐演变成二维Gabo
  • HTTP项目常见状态码笔记(200,302,400,403,404,405,500...),

    目录 认识 状态码 status code 出现200 403 Forbidden 出现404 404 Not Found 出现 405 Method Not Allowed 出现 500 Internal Server Error 504
  • windows如何让电脑朗读你的文字

    在使用电脑的过程中 常常需要文字能够自动朗读 那么你是如何解决的呢 其实可以不借助任何外部软件 而使用windows记事本就能简单将任意文字转化成语音朗读 步骤1 新建一个记事本 注意记事本的默认后缀名为 txt 步骤2 打开记事本 在记事
  • python库和模块的区别_python中模块、包、库的区别和使用

    模块 就是 py文件 里面定义了一些函数和变量 需要的时候就可以导入这些模块 包 在模块之上的概念 为了方便管理而将文件进行打包 包目录下第一个文件便是 init py 然后是一些模块文件和子目录 假如子目录中也有 init py 那么它就
  • Docker 数据迁移

    问题描述 docker存储目录磁盘空间不够了 需要迁移到新的磁盘 忒扣了 迁移分析 迁移数据前向客户说明时间 提前告知业务部门系统维护相关事宜 查看中间件状态 应用服务是否正常 正常情况下才可停止服务进行下一步 停止应用 中间件等程序 停止
  • 解决问题记录10:JAVA调用,kettle资源库连接mysql8小时后报错问题

    先说解决方法 最终我的解决方式是使用心跳机制 创建了一个很简单的查询作业 让它每3小时执行一次 这几天在凌晨1点是跑kettle作业时遇到了一个问题 报错如下 The last packet successfully received fr
  • Python爬取ppt工作项目模板

    前言 ppt模板爬取 大约有一百多套工作项目ppt模板 需要的小伙伴可以通过以下程序来下载 1 爬取程序 author 爱分享的山哥 import requests from bs4 import BeautifulSoup import
  • 服务器上的网站突然打不开了,网站突然打不开是什么情况!

    对于一个网站来说 宣传企业 发展业务是根本目的 保证网站能打开却是最基本的需求 如果一个网站隔三差五就打不开 对于用户体验和网站排名都有很大的影响的 也是让很多站长非常烦恼的地方 下面小编就来跟大家聊一下网站突然打不开是什么情况 第一 域名
  • 【译】无缓冲 I/O 会让你的 Rust 程序变慢

    文章标题 译 无缓冲 I O 会让你的 Rust 程序变慢 Unbuffered I O Can Make Your Rust Programs Much Slower 译文 原文链接 https era co blog unbuffere