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); } }
いくつかの説明:
- ループで出力する行は、 expletiveという最初のコマンドラインパラメーターです。 私は
yes
マニュアルからこの言葉を学びました。 -
unwrap_or
を使用して、パラメーターからunwrap_or
を取得します。 パラメータが設定されていない場合、デフォルトは「y」です。 - デフォルトパラメータは、
into()
を使用into()
、文字列フラグメント(&str
)からヒープ内のowned()
(String
)にinto()
ます。
テストします。
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); }
これはまったく別の問題です!
- 各ループで再利用される文字列バッファが用意されています。
- 標準出力ストリーム(stdout)はロックによって保護されています 。 そのため、継続的にキャプチャおよびリリースするのではなく、常に保持しています。
-
std::ffi::OsString
およびstd::ffi::OsString
プラットフォームにネイティブなstd::borrow::Cow
を使用して、不要なメモリ割り当てを回避します。
追加できるのは
mut
ことだけです。
学んだ教訓
簡単な
yes
プログラムは実際にはそれほど単純ではありませんでした。 パフォーマンスを改善するために、出力バッファリングとメモリアライメントを使用します。
標準のUnixツールのリサイクルは楽しい経験であり、コンピューターを高速化する洗練されたトリックに感謝します。