メソッドを最速でプロファイリングする方法(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社エンジニアの「?」を「!」に。原著者の許諾を得て翻訳・公開いたします。Overflowでの開発経験もあります。メソッドを最速でプロファイリングする方法(翻訳)Rubyのメソッドを最短かつ洗練された方法でプロファイリングできる方法はどれでしょうか?最近私たちのDiscourseにいくつかのinstrumentationを追加する必要が生じました。あらゆるリクエストを測定対象に加えたかったのです。特定のセクションの実行に要する時間これは、より一般性の高い問題に簡単に抽象化できます。endendこの問題を解決するうえで、いくつかのサブ問題に分割します。最初に解決すべき最もシンプルな問題は、パッチが当たっていないかのように見せかけることです。ありとあらゆるソースコードを開いて望みのinstrumentationを片っ端から追加してよいと仮定した場合、経過時間を最短で測定できるのはどんな方法でしょうか?Rubyで使える手法は次の2とおりが考えられます。ある瞬間の直前と直後にわたってTimeオブジェクトを作成するある瞬間の直前と直後にわたってProcess.clock_gettimeでFloatを返すそれでは測定してみましょう。5.011634sProcess.clock_gettime(Process::CLOCK_MONOTONIC)ではオブジェクトの割り当ても後からのガベージコレクションも発生しないので、予想どおり最速です。ついでながら、これは最も正確に期間を測定できる方法です。さしあたってこれを使うことにしましょう。#workが空である件について上述の方法では#workになっていますが、ここで何らかの処理を行うと、2つの方法の違いはかなり微妙になります。workを$redis.get('x')(これはできるだけ最短時間で測定したいと思っている項目です)に置き換えたとすると、次の結果が得られます。5.022470sつまりredisを1秒間にそれぞれ17500回/17110回/16700回実行できることになり、測定なしの場合が最速、最も低速なのはTime.nowになります。最初のベンチマークに比べれば大した差ではありませんが、もっと高速かつ安全な方法が他にあるなら、Time.nowという高い税金を払おうという人などいないでしょう。測定するコードが私たちが所有していないものである場合、ソースレベルの変更などという贅沢はできません。残る手段は次の3つです。対象となるクラスにモジュールをprependして元のメソッドをオーバーライドするalias_methodを使って元のメソッドのあたりに新しいメソッドをチェインする対象のメソッドを「パッチ適用版メソッド」に置き換えるメソッドの置き換えは危険がいっぱいです。サードパーティライブラリ内のメソッドが変更されてしまえばパッチの修正が必要ですし、長期間のメンテも無理な相談です。ひとつ興味深いオプションについて後述しますが、これはRubyVM::InstructionSequence#ofを使ってインストラクションシーケンスを強引に出力し、そこにパッチを当ててからメソッドを読み込むというものです。パッチを最速で当てられる方法であることは間違いありませんが、インストラクションシーケンスの改変という裏技は少々闇が深すぎるので常に使えるとは限りません。5.015797sprepend法はメソッドへのパッチとしては極めて洗練されていますが、古臭いalias_method法に速度でかなり負けています。#workに処理を導入するとその差はかなり微妙になります。"x"を実行した場合の結果は以下のとおりです。5.024699sパフォーマンスはほぼ拮抗しているので、下手をすると違いに気づかないかもしれません。今回のユースケースで最も頭を痛めたのは、測定結果が現在のスレッドだけに基づいていることをどのように保証するかでした。Discourseではいくつかのバックグラウンドスレッドが遅延処理を実行していることがあり、Webサービスを提供しているそうしたスレッドが測定結果に干渉しないようにしたかったのです。Pumaベースの環境では、同一のグローバルスレッドセーフRedis接続に多数のスレッドがアクセスするため、臨界点を超えてしまいます。接続をオープンする前にinstrumentationを有効にしておく手もあります。それではスレッドセーフの影響を測定することにしましょう。endendスレッド安全性に配慮する手法として次の2つが考えられます。Thread.currentがプロファイル対象のスレッドである場合に限ってinstrumentationを実行するinstrumentationデータをThread.currentに保存する(外部レジストリは速度が落ちる点に注意)Thread.currentの比較を使う方法が高速であることは見当が付きますが、処理を始めてみるとこの違いはほとんどありません。安全性のためのコストはかかるものの、大きなコストではありません。'x'の場合は以下のようになります。一般的なメソッドプロファイラここまで登場した要素をひとつにまとめ、最速のマイクロベンチを用いて一般的なメソッドプロファイラを組み立て、測定値を収集します。12}}これで以下のように実装できます(Gist)。endend「workが空」のシナリオでは、予想どおりパフォーマンスに明確な違いが生じます。‘x’を入れると、次の結果になります。結論あらゆるマイクロベンチマークを基礎とすることで、実行時の影響を最小限に留めたメソッドプロファイリング用の一般的なRubyのクラスをかなり効率よく設計できるようになります。本記事のどのコードも、帰属の表示の有無にかかわらずご自由に利用・構築いただけます。コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。enno.jpを運営。実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。Storageを使ってはいけない(翻訳)#Tags#会社紹介#自社サービス#勉強会#Rails#PHP#デザイン#実績#週刊RailsウオッチCONTACTお気軽にご相談ください。(翻訳)CONTACTTechRachoでは、パートナーシップをご検討いただける方からのご連絡をお待ちしております。お気軽にご意見・ご相談ください。