原文阅读: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_orexpletive传参,为了防止参数没有初始化,我们将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?yesto 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 and std::borrow::Cow to avoid unnecessary allocations.

The only thing, that I could contribute was removing an unnecessary mut. ?

Lessons learned

The trivial programyesturns 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` 的小故事相关推荐

  1. 关于 Unix 命令 `yes` 的小故事

    原文阅读:A Little Story About the `yes` Unix Command 写在前面:瑟瑟发抖的首次翻译 这是第一次动手翻译一篇外文,看懂和翻懂是不一样的,你所见到的是 v3.0 ...

  2. 搞不懂SDN和SD-WAN?那是因为你没看这个小故事—Vecloud微云

    很久很久以前,有一个村子,名叫"通信(童心)村". 村里的每一户,都有一个男人和一个女人. 每一户,都以搬砖为生. 从不同的地方,搬到不同的地方. 他们怎么办呢?很简单,男人负责搬 ...

  3. 睡前小故事之Html

    睡前小故事之Html HTML的英文全称是 Hypertext Marked Language,即超文本标记语言.HTML是由Web的发明者 Tim Berners-Lee和同事 Daniel W. ...

  4. 睡前小故事之MySQL起源

    睡前小故事之MySQL起源 MySQL起源 作者介绍 整理来自网络 MySQL起源 MySQL的海豚标志的名字叫"sakila",它是由MySQLAB的创始人Monty从用户在&q ...

  5. @程序员,你真的会用 Unix 命令?

    那些常用的 Unix 命令,你不知道的功能! 作者 | Vegard Stikbakke 译者 | 弯月 责编 | 屠敏 出品 | CSDN(ID:CSDNNews) 如何挑战百万年薪的人工智能! h ...

  6. Java 入门-02-人机交互-图形化界面的小故事

    人机交互的小故事 1981 年,IBM 和 wicrosoft 共同推出的 ms-dos 系统,在黑屏下面输入命令 1981 年 4 月 27 日,施乐公司推出了第一个有操作窗口的系统,引起了很大的轰 ...

  7. GNU 和 UNIX 命令

    使用命令行 本节讨论初级管理(LPIC-1)考试 101 的主题 1.103.1 的内容.这个主题的权值是 5. 在本节中,学习以下主题: 使用命令行与 shell 和命令进行交互 有效的命令和命令序 ...

  8. 管理小故事精髓 100例(转) 1

    1.黄金台招贤 如何将企业治理好,一直是管理者的一个"研究课题".有的研究有素,也就治理有方:有的研究无得,也就治理失败.要治理好企业,必须网罗人才,古代燕昭王黄金台招贤,便是最著 ...

  9. 【爬虫】每天定时爬取网页小故事并发送至指定邮箱

    看题目 ,需要实现三部分工作,第一部分为爬取网页小故事,第二部分为发送至指定邮箱,第三部分为定时启动程序.爬取网页内容可以使用BeautifulSoup库实现,发送邮件可以使用smtplib库实现,定 ...

最新文章

  1. python日历提醒_Python之时间:calender模块(日历)
  2. java crossdomin.xml_crossdomain.xml的配置详解
  3. sklearn学习(一)
  4. mysql导出数据表 .xls_mysql数据库导出xls-自定义
  5. oppo设备怎么样无需root激活XPOSED框架的教程
  6. Mobile孵化周即将在加州召开!
  7. 【转】人工智能-1.2.2 神经网络是如何进行预测的
  8. php 语法验证_PHP用户登录验证模块
  9. 【宇润日常疯测-004】JS 遍历数组如何快!快!快!
  10. 算法设计与分析基础知识总结——dayOne
  11. 智能雷达感应人体存在,照明雷达技术应用,雷达模块技术方案
  12. Win11怎么设置鼠标箭头图案?Win11更换鼠标图案的方法
  13. 拯救你的SD卡,找回丢失的文件
  14. 【邮件格式规则】-工作中电子邮件的使用
  15. python证件照换底色_python利用opencv实现证件照换底
  16. js原生 在线客服功能
  17. Firefox的下载处理器:FlashGot v1.0 Final颁发
  18. 【XBEE手册】ZigBee网络
  19. java工单管理系统_企业工单管理系统--使用mybatis
  20. 美国将派大量自动昆虫机器人到火星执行任务

热门文章

  1. 店铺人群标签乱了怎么办,如何纠正店铺人群标签
  2. QuestMobile春节大报告:用户增速快手第一百度第二
  3. 评价一个学习算法(斯坦福machine learning week 6)
  4. Unity2018灯光烘培
  5. 马上消费金融接受中金、中信建投辅导,拟公开发行不超过13亿股
  6. 51单片机入门学习日记day05
  7. 「Linux」FTP Error 550 - Server denied you to change to the given directory
  8. 男生学会计专业好还是学计算机专业,计算机和会计哪个难学 哪个更有发展前景...
  9. huntshowdown服务器维护吗,猎杀对决《HuntShowdown》新手入门攻略
  10. 005 Linux系统内存错误产生的原因及调试方法(段错误|core dumped)