この出版物は、「 何千ものシステムコールを避けるためにTZ環境変数を設定する方法 」の投稿の論理的な続きです。 ここでは、マイクロ最適化(システムコールの削除など)がパフォーマンスに大きく影響する典型的な状況を考えます。
顕著な改善とは何ですか?
前に、 アプリケーションが何千もの追加のシステムコールを避けるために設定できる環境変数について説明しました 。 この記事には、懐疑的な見方をした公正な質問がありました。
- 「微妙ですが、単一のシステムコールを削除すると、プログラム全体のパフォーマンスに大きな影響を与えますか?」
- 「それは不要に見えます。 Linuxでは、システムコールはすでに高速です。」
各開発者が特定のアプリケーションに関連する「注目すべき改善」の概念に投資していると言うことは困難です。 カーネルおよびドライバーの開発者は、多くの場合、コードとデータ構造の微最適化に多くの時間を費やして、プロセッサーキャッシュを最大限に活用し、CPU消費を削減します。 たとえほとんどのプログラマーがその利点を非常に小さいと感じたとしても。 このような最適化にはあまり注意を払う必要はありませんか? 誰かが、マイクロ最適化を目立ったものとみなすことはできないとさえ言うかもしれません。
この記事のフレームワークでは、目に見えるものを簡単に測定でき、完全に明白なものとして定義します。 コードパスから低速( vDSOなし )のシステムコールを削除すると、簡単に測定可能で完全に明らかな結果が得られる場合の実際の例を示すことができますか?
パッケージスニファからランタイムプログラミング言語まで、多くの実世界の例があります。 Ruby言語の実行時に対するsigprocmask
の影響として悪名高いケースを考えてください。
sigprocmask
とはsigprocmask
ですか?
sigprocmask
現在のプロセスのシグナルマスクをチェックまたは設定するために使用されるシステムコール。 これにより、プログラムはシグナルをブロックまたは許可できます。これは、中断できない重要なコードを実行する必要がある場合に便利です。
これは特に難しいシステムコールではありません。 sigprocmaskに関連するカーネルコードは、呼び出しがsigset_t
を現在のプロセスの状態を含むC構造体(カーネルではtask_struct
と呼ばれる)に書き込むことをsigset_t
ています。 これは非常に高速な操作です。
小さなループでsigprocmask
を呼び出す簡単なテストプログラムを作成しましょう。 strace
とtime
を使用して測定します。
#include <stdlib.h> #include <signal.h> int main(int argc, char *argv[]) { int i = 0; sigset_t test; for (; i < 1000000; i++) { sigprocmask(SIG_SETMASK, NULL, &test); } return 0; }
gcc -o test test.c
コンパイルしgcc -o test test.c
まず、 time
で実行し、次にstrace
とtime
実行します。
私のテストシステムで:
- 実行
time ./test
示したもの:0.047秒-リアルタイム、0.012秒-ユーザー時間、0.036秒-システム時間。 - 実行
time strace -ttT ./test
示したもの:52.364秒-リアルタイム、9.313秒-ユーザー時間、14.349秒-システム時間。
strace
の場合、各sigprocmask
呼び出し(おそらくシステムでrt_sigprocmask
としてrt_sigprocmask
れます)について、おおよそのrt_sigprocmask
が表示されます。 彼らは非常に小さいです。 私のテストシステムでは、ほとんどの場合、0.000003秒前後の値を受け取りました-予期せぬ最大0.000074秒の急増です。
多くの理由により、システムコールの正確な実行時間を測定することは非常に困難です。 これらは、この記事で説明した問題をはるかに超えています。 したがって、すべての測定が等しく不正確に実行されたと想定できます。
したがって、私たちがすでに知っていること
-
sigprocmask
現在のプロセスのシグナルマスクを設定または検証するために使用されるシステムコール。 - カーネルコードはシンプルで、非常に高速に実行されるはずです。
-
time
とstrace
を使用しtime
測定では、100万のsigprocmask呼び出しが短いサイクルで実行された場合、各呼び出しにかかる時間は非常に短いことが示されています。
それでは、なぜ余分なsigprocmask
を取り除く必要があるのでしょうか?
もっと詳しく理解します
アプリケーションで使用する他の誰かのコード(システムライブラリ、カーネル、glibcなど)が予期しないことを行ったり、一見して明らかでない副作用を引き起こすことがあります。 以下の例として、テストプログラムでsigprocmask
を間接的に使用すると、パフォーマンスが大幅に低下することを示します。 そして、これが実際のアプリケーションでどのように現れるかを示します。
sigprocmask
追加呼び出しがどのように明らかで簡単に測定可能なパフォーマンスsigprocmask
につながったかの最も明確な例の1つは、Ruby 1.8.7に関連付けられました。 これは、コードが1つの特定のconfigure
フラグでコンパイルされた場合に注意されました。
Ruby 1.8.7の広範な使用中に、ほとんどのオペレーティングシステム(Debian、Ubuntuなど)で使用されたデフォルトの構成フラグ値から始めましょう。
sigprocmask
テスト
テストコードを見てください。
def make_thread Thread.new do a = [] 10_000_000.times do a << "a" a.pop end end end t = make_thread t1 = make_thread t.join t1.join
ここではすべてが簡単です。実行スレッドを2つ作成し、それぞれが1,000万回配列にデータを追加および削除します。 設定フラグとstrace
のデフォルト値で実行すると、驚くべき結果が得られます。
$ strace -ce rt_sigprocmask /tmp/test-ruby/usr/local/bin/ruby /tmp/test.rb Process 30018 attached Process 30018 detached % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 0.50 0.139288 0 20033025 rt_sigprocmask
Ruby仮想マシンは、 2,000万を超える sigprocmask
システムコールを生成しました。 あなたは言うでしょう:しかし、彼らは非常に少しの時間しかかかりませんでした! これは何ですか?」
すでに述べたように、システムコールの継続時間を測定するのはそれほど簡単ではありません。 strace
代わりにtime
テストプログラムを再起動し、システムで完了するまでにかかる時間を確認します。
$ time /tmp/gogo/usr/local/bin/ruby /tmp/test.rb real 0m6.147s user 0m5.644s sys 0m0.476s
約6秒の実際の実行。 これは、1秒あたり約330万のsigprocmask
呼び出しです。 わあ
また、1つのconfigure
フラグを構成configure
と、Ruby仮想マシンがシステムを構築し、 sigprocmask
呼び出しを回避します!
strace
とtime
してテストを再開しますが、今回はsigprocmask
呼び出しを回避するためにRubyを少しひねります。
$ strace -ce rt_sigprocmask /tmp/test-ruby-2/usr/local/bin/ruby /tmp/test.rb % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- -nan 0.000000 0 3 rt_sigprocmask
かっこいい! sigprocmask
呼び出しの回数を2000万回から3 sigprocmask
減らしました。システムコールの実行時間の計算にstrace
問題があったようstrace
:(
time
が何を言うか見てみましょう:
$ time /tmp/test-ruby-2/usr/local/bin/ruby /tmp/test.rb real 0m3.716s user 0m3.692s sys 0m0.004s
前の例よりも約40%高速(リアルタイム)であり、1秒あたり1 sigprocmask
呼び出し未満です。
素晴らしい結果ですが、いくつかの疑問が生じます。
- この例は少し難解ですか?
- 実際のプロジェクトで本当に関連するのはいつですか?
- 構成フラグを設定すると、呼び出し回数が減りますか?
まず実際の例を見てから、詳細を理解しましょう。
実際の例:Puppet
Puppetで見つかったバグは、Ruby仮想マシンでの追加のsigprocmask
呼び出しの効果を正確に示しています。
深刻なパフォーマンスの問題が発生しました。 Puppetは非常に遅いです。
最初の例:
$ time puppet —version 0.24.5 real 0m0.718s user 0m0.576s sys 0m0.140s
その時点で、数千ではないにしても数百のrt_sigprocmask呼び出しが行われました(SIG_BLOCK、NULL、[]、8)。 そして、これらはすべてバージョンを表示するためです。
通信を読むと、Puppetのパフォーマンスの低下について不満を言う他のコメントがあります。 Stackoverflowに関する質問は 、同じ問題に関するものです。
しかし、これはRubyとPuppetだけではありません。 他のプロジェクトのユーザーは、 このようなバグについて書いており、プロセッサの全負荷と数十万のsigprocmask
呼び出しをsigprocmask
ます。
なぜこれが起こっているのですか? これは簡単に修正できますか?
事実、 sigprocmask
呼び出しsigprocmask
、glibcの2つの関数(システム呼び出しではない)によって行われます: getcontext
とsetcontext
。
これらは、プロセッサの状態を保存および復元するために使用されます。 これらは、ユーザー空間で例外処理またはスレッドを実装するプログラムおよびライブラリで広く使用されています。 Ruby 1.8.7の場合、ユーザー空間でのスレッドの実装には、スレッド間でコンテキストを切り替えるためにsetcontext
とgetcontext
が必要です。
これらの2つの関数はかなり高速であると思われるかもしれません。 結局のところ、彼らは単にプロセッサレジスタの小さなセットを保存または復元するだけです。 はい、保存は非常に簡単な操作です。 しかし、どうやら、glibcでのこれらの関数の実装は、シグナルマスクの保存と復元にsigprocmask
システムコールが使用されるようなものsigprocmask
。
Linuxには、カーネルの代わりに特定のシステムコールを行うメカニズム( vDSO )が用意されていることを思い出してください。 これにより、実装コストが削減されます。 残念ながら、 sigprocmask
それらの1 sigprocmask
ません。 すべてのsigprocmask
システムコールは、ユーザー空間からカーネルへの移行をもたらします。
このような遷移のコストは、 setcontext
およびgetcontext
(メモリへの単純な書き込みを表す)の他の操作のコストよりもはるかに高くなります。 これらの関数を頻繁に呼び出す場合、何かをすばやく行う必要があるたびに(たとえば、切り替えのためにプロセッサレジスタのセットを保存または復元するたびに)遅い操作(この場合はsigprocmask
を通過しない sigprocmask
システムコール)を実行します実行のスレッド)。
構成フラグを変更すると状況が改善されるのはなぜですか?
Ruby 1.8.7が広く使用された時代、デフォルトのフラグは--enable-pthread
でした。これは、タイマー(タイマースレッド)で実行されるOSレベルで別のスレッドをアクティブにします。 Ruby仮想マシンをインターセプト(プリエンプション)するために必要に応じて開始しました。 したがって、マシンは、Rubyプログラムで作成されたスレッドにマップされたユーザー空間内のスレッド間を切り替える時であることがわかりました。 また、 --enable-pthread
にアクセスすると、 configure
スクリプトがgetcontext
およびsetcontext
関数を見つけて使用します。
--enable-pthread
を使用しない--enable-pthread
、 configure
スクリプトは_setjmp
および_longjmp
を検索して使用します(アンダースコアに注意してください)。 これらの関数は、シグナルマスクを保存または復元しないため、 sigprocmask
システムコールを生成しません。
だから:
-
--enable-pthread
および--disable-pthread
、Ruby仮想マシンのユーザー空間でマルチスレッドを制御するため--disable-pthread
設計されました:OSレベルでの実行のスレッドまたは単純なVTALRM
信号のいずれかがVTALRM
、別のRubyスレッドに切り替える時間であることをVMに通知します。 - 2つのプリエンプトメソッドを切り替えることによる予期しない副作用は、スイッチを実装するプリミティブが
setcontext/getcontext
または_setjmp/_longjmp
いずれかを_setjmp/_longjmp
。 - これにより、
setcontext/getcontext
ペアが使用された場合、sigprocmask
追加呼び出しsigprocmask
れたという事実にsigprocmask
。 - そして、これにより、生産性が低下しました。
- Puppetを含む多くのプロジェクトの作成者が直面したこと。
そして、これはすべて、単一のシステムコールを有効または無効にする1つのフラグのためです。
おわりに
マイクロ最適化は重要です。 ただし、それらの重要度は、もちろん、アプリケーション自体の詳細に依存します。 依存するライブラリとコードは、予期しないことを実行できることに注意してください(たとえば、遅いシステムコールを行う)。 このような問題を特定して修正する方法を知っている場合、これはユーザーに大きな影響を与えます。 そして場合によっては-ユーザーのユーザーに対して。