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ツールのリサイクルは楽しい経験であり、コンピューターを高速化する洗練されたトリックに感謝します。



All Articles