译| 关于 Unix 命令 `yes` 的小故事
原文阅读:A Little Story About the `yes` Unix Command
写在前面:瑟瑟发抖的首次翻译
这是第一次动手翻译一篇外文,看懂和翻懂是不一样的,你所见到的是 v3.0 版本…
感谢 依云 信雅达
的科普和满满的批注,还有依云和传奇老师的最后的校正,以及,H 老师的文章分享~
如果你发现本文有任何一处翻译不当的,欢迎指教,感谢感谢(///▽///)
译文开始
你所知的最简单的 Unix 命令是什么呢?
有echo
命令,用于将字符串打印到标准输出流,并以 o 为结束的命令。
在成堆的简单 Unix 命令中,也有 yes
命令。如果你不带参数地运行yes
命令,你会得到一串无尽的被换行符分隔开的 y 字符流:
y
y
y
y
(...你明白了吧)
复制代码
一开始看似无意义的东西原来它是非常的有用:
yes | sh 糟心的安装.sh
复制代码
你曾经有安装一个程序,需要你输入“y”并按下回车继续安装的经历吗?yes
命令就是你的救星。它会很好地履行安装程序继续执行的义务,而你可以继续观看 Pootie Tang.(一部歌舞喜剧)。
编写 yes
emmm,这是 BASIC 编写 ‘yes’的一个基础版本:
10 PRINT "y"
20 GOTO 10
复制代码
下面这个是用 Python 实现的编写 ‘yes’:
while True:print("y")
复制代码
看似很简单?不,执行速度没那么快! 事实证明,这个程序执行的速度非常慢。
python yes.py | pv -r > /dev/null
[4.17MiB/s]
复制代码
和我 Mac 自带的版本执行速度相比:
yes | pv -r > /dev/null
[34.2MiB/s]
复制代码
所以我重新写了一个执行速度更快的的 Rust 版本,这是我的第一次尝试:
use std::env;fn main() {let expletive = env::args().nth(1).unwrap_or("y".into());loop {println!("{}", expletive);}
}
复制代码
解释一下:
- 循环里想打印的那个被叫做
expletive
字符串是第一个命令行的参数。expletive
这个词是我在yes
书册里学会的; - 用
unwrap_or
给expletive
传参,为了防止参数没有初始化,我们将yes
作为默认值 - 用
into()
方法将默认参数将从单个字符串转换为堆上的字符串
来,我们测试下效果:
cargo run --release | pv -r > /dev/nullCompiling yes v0.1.0Finished release [optimized] target(s) in 1.0 secsRunning `target/release/yes`
[2.35MiB/s]
复制代码
emmm,速度上看上去并没有多大提升,它甚至比 Python 版本的运行速度更慢。这结果让我意外,于是我决定分析下用 C 实现的写入‘yes’程序的源代码。
这是 C 语言的第一个版本 ,这是 Ken Thompson 在 1979 年 1 月 10 日 Unix 第七版里的 C 实现的编写‘yes’程序:
main(argc, argv)
char **argv;
{for (;;)printf("%s\n", argc>1? argv[1]: "y");
}
复制代码
这里没有魔法。
将它同 GitHub 上镜像的 GNU coreutils 的 128 行代码版 相比较,即使 25 年过去了,它依旧在发展更新。上一次的代码变动是在一年前,现在它执行速度快多啦:
# brew install coreutils
gyes | pv -r > /dev/null
[854MiB/s]
复制代码
最后,重头戏来了:
/* Repeatedly output the buffer until there is a write error; then fail. */
while (full_write (STDOUT_FILENO, buf, bufused) == bufused)continue;
复制代码
wow,让写入速度更快他们只是用了一个缓冲区。 常量BUFSIZ
用来表明这个缓冲区的大小,根据不同的操作系统会选择不同的缓冲区大小【写入/读取】操作高效(延伸阅读传送门 。我的系统的缓冲区大小是 1024 个字节,事实上,我用 8192 个字节能更高效。
好,来看看我改进的 Rust 新版本:
use std::io::{self, Write};const BUFSIZE: usize = 8192;fn main() {let expletive = env::args().nth(1).unwrap_or("y".into());let mut writer = BufWriter::with_capacity(BUFSIZE, io::stdout());loop {writeln!(writer, "{}", expletive).unwrap();}
}
复制代码
最关键的一点是,缓冲区的大小要是 4 的倍数以确保内存对齐 。
现在运行速度是 51.3MiB/s ,比我系统默认的版本执行速度快多了,但仍然比 Ken Thompson 在 [高效的输入输出] (https://www.gnu.org/software/libc/manual/html_node/Controlling-Buffering.html) 文中说的 10.2GiB/s 慢。
更新
再一次,Rust 社区没让我失望。
这篇文章刚发布到 Reddit 的 Rust 板块, Reddit 的用户 nwydo 就提到了之前关于速率问题的讨论 。这个是先前讨论人员的优化代码,它打破了我机子的 3GB/s 的速度:
use std::env;
use std::io::{self, Write};
use std::process;
use std::borrow::Cow;use std::ffi::OsString;
pub const BUFFER_CAPACITY: usize = 64 * 1024;pub fn to_bytes(os_str: OsString) -> Vec<u8> {use std::os::unix::ffi::OsStringExt;os_str.into_vec()
}fn fill_up_buffer<'a>(buffer: &'a mut [u8], output: &'a [u8]) -> &'a [u8] {if output.len() > buffer.len() / 2 {return output;}let mut buffer_size = output.len();buffer[..buffer_size].clone_from_slice(output);while buffer_size < buffer.len() / 2 {let (left, right) = buffer.split_at_mut(buffer_size);right[..buffer_size].clone_from_slice(left);buffer_size *= 2;}&buffer[..buffer_size]
}fn write(output: &[u8]) {let stdout = io::stdout();let mut locked = stdout.lock();let mut buffer = [0u8; BUFFER_CAPACITY];let filled = fill_up_buffer(&mut buffer, output);while locked.write_all(filled).is_ok() {}
}fn main() {write(&env::args_os().nth(1).map(to_bytes).map_or(Cow::Borrowed(&b"y\n"[..],),|mut arg| {arg.push(b'\n');Cow::Owned(arg)},));process::exit(1);
}
复制代码
一个新的实现方式!
- 我们预先准备了一个填充好的字符串缓冲区,在每次循环中重用。
- 标准输出流被锁保护着,所以,我们不采用不断地获取、释放的形式,相反的,我们用 lock 进行数据写入同步。
- 我们用平台原生的 std::ffi::OsString 和 std::borrow::Cow 去避免不必要的空间分配
我唯一能做的事情就是 删除一个不必要的 mut 。
这是我这次经历的一个总结:
看似简单的 yes 程序其实没那么简单,它用了一个输出缓冲和内存对齐形式去提高性能。重新实现 Unix 工具很有意思,我很欣赏那些让电脑运行飞速的有趣的小技巧。
附上原文
A Little Story About the yes
Unix Command
What's the simplest Unix command you know? There's echo
, which prints a string to stdout andtrue
, which always terminates with an exit code of 0.
Among the rows of simple Unix commands, there's alsoyes
. If you run it without arguments, you get an infinite stream of y's, separated by a newline:
y
y
y
y
(...you get the idea)
复制代码
What seems to be pointless in the beginning turns out to be pretty helpful :
yes | sh boring_installation.sh
复制代码
Ever installed a program, which required you to type "y" and hit enter to keep going?yes
to the rescue! It will carefully fulfill this duty, so you can keep watchingPootie Tang.
Writing yes
Here's a basic version in... uhm... BASIC.
10 PRINT "y"
20 GOTO 10
复制代码
And here's the same thing in Python:
while True:print("y")
复制代码
Simple, eh? Not so quick! Turns out, that program is quite slow.
python yes.py | pv -r > /dev/null
[4.17MiB/s]
复制代码
Compare that with the built-in version on my Mac:
yes | pv -r > /dev/null [34.2MiB/s] So I tried to write a quicker version in Rust. Here's my first attempt:
use std::env;fn main() {let expletive = env::args().nth(1).unwrap_or("y".into());loop {println!("{}", expletive);}
}
复制代码
Some explanations:
- The string we want to print in a loop is the first command line parameter and is named expletive. I learned this word from the yes manpage.
- I use unwrap_or to get the expletive from the parameters. In case the parameter is not set, we use "y" as a default.
- The default parameter gets converted from a string slice (&str) into an owned string on the heap (String) using into().
Let's test it.
cargo run --release | pv -r > /dev/nullCompiling yes v0.1.0Finished release [optimized] target(s) in 1.0 secsRunning `target/release/yes`
[2.35MiB/s]
复制代码
Whoops, that doesn't look any better. It's even slower than the Python version! That caught my attention, so I looked around for the source code of a C implementation.
Here's the very first version of the program, released with Version 7 Unix and famously authored by Ken Thompson on Jan 10, 1979:
main(argc, argv)
char **argv;
{for (;;)printf("%s\n", argc>1? argv[1]: "y");
}
复制代码
No magic here.
Compare that to the 128-line-version from the GNU coreutils, which is mirrored on Github. After 25 years, it is still under active development! The last code change happened around a year ago. That's quite fast:
# brew install coreutils
gyes | pv -r > /dev/null
[854MiB/s]
复制代码
The important part is at the end:
/* Repeatedly output the buffer until there is a write error; then fail. */
while (full_write (STDOUT_FILENO, buf, bufused) == bufused)continue;
复制代码
Aha! So they simply use a buffer to make write operations faster. The buffer size is defined by a constant namedBUFSIZ
, which gets chosen on each system so as to make I/O efficient (see here). On my system, that was defined as 1024 bytes. I actually had better performance with 8192 bytes.
I've extended my Rust program:
use std::env;
use std::io::{self, BufWriter, Write};const BUFSIZE: usize = 8192;fn main() {let expletive = env::args().nth(1).unwrap_or("y".into());let mut writer = BufWriter::with_capacity(BUFSIZE, io::stdout());loop {writeln!(writer, "{}", expletive).unwrap();}
}
复制代码
The important part is, that the buffer size is a multiple of four, to ensure memory alignment.
Running that gave me 51.3MiB/s. Faster than the version, which comes with my system, but still way slower than the results from this Reddit post that I found, where the author talks about 10.2GiB/s.
####Update
Once again, the Rust community did not disappoint. As soon as this post hit the Rust subreddit, user nwydo pointed out a previous discussion on the same topic. Here's their optimized code, that breaks the 3GB/s mark on my machine:
use std::env;
use std::io::{self, Write};
use std::process;
use std::borrow::Cow;use std::ffi::OsString;
pub const BUFFER_CAPACITY: usize = 64 * 1024;pub fn to_bytes(os_str: OsString) -> Vec<u8> {use std::os::unix::ffi::OsStringExt;os_str.into_vec()
}fn fill_up_buffer<'a>(buffer: &'a mut [u8], output: &'a [u8]) -> &'a [u8] {if output.len() > buffer.len() / 2 {return output;}let mut buffer_size = output.len();buffer[..buffer_size].clone_from_slice(output);while buffer_size < buffer.len() / 2 {let (left, right) = buffer.split_at_mut(buffer_size);right[..buffer_size].clone_from_slice(left);buffer_size *= 2;}&buffer[..buffer_size]
}fn write(output: &[u8]) {let stdout = io::stdout();let mut locked = stdout.lock();let mut buffer = [0u8; BUFFER_CAPACITY];let filled = fill_up_buffer(&mut buffer, output);while locked.write_all(filled).is_ok() {}
}fn main() {write(&env::args_os().nth(1).map(to_bytes).map_or(Cow::Borrowed(&b"y\n"[..],),|mut arg| {arg.push(b'\n');Cow::Owned(arg)},));process::exit(1);
}
复制代码
Now that's a whole different ballgame!
- We prepare a filled string buffer, which will be reused for each loop.
- Stdout is protected by a lock. So, instead of constantly acquiring and releasing it, we keep it all the time.
- We use a the platform-native
std::ffi::OsString
andstd::borrow::Cow
to avoid unnecessary allocations.
The only thing, that I could contribute was removing an unnecessary mut
. ?
Lessons learned
The trivial programyes
turns out not to be so trivial after all. It uses output buffering and memory alignment to improve performance. Re-implementing Unix tools is fun and makes me appreciate the nifty tricks, which make our computers fast.
转载于:https://juejin.im/post/5a3133b86fb9a0451171214c
译| 关于 Unix 命令 `yes` 的小故事相关推荐
- 关于 Unix 命令 `yes` 的小故事
原文阅读:A Little Story About the `yes` Unix Command 写在前面:瑟瑟发抖的首次翻译 这是第一次动手翻译一篇外文,看懂和翻懂是不一样的,你所见到的是 v3.0 ...
- 搞不懂SDN和SD-WAN?那是因为你没看这个小故事—Vecloud微云
很久很久以前,有一个村子,名叫"通信(童心)村". 村里的每一户,都有一个男人和一个女人. 每一户,都以搬砖为生. 从不同的地方,搬到不同的地方. 他们怎么办呢?很简单,男人负责搬 ...
- 睡前小故事之Html
睡前小故事之Html HTML的英文全称是 Hypertext Marked Language,即超文本标记语言.HTML是由Web的发明者 Tim Berners-Lee和同事 Daniel W. ...
- 睡前小故事之MySQL起源
睡前小故事之MySQL起源 MySQL起源 作者介绍 整理来自网络 MySQL起源 MySQL的海豚标志的名字叫"sakila",它是由MySQLAB的创始人Monty从用户在&q ...
- @程序员,你真的会用 Unix 命令?
那些常用的 Unix 命令,你不知道的功能! 作者 | Vegard Stikbakke 译者 | 弯月 责编 | 屠敏 出品 | CSDN(ID:CSDNNews) 如何挑战百万年薪的人工智能! h ...
- Java 入门-02-人机交互-图形化界面的小故事
人机交互的小故事 1981 年,IBM 和 wicrosoft 共同推出的 ms-dos 系统,在黑屏下面输入命令 1981 年 4 月 27 日,施乐公司推出了第一个有操作窗口的系统,引起了很大的轰 ...
- GNU 和 UNIX 命令
使用命令行 本节讨论初级管理(LPIC-1)考试 101 的主题 1.103.1 的内容.这个主题的权值是 5. 在本节中,学习以下主题: 使用命令行与 shell 和命令进行交互 有效的命令和命令序 ...
- 管理小故事精髓 100例(转) 1
1.黄金台招贤 如何将企业治理好,一直是管理者的一个"研究课题".有的研究有素,也就治理有方:有的研究无得,也就治理失败.要治理好企业,必须网罗人才,古代燕昭王黄金台招贤,便是最著 ...
- 【爬虫】每天定时爬取网页小故事并发送至指定邮箱
看题目 ,需要实现三部分工作,第一部分为爬取网页小故事,第二部分为发送至指定邮箱,第三部分为定时启动程序.爬取网页内容可以使用BeautifulSoup库实现,发送邮件可以使用smtplib库实现,定 ...
最新文章
- python日历提醒_Python之时间:calender模块(日历)
- java crossdomin.xml_crossdomain.xml的配置详解
- sklearn学习(一)
- mysql导出数据表 .xls_mysql数据库导出xls-自定义
- oppo设备怎么样无需root激活XPOSED框架的教程
- Mobile孵化周即将在加州召开!
- 【转】人工智能-1.2.2 神经网络是如何进行预测的
- php 语法验证_PHP用户登录验证模块
- 【宇润日常疯测-004】JS 遍历数组如何快!快!快!
- 算法设计与分析基础知识总结——dayOne
- 智能雷达感应人体存在,照明雷达技术应用,雷达模块技术方案
- Win11怎么设置鼠标箭头图案?Win11更换鼠标图案的方法
- 拯救你的SD卡,找回丢失的文件
- 【邮件格式规则】-工作中电子邮件的使用
- python证件照换底色_python利用opencv实现证件照换底
- js原生 在线客服功能
- Firefox的下载处理器:FlashGot v1.0 Final颁发
- 【XBEE手册】ZigBee网络
- java工单管理系统_企业工单管理系统--使用mybatis
- 美国将派大量自动昆虫机器人到火星执行任务
热门文章
- 店铺人群标签乱了怎么办,如何纠正店铺人群标签
- QuestMobile春节大报告:用户增速快手第一百度第二
- 评价一个学习算法(斯坦福machine learning week 6)
- Unity2018灯光烘培
- 马上消费金融接受中金、中信建投辅导,拟公开发行不超过13亿股
- 51单片机入门学习日记day05
- 「Linux」FTP Error 550 - Server denied you to change to the given directory
- 男生学会计专业好还是学计算机专业,计算机和会计哪个难学 哪个更有发展前景...
- huntshowdown服务器维护吗,猎杀对决《HuntShowdown》新手入门攻略
- 005 Linux系统内存错误产生的原因及调试方法(段错误|core dumped)