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


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


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


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



各開発者が特定のアプリケーションに関連する「注目すべき改善」の概念に投資していると言うことは困難です。 カーネルお​​よびドライバーの開発者は、多くの場合、コードとデータ構造の微最適化に多くの時間を費やして、プロセッサーキャッシュを最大限に活用し、CPU消費を削減します。 たとえほとんどのプログラマーがその利点を非常に小さいと感じたとしても。 このような最適化にはあまり注意を払う必要はありませんか? 誰かが、マイクロ最適化を目立ったものとみなすことはできないとさえ言うかもしれません。


この記事のフレームワークでは、目に見えるものを簡単に測定でき、完全に明白なものとして定義します。 コードパスから低速( vDSOなし )のシステムコール削除すると、簡単に測定可能で完全に明らかな結果が得られる場合の実際の例を示すことができますか?


パッケージスニファからランタイムプログラミング言語まで、多くの実世界の例があります。 Ruby言語の実行時に対するsigprocmaskの影響として悪名高いケースを考えてください。


sigprocmaskとはsigprocmaskですか?


sigprocmask現在のプロセスのシグナルマスクをチェックまたは設定するために使用されるシステムコール。 これにより、プログラムはシグナルをブロックまたは許可できます。これは、中断できない重要なコードを実行する必要がある場合に便利です。


これは特に難しいシステムコールではありません。 sigprocmaskに関連するカーネルコードは、呼び出しがsigset_tを現在のプロセスの状態を含むC構造体(カーネルではtask_structと呼ばれる)に書き込むことをsigset_tています。 これは非常に高速な操作です。


小さなループでsigprocmaskを呼び出す簡単なテストプログラムを作成しましょう。 stracetimeを使用して測定します。


 #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で実行し、次にstracetime実行します。


私のテストシステムで:



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呼び出しを回避します!


stracetimeしてテストを再開しますが、今回は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つの関数(システム呼び出しではない)によって行われます: getcontextsetcontext


これらは、プロセッサの状態を保存および復元するために使用されます。 これらは、ユーザー空間で例外処理またはスレッドを実装するプログラムおよびライブラリで広く使用されています。 Ruby 1.8.7の場合、ユーザー空間でのスレッドの実装には、スレッド間でコンテキストを切り替えるためにsetcontextgetcontextが必要です。


これらの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-pthreadconfigureスクリプトは_setjmpおよび_longjmpを検索して使用します(アンダースコアに注意してください)。 これらの関数は、シグナルマスクを保存または復元しないため、 sigprocmaskシステムコールを生成しません。


だから:



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


おわりに


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



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


All Articles