マイクロ最適化は重要です:2000万のシステムコールを防止します



この出版物は、「 何千ものシステムコールを避けるためにTZ環境変数を設定する方法 」の投稿の論理的な続きです。 ここでは、マイクロ最適化(システムコールの削除など)がパフォーマンスに大きく影響する典型的な状況を考えます。







顕著な改善とは何ですか?



前に、 アプリケーションが何千もの追加のシステムコールを避けるために設定できる環境変数について説明しまし 。 この記事には、懐疑的な見方をした公正な質問がありました。









各開発者が特定のアプリケーションに関連する「注目すべき改善」の概念に投資していると言うことは困難です。 カーネルお​​よびドライバーの開発者は、多くの場合、コードとデータ構造の微最適化に多くの時間を費やして、プロセッサーキャッシュを最大限に活用し、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



実行します。







私のテストシステムで:









strace



の場合、各sigprocmask



呼び出し(おそらくシステムでrt_sigprocmask



としてrt_sigprocmask



れます)について、おおよそのrt_sigprocmask



が表示されます。 彼らは非常に小さいです。 私のテストシステムでは、ほとんどの場合、0.000003秒前後の値を受け取りました-予期せぬ最大0.000074秒の急増です。







多くの理由により、システムコールの正確な実行時間を測定することは非常に困難です。 これらは、この記事で説明した問題をはるかに超えています。 したがって、すべての測定が等しく不正確に実行されたと想定できます。







したがって、私たちがすでに知っていること









それでは、なぜ余分な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



システムコールを生成しません。







だから:









そして、これはすべて、単一のシステムコールを有効または無効にする1つのフラグのためです。







おわりに



マイクロ最適化は重要です。 ただし、それらの重要度は、もちろん、アプリケーション自体の詳細に依存します。 依存するライブラリとコードは、予期しないことを実行できることに注意してください(たとえば、遅いシステムコールを行う)。 このような問題を特定して修正する方法を知っている場合、これはユーザーに大きな影響を与えます。 そして場合によっては-ユーザーのユーザーに対して。








All Articles