Unixの `yes`コマンドに関する少しの歴史

最も単純なUnixコマンドを知っていますか? echoがあり、行をstdoutに出力しtrue 、何も行いませんが、ゼロコードでのみ終了します。

多くの単純なUnixコマンドの中で、 yesコマンドは隠れていました。 引数なしで実行すると、文字「y」の無限のストリームが得られ、それぞれに新しい行が追加されます。

 y y y y (...   ) 

一見したところ、チームは無意味に見えますが、時には役立つこともあります。

 yes | sh boring_installation.sh 

「y」と入力してEnterキーを押してインストールする必要があるプログラムをインストールしたことがありますか? yesコマンドが助けになります! 彼女はこのタスクをきちんと完了しますので、あなたはPootie Tangを見て気を散らすことはできません。

はいと書く


これが... hmm ... BASICの基本バージョンです。

 10 PRINT "y" 20 GOTO 10 

Pythonでも同じことが言えます:

 while True: print("y") 

簡単そう? ちょっと待って!

結局のところ、そのようなプログラムは非常にゆっくり実行されます。

 python yes.py | pv -r > /dev/null [4.17MiB/s] 

「ポピー」の組み込みバージョンと比較してください。

 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); } } 

いくつかの説明:


テストします。

 cargo run --release | pv -r > /dev/null Compiling yes v0.1.0 Finished release [optimized] target(s) in 1.0 secs Running `target/release/yes` [2.35MiB/s] 

本当に改善されたものはありません。 Pythonバージョンよりもさらに遅いです! これに興味があったので、Cでの実装のソースコードを探しました。

以下は、1979年1月10日にKen Thompsonの名誉ある著者によってバージョン7 Unixの一部としてリリースされたプログラムの最初のバージョンです

 main(argc, argv) char **argv; { for (;;) printf("%s\n", argc>1? argv[1]: "y"); } 

魔法はありません。

Githubにミラーを備えたGNU coreutilsキット128行バージョンと比較してください。 25年後、プログラムはまだ活発に開発されています! 最後のコード変更は約1年前に発生しました。 彼女はかなり速いです:

 # 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; 

うん! そのため、バッファを使用して書き込み操作を高速化します。 バッファサイズはBUFSIZ定数によって設定されます。これは、I / O操作を最適化するために各システムに対して選択されます( こちらを参照)。 私のシステムでは、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.3 MiB / sを生成します。 システムにインストールされているバージョンよりも高速ですが、 Redditで見つけた投稿の著者のバージョンよりもはるかに低速です。 彼は10.2 GiB / sの速度を達成したと言います。

追加


いつものように、Rustコミュニティは失望しませんでした。 この記事がRustに入るとすぐに、ユーザーnwydoはこのトピックに関する以前の議論を指摘しました。 私のマシンで3 GB / 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); } 

これはまったく別の問題です!


追加できるのは mutことだけです。

学んだ教訓


簡単なyesプログラムは実際にはそれほど単純ではありませんでした。 パフォーマンスを改善するために、出力バッファリングとメモリアライメントを使用します。

標準のUnixツールのリサイクルは楽しい経験であり、コンピューターを高速化する洗練されたトリックに感謝します。

Source: https://habr.com/ru/post/J342002/


All Articles