Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHP で実⾏中のスクリプトの動作を下から覗き⾒る

sji
November 30, 2020

PHP で実⾏中のスクリプトの動作を下から覗き⾒る

https://tagayas.connpass.com/event/193881/ で使ったスライド
https://github.com/reliforp/reli-prof が現在のリポジトリ
https://qiita.com/sj-i/items/a29d54cfd83f230ddc3d にもツールの実装を解説した記事
https://speakerdeck.com/sji/php-de-php-falsepurohuairawotukurou でも同じツールについて別の形で紹介

sji

November 30, 2020
Tweet

More Decks by sji

Other Decks in Programming

Transcript

  1. メモリ それぞれ背番号を持ち 1 バイト分の情報を保持する領域の並び 16GB なら背番号 0 から 170 億番くらいまでの範囲

    背番号を番地またはアドレスと呼ぶ 各番地の情報を取り出せる プログラム = CPU への命令はメモリへ読み込むことで CPU から 実⾏可能
  2. FFI 去年リリースされた PHP 7.4 の機能 Foreign Function Interface の略 PHP

    から他の⾔語で作られた関数を呼び出すための機能
  3. FFI 導⼊以前 7.3 までの PHP は PHP 単体でできないことが多かった PHP Manual

    に載っている標準関数の範囲外は C の拡張機能を作 る or 導⼊する必要があった 実は標準関数⾃体も処理系に標準添付されている拡張という形で 実現されている PHP の拡張を作るには特殊な作法と⼀定以上の C ⾔語の知識が 必要
  4. FFI 導⼊以降 FFI 以外の拡張を使わず C ⾔語資産を PHP から直接呼び出せる システムコールやメモリ操作の関数、処理系の内部関数もそのま ま呼べる

    当然そのリスクも⼀緒に持ち込まれる C ⾔語 や PHP 処理系内部の深い知識、拡張を書く際の特有の作 法はいらなくなる しかもそういうコードを composer からインストールできる
  5. FFI の使い⽅ $ffi = \FFI::cdef( 'int printf(const char * restrict

    format, ... );', // 'libc.so' /* libc は処理系とともに読み込まれているので不要 */ ); $ffi->printf('hello clang world');
  6. /proc/<pid>/maps の例 address perms offset dev inode pathname 5636ed586000-5636ed692000 r--p

    00000000 08:01 3672367 /usr/local/bin/php 5636ed786000-5636edb19000 r-xp 00200000 08:01 3672367 /usr/local/bin/php 5636edb86000-5636ee36c000 r--p 00600000 08:01 3672367 /usr/local/bin/php 5636ee6df000-5636ee786000 r--p 00f59000 08:01 3672367 /usr/local/bin/php 5636ee786000-5636ee78d000 rw-p 01000000 08:01 3672367 /usr/local/bin/php ... 7f872b6df000-7f872b6e0000 r--p 00000000 08:01 2238306 /lib/x86_64-linux-gnu/ld-2.28.so 7f872b6e0000-7f872b6fe000 r-xp 00001000 08:01 2238306 /lib/x86_64-linux-gnu/ld-2.28.so 7f872b6fe000-7f872b706000 r--p 0001f000 08:01 2238306 /lib/x86_64-linux-gnu/ld-2.28.so 7f872b706000-7f872b707000 r--p 00026000 08:01 2238306 /lib/x86_64-linux-gnu/ld-2.28.so 7f872b707000-7f872b708000 rw-p 00027000 08:01 2238306 /lib/x86_64-linux-gnu/ld-2.28.so
  7. ELF

  8. PHP 処理系のシンボル情報 mod_php でも cli や fpm でも、拡張機能を共有オブジェクト / DLL

    で必要に応じてロードできる 拡張から PHP のコア機能にアクセスするためのシンボル情報は 処理系に付いてくる 処理系の ELF ファイルを解釈してシンボル情報を読み込めば、 これらの関数やデータがどの位置にあるかが分かる 拡張から可能なのに近いレベルで情報が取れる
  9. PHP で ELF を読む php-pro ler では ELF パーサを⾃前で書いてみた PHP

    で書いたの俺以外に世界で 5 ⼈くらいしかいない説がある 案外普通に書けた
  10. PHP の⽂字列はエンコード情報を持たないバイト列 添え字アクセスと ord() で 1 バイト分のデータを取り出す実 装が作れる final class

    StringByteReader implements ByteReaderInterface { use ByteReaderDisableWriteAccessTrait; public function offsetGet($offset): int { return ord($this->source[$offset]); }
  11. このインターフェースを通じて 32 ビットや 64 ビットの整数値 を取り出すクラスも作る final class LittleEndianReader implements

    IntegerByteSequenceReader { public function read8(ByteReaderInterface $data, int $offset): int { return $data[$offset]; } public function read16(ByteReaderInterface $data, int $offset): int { return ($data[$offset + 1] << 8) | $data[$offset]; } public function read32(ByteReaderInterface $data, int $offset): int { return ($data[$offset + 3] << 24) | ($data[$offset + 2] << 16) | ($data[$offset + 1] << 8) | $data[$offset]; }
  12. PHP でのバイナリ読みで困ったところ PHP では符号なし 64 ビット整数が普通には扱えない int は 64 ビット版

    PHP でも符号あり整数値 評価値が上限値である PHP_INT_MAX を超える式の値は、整数 同⼠の演算であっても oat へ暗黙キャスト 真⾯⽬に扱うと多倍超演算⽤の拡張である gmp を使うとか
  13. ZendEngine とは PHP の処理系のコアは ZendEngine と呼ばれている PHP 4 の頃に Zeev

    さんと Andi さんが処理系のコアを刷新 ⼆⼈の名前から Ze と nd をとって Zend Zend Technologies という会社が作られたりした これを⺟体に ZendFramework というフレームワークが作ら れたりもした
  14. zend_execute_data EG には zend_execute_data 型の current_execute_data というメ ンバがある 現在実⾏中の仮想マシン命令についての情報が含まれている 元はどの関数のコードから⽣成されたものか

    どの PHP スクリプトファイルの何⾏⽬から⽣成されたもの か prev_execute_data というメンバがある 同じ構造で関数の呼び出し元の情報が⼊ってる
  15. ZendEngine の構造体を PHP で読む Executor Globals や zend_execute_data は C

    ⾔語の構造体 C ⾔語構造体を PHP 側で 1 バイトずつ解釈するコードを書くの は少し⾯倒 FFI の機能で型キャストがある process_vm_readv で得たデータへのポインタをキャストすれば FFI 経由でアクセス可能 PHP 処理系のソースから必要な構造体定義を抜き出して使う
  16. ZendEngine 内部の参考資料 PHP と SAPI と ZendEngine3 と PHP の関数実⾏とその計測

    PHP による hello world ⼊⾨ https://www.slideshare.net/do_aki/php-sapi-zendengine3 https://qiita.com/sj-i/items/836fa5a5e246961c40b6 http://tech.respect-pal.jp/php-helloworld/
  17. nbody benchmark まあまあレガシーな雰囲気のコード Node での実⾏結果 8 秒に対して、PHP(7.4) での実⾏結果が 235 秒

    ⼿元のマシンでは Node で 3.6 秒、PHP(7.4) 118 秒 https://benchmarksgame- team.pages.debian.net/benchmarksgame/program/nbody-php- 3.html
  18. プロファイル結果 NBodySystem::advance() 内 130 〜 144 ⾏⽬あたりが重そう オペコードでは 60 番と

    12 番、28 番と 82 番が頻出 https://gist.github.com/sj- i/d0cbc6c0baa414ffcd00be6840a9166f
  19. プロパティアクセスの対応(1) 最内ループのプロパティアクセスで外側へ出せるものを移動 修正前 foreach ($this->bodies as $index => $referenceBody) {

    $nbBodies = count($this->bodies); for ($i = $index + 1; $i < $nbBodies; ++$i) { $body = $this->bodies[$i]; $dx = $referenceBody->x - $body->x; $dy = $referenceBody->y - $body->y;
  20. プロパティアクセスの対応(1) 最内ループのプロパティアクセスで外側へ出せるものを移動 修正後 foreach ($this->bodies as $index => $referenceBody) {

    $nbBodies = count($this->bodies); $rbx = $referenceBody->x; $rby = $referenceBody->y; for ($i = $index + 1; $i < $nbBodies; ++$i) { $body = $this->bodies[$i]; $dx = $rbx - $body->x; $dy = $rby - $body->y;
  21. 最適化の効果 書き換え後のコードで JIT 有効で実⾏すると 29 秒に 85 秒 → 29

    秒、約 3 倍⾼速化 3.6 秒の Node(V8) とはまだ 8 倍くらいの差 プロパティアクセスが遅めなの⾃体は JIT でも変わらず、PHP 8 の今後に期待
  22. C ⾔語側までトレースを取る ptrace システムコールで対象プロセスの実⾏を⼀瞬⽌めれば CPU 的な状態も取得できる C プログラムのデバッグ⽤情報を格納する DWARF 形式を使う

    両⽅使えば更に細かい C ⾔語側のトレースまで取れる筈 PHP コードのどの関数のどの⾏のどのオペコードを実装して いるどの処理系内の C ⾔語関数が遅いのか 「なぜか echo に 7 秒かかる」等の性能調査や処理系⾃体の性能 改善に使えそう