Slide 1

Slide 1 text

PHP で実⾏中のスクリプトの動 作を下から覗き⾒る 五⼗嵐 進⼠ / @sji_ch

Slide 2

Slide 2 text

私は誰ですか 五⼗嵐 進⼠ スマートホンゲームのサーバサイドプログラマー ⼤体は PHP のコードを読み書きして過ごしてる

Slide 3

Slide 3 text

昨年娘ができた PHP カンファレンス仙台とかやりました PHP 歴は累計 7 年くらい 元は趣味で C ⾔語を少し

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Agenda php-pro ler の紹介 下からのぞき⾒るとは php-pro ler の実装 活⽤例 将来の展望

Slide 6

Slide 6 text

php-pro ler の紹介

Slide 7

Slide 7 text

php-pro ler とは 実⾏中の PHP スクリプトからコールトレースを抜き取るツール

Slide 8

Slide 8 text

元々 ruby で同様のことをやる rbspy という rust 製のプログラム がある https://github.com/rbspy/rbspy

Slide 9

Slide 9 text

rbspy をパクった phpspy という C ⾔語製のプログラムがある https://github.com/adsr/phpspy

Slide 10

Slide 10 text

この phpspy を更にパクった PHP 製のツールが php-pro ler https://github.com/sj-i/php-pro ler

Slide 11

Slide 11 text

デモ

Slide 12

Slide 12 text

下からのぞき⾒るとは︖

Slide 13

Slide 13 text

コンピュータシステムは階層的なシステム 物理的な⼟台に上物が乗っていくイメージ

Slide 14

Slide 14 text

階層の上のほうにある技術 = ⾼レイヤー 階層の下のほうにある技術 = 低レイヤー

Slide 15

Slide 15 text

PHP は PHP より低いレイヤーの仕組みで動いてる php-pro ler は PHP より低いレイヤーの仕組み越しに PHP スク リプトの挙動へアクセスするツール

Slide 16

Slide 16 text

前提知識

Slide 17

Slide 17 text

PHP でコードを書いている分には普通あまり意識しない前提知 識が必要

Slide 18

Slide 18 text

例えばコンピュータはなぜ動くのか https://www.amazon.co.jp/dp/4822281655

Slide 19

Slide 19 text

例えば Linux の仕組み https://www.amazon.co.jp/dp/B079YJS1J1/

Slide 20

Slide 20 text

CPU コンピュータの部品 電気信号をコンピュータを動かすための命令として解釈 様々な計算処理を⾏う 接続された他機器を操作するための電気信号を送る ⾃⾝への命令はメモリから読むこむ

Slide 21

Slide 21 text

メモリ それぞれ背番号を持ち 1 バイト分の情報を保持する領域の並び 16GB なら背番号 0 から 170 億番くらいまでの範囲 背番号を番地またはアドレスと呼ぶ 各番地の情報を取り出せる プログラム = CPU への命令はメモリへ読み込むことで CPU から 実⾏可能

Slide 22

Slide 22 text

OS のプロセス OS の機能により、コンピュータ上では複数のプログラムが同時 に動作(マルチタスク / マルチプロセス) OS 上で動作中のプログラムのことをプロセスと呼ぶ

Slide 23

Slide 23 text

プロセス間のメモリは隔離されてる 複数のプロセスがお互いに意図せぬ⼲渉をすると困る OS は各プロセスがそれぞれ固有のメモリを持つかのようにプロ セス間の環境を隔離 CPU の機能を利⽤して実現

Slide 24

Slide 24 text

プロセス間のメモリは隔離されてる 同じメモリアドレスでもプロセスが違えば異なる内容を持ち得る 別プロセスのメモリに普通の⽅法でアクセスすることはできない

Slide 25

Slide 25 text

PHP スクリプトの動作 PHP は⾼級⾔語 CPU によって直接解釈される命令列(機械語)ではない

Slide 26

Slide 26 text

機械語 機械語は CPU にプログラムを実⾏させるための命令列 同じ信号のパターンが別の CPU では違った意味を持つ 別の種類の CPU で同じプログラムを動かそうとした時、移植が 困難 ⼈間より機械の都合にあわせた⾔語

Slide 27

Slide 27 text

⾼級⾔語 より⼈間の都合にあわせた⾔語 何らかの形でプログラムにより機械語へ翻訳して使う 最初から翻訳が前提なので移植性が⾼い 型の保護などにより動作検証性やメンテナンス性を保ちながらプ ログラムを作れる 複数⼈でプロジェクトの⾼度な概念を共有するのにも使える

Slide 28

Slide 28 text

処理系 ある⾔語から別の⾔語への翻訳処理をコンパイルと呼ぶ プログラムをコンパイルまたは逐次解釈で実⾏するためのプログ ラムを、その⾔語の処理系と呼ぶ

Slide 29

Slide 29 text

⾼級⾔語で更に⾼級⾔語が作られる C は⾼級⾔語の中では機械語に近い ⼈間の都合に寄り切ってない C でより⾼級な⼈間の都合寄りの別の⾔語が作られている PHP 処理系は C で書かれている

Slide 30

Slide 30 text

PHP 処理系 PHP の処理系はある種の仮想マシン PHP スクリプトはまずメモリ上でこの仮想マシンのための機械 語命令列にコンパイル 仮想マシンのための機械語命令=オペコード 仮想マシンがこのオペコード列を実⾏

Slide 31

Slide 31 text

PHP 処理系の状態はメモリ上にある PHP 処理系の仮想マシンはプログラムであり、メモリ上に⾃⾝ の状態を持つ 今現在コンパイルされた命令列のどの位置を実⾏しているか 実⾏中の関数からの return 先の命令位置はどこか等

Slide 32

Slide 32 text

php-pro ler の実装

Slide 33

Slide 33 text

モチベーション スクリプトの性能計測やトラブルシュートの道具がほしい 処理系のプロセス外から処理系の状態を盗み⾒たい

Slide 34

Slide 34 text

なぜプロセス外から⾒たいか xdebug や xhprof はフック型プロファイラ フック型では計測対象の実⾏性能に影響を与えてしまう

Slide 35

Slide 35 text

なぜプロセス外から⾒たいか 別プロセスから勝⼿に覗き込むなら、CPU コアが余ってれば性 能劣化は回避可能 何か間違ってもツールの⽅が落ちるだけ 調査対象への影響が少なければ本番環境での利⽤も視野に⼊って くる 調査対象プロセス暴⾛時の原因調査にも使える

Slide 36

Slide 36 text

基本的アイディア PHP スクリプトは仮想マシンの状態を変化させながら動作 仮想マシンの状態は処理系が動作するプロセス⽤のメモリ領域に 保存 以下 2 つの材料が揃えば覗き⾒できる 別プロセスの指定番地のメモリ内容を覗き⾒る⽅法 別プロセスのメモリ内のどの番地に何のデータがあるかを知 る⽅法

Slide 37

Slide 37 text

コールトレース ⼀⼝に処理系の状態といっても⾊々ある 今回欲しいのはコールトレース 実⾏中の関数がどの関数から呼ばれて、更にその関数がどの関数 から呼ばれて、という奴

Slide 38

Slide 38 text

なぜコールトレースか よくあるプログラムは関数呼び出しの繰り返しで構成 ある関数が別の関数を呼び出して、終わったら次の関数を呼び出 して、と進んでいく

Slide 39

Slide 39 text

コールトレースのサンプリング 「今何を実⾏中か」のコールトレースを、⼤体等間隔でサンプリ ングして取る よく現れる関数は処理時間が⻑かったり頻繁に呼び出されたり で、多くの時間を使っている確率が⾼い 性能問題やシステムが無応答となるような問題の原因を突き⽌め やすくなる

Slide 40

Slide 40 text

FFI で PHP からシステムコールを呼ぶ

Slide 41

Slide 41 text

FFI 去年リリースされた PHP 7.4 の機能 Foreign Function Interface の略 PHP から他の⾔語で作られた関数を呼び出すための機能

Slide 42

Slide 42 text

FFI 導⼊以前 7.3 までの PHP は PHP 単体でできないことが多かった PHP Manual に載っている標準関数の範囲外は C の拡張機能を作 る or 導⼊する必要があった 実は標準関数⾃体も処理系に標準添付されている拡張という形で 実現されている PHP の拡張を作るには特殊な作法と⼀定以上の C ⾔語の知識が 必要

Slide 43

Slide 43 text

FFI 導⼊以降 FFI 以外の拡張を使わず C ⾔語資産を PHP から直接呼び出せる システムコールやメモリ操作の関数、処理系の内部関数もそのま ま呼べる 当然そのリスクも⼀緒に持ち込まれる C ⾔語 や PHP 処理系内部の深い知識、拡張を書く際の特有の作 法はいらなくなる しかもそういうコードを composer からインストールできる

Slide 44

Slide 44 text

FFI の使い⽅ $ffi = \FFI::cdef( 'int printf(const char * restrict format, ... );', // 'libc.so' /* libc は処理系とともに読み込まれているので不要 */ ); $ffi->printf('hello clang world');

Slide 45

Slide 45 text

process_vm_readv Linux のシステムコール プロセスのアドレス空間間でデータを転送する 対象プロセス ID と対象プロセス内でのメモリアドレス、データ サイズ、⾃プロセスのバッファを指定して呼ぶ 別プロセスのデータを⾃プロセスの指定バッファ内へコピー可能

Slide 46

Slide 46 text

process_vm_readv の使⽤条件 呼び出し元は以下を満たす必要がある CAP_SYS_PTRACE という特別な権限を持ったプロセス 対象プロセスと同ユーザ / グループのプロセス gdb でプロセスにアタッチする時と⼤体同じ権限があればいける 脳死で sudo しててもダイジョブ

Slide 47

Slide 47 text

/proc//maps

Slide 48

Slide 48 text

procfs Process Filesystem の略 /proc/ 下の擬似的なファイルとして、linux のプロセスに関する 様々な情報を取得できる ps コマンドなども procfs の内容を解釈して出⼒するような実装 https://gitlab.com/procps-ng/procps

Slide 49

Slide 49 text

/proc//maps pid のプロセス内で、どのファイルがどのメモリアドレスに読み 込まれている、というマッピング情報を提供するもの 使われている各実⾏ファイルや共有ライブラリのメモリ上での位 置が分かる PHP からはファイルとして開いて正規表現等でパース可能

Slide 50

Slide 50 text

/proc//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

Slide 51

Slide 51 text

ELF

Slide 52

Slide 52 text

ELF とは これではない

Slide 53

Slide 53 text

ELF とは プログラムコードを格納するファイル形式 Executable and Linkable Format の略 プログラムで使うための機械語コードやデータとシンボル情報な どの付加情報をまとめたもの

Slide 54

Slide 54 text

シンボル情報 関数や変数の名前からファイル内への位置を引っ張り出すための 索引情報

Slide 55

Slide 55 text

分割コンパイルとリンク C ⾔語などでは普通コードをファイルごとに分割でコンパイル 分割されたコード断⽚を⼀つの実⾏可能ファイルへリンク リンクの際には各ファイルの何バイト⽬にどの関数がある、とい った情報が必要(この際にシンボル情報が使われる)

Slide 56

Slide 56 text

動的リンク ディスクやメモリ容量削減のため、リンクのタイミングは実⾏の 直前まで後回しにできる 実⾏時リンクとか動的リンクと呼ばれる この際に実⾏可能ファイルと実⾏時にリンクされるのが、共有ラ イブラリとか共有オブジェクトと呼ばれるファイル Windows では .dll、Linux 等では .so とか呼ばれる

Slide 57

Slide 57 text

実⾏可能ファイルのシンボル情報 ⼀度完全にリンクされればシンボル情報は不要となる 容量削減の観点からファイルから削除することも可能(strip) 実際には実⾏時リンクのため、⼀部のシンボル情報は実⾏可能フ ァイルになっても残しておく必要がある

Slide 58

Slide 58 text

PHP 処理系のシンボル情報 mod_php でも cli や fpm でも、拡張機能を共有オブジェクト / DLL で必要に応じてロードできる 拡張から PHP のコア機能にアクセスするためのシンボル情報は 処理系に付いてくる 処理系の ELF ファイルを解釈してシンボル情報を読み込めば、 これらの関数やデータがどの位置にあるかが分かる 拡張から可能なのに近いレベルで情報が取れる

Slide 59

Slide 59 text

PHP で ELF を読む php-pro ler では ELF パーサを⾃前で書いてみた PHP で書いたの俺以外に世界で 5 ⼈くらいしかいない説がある 案外普通に書けた

Slide 60

Slide 60 text

PHP でバイナリデータを読む ArrayAccess で添え字でバイト列から 1 バイト分の整数値を読み 込めるようにする interface ByteReaderInterface extends ArrayAccess

Slide 61

Slide 61 text

PHP の⽂字列はエンコード情報を持たないバイト列 添え字アクセスと ord() で 1 バイト分のデータを取り出す実 装が作れる final class StringByteReader implements ByteReaderInterface { use ByteReaderDisableWriteAccessTrait; public function offsetGet($offset): int { return ord($this->source[$offset]); }

Slide 62

Slide 62 text

このインターフェースを通じて 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]; }

Slide 63

Slide 63 text

PHP でのバイナリ読みで困ったところ PHP では符号なし 64 ビット整数が普通には扱えない int は 64 ビット版 PHP でも符号あり整数値 評価値が上限値である PHP_INT_MAX を超える式の値は、整数 同⼠の演算であっても oat へ暗黙キャスト 真⾯⽬に扱うと多倍超演算⽤の拡張である gmp を使うとか

Slide 64

Slide 64 text

今回は int で押し通してる 今回は敢えて PHP の int をそのまま使っている

Slide 65

Slide 65 text

int で⾜りないのはどんな時か ディスク上やメモリ上の ELF ファイルを読み込む上でどこに符 号なしの 64bit 整数値が使われるか 今回の⽤途では ELF 内でアドレスやサイズを保持する型を使 う時

Slide 66

Slide 66 text

Linux プロセスのメモリレイアウトを確認してみる x86-64 のみ考慮 ユーザ空間のメモリアドレス上限は符号あり整数値の範囲内にお さまる 符号なし 64bit 整数値を扱う必要が出るのはカーネル空間のメモ リアドレスを取り扱いたくなった時 https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt

Slide 67

Slide 67 text

PHP: Hypertext Preprocessor カーネル空間のメモリアドレスを PHP で扱おうとするのが間違 っている PHP が何の略だと思っているのか

Slide 68

Slide 68 text

ZendEngine

Slide 69

Slide 69 text

ZendEngine とは PHP の処理系のコアは ZendEngine と呼ばれている PHP 4 の頃に Zeev さんと Andi さんが処理系のコアを刷新 ⼆⼈の名前から Ze と nd をとって Zend Zend Technologies という会社が作られたりした これを⺟体に ZendFramework というフレームワークが作ら れたりもした

Slide 70

Slide 70 text

ZendEngine のコンパイラと仮想マシン ZendEngine は、雑に⼤きく分けると以下 2 つから成り⽴つ PHP スクリプトを仮想マシンのコードへ変換するコンパイラ それを実⾏する仮想マシン 仮想マシン部分は Executor とも呼ばれている

Slide 71

Slide 71 text

仮想マシンの状態 Executor Globals(EG) というグローバル変数の構造体に仮想マシ ンの状態が格納

Slide 72

Slide 72 text

zend_execute_data EG には zend_execute_data 型の current_execute_data というメ ンバがある 現在実⾏中の仮想マシン命令についての情報が含まれている 元はどの関数のコードから⽣成されたものか どの PHP スクリプトファイルの何⾏⽬から⽣成されたもの か prev_execute_data というメンバがある 同じ構造で関数の呼び出し元の情報が⼊ってる

Slide 73

Slide 73 text

ZendEngine の構造体を PHP で読む Executor Globals や zend_execute_data は C ⾔語の構造体 C ⾔語構造体を PHP 側で 1 バイトずつ解釈するコードを書くの は少し⾯倒 FFI の機能で型キャストがある process_vm_readv で得たデータへのポインタをキャストすれば FFI 経由でアクセス可能 PHP 処理系のソースから必要な構造体定義を抜き出して使う

Slide 74

Slide 74 text

ZendEngine の構造体を PHP で読む際の注意点(1) FFI は C ⾔語のマクロに対応してないので、展開しておく必要が ある

Slide 75

Slide 75 text

ZendEngine の構造体を PHP で読む際の注意点(2) 処理系のバージョンが違えば内部構造も変わるので、各 PHP バ ージョン⽤の定義が必要

Slide 76

Slide 76 text

ZendEngine の構造体を PHP で読む際の注意点(3) データ内でポインタのアドレスはコピー元プロセスでのアドレス を指している そのままだと FFI が無効なポインタをたどろうとして SEGV 構造体定義側でポインタを整数値型に変更 ポインタをたどる時は繰り返し process_vm_readv して⼿動でた どる

Slide 77

Slide 77 text

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/

Slide 78

Slide 78 text

活⽤例

Slide 79

Slide 79 text

スクリプトの⾼速化

Slide 80

Slide 80 text

The Computer Language Benchmarks 複数⾔語でおおむね等価な処理を書いて、実⾏性能をベンチマー ク https://benchmarksgame- team.pages.debian.net/benchmarksgame/index.html

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

書き直してみた⼈がいる ⽐較的モダンっぽく⾒えるバージョンに書き直した⼈が https://gist.github.com/Girgias/e21b57cff72b8d05be06883d98552

Slide 83

Slide 83 text

遅くなってしまった ⼿元のマシンで⼤体 174 秒ほど、遅くなってしまった PHP8 にして JIT コンパイラを使えば 85 秒 でも修正前なら JIT で 48 秒

Slide 84

Slide 84 text

プロファイルをとる php-pro ler でスクリプトの計測をとる 出⼒を少し調整、どの関数のどの⾏かだけでなく、どの仮想マシ ン命令を実⾏中かも出す php スクリプトなのでちょっとした修正が簡単 https://github.com/sj-i/php-pro ler/tree/experiment-opcode- tracer

Slide 85

Slide 85 text

ワンライナーで集計 結果をファイルに吐き、重複⾏をカウントし、カウントの順でソ ート time php ./n-body-test.php 50000000 sudo ./php-profiler inspector:trace -p 250359 >result cat result | sort | uniq -c | sort -nr

Slide 86

Slide 86 text

プロファイル結果 NBodySystem::advance() 内 130 〜 144 ⾏⽬あたりが重そう オペコードでは 60 番と 12 番、28 番と 82 番が頻出 https://gist.github.com/sj- i/d0cbc6c0baa414ffcd00be6840a9166f

Slide 87

Slide 87 text

頻出オペコード 60 番 ZEND_DO_FCALL は関数呼び出しの命令 ここからの関数呼び出し(NBodySystem::advance)が重い 12 番 ZEND_POW は累乗演算⼦の命令、妙に重い 28 番 ZEND_ASSIGN_OBJ_OP と 82 番 ZEND_FETCH_OBJ_R はオ ブジェクトのプロパティへのアクセス⽤命令

Slide 88

Slide 88 text

ZEND_POW の対応 累乗演算⼦で 2 乗している部分を普通の乗算に書き換え 修正前 修正後 $distance² = $dx**2 + $dy**2 + $dz**2; $distance² = $dx*$dx + $dy*$dy + $dz*$dz;

Slide 89

Slide 89 text

プロパティアクセスの対応(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;

Slide 90

Slide 90 text

プロパティアクセスの対応(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;

Slide 91

Slide 91 text

プロパティアクセスの対応(2) PHP の型宣⾔は実⾏時に型検査のコストがかかるので外す 修正前 public float $x; public float $y; public float $z; public float $vx; public float $vy; public float $vz; public float $mass;

Slide 92

Slide 92 text

プロパティアクセスの対応(2) PHP の型宣⾔は実⾏時に型検査のコストがかかるので外す 修正後 public $x; public $y; public $z; public $vx; public $vy; public $vz; public $mass;

Slide 93

Slide 93 text

最適化の効果 書き換え後のコードで JIT 有効で実⾏すると 29 秒に 85 秒 → 29 秒、約 3 倍⾼速化 3.6 秒の Node(V8) とはまだ 8 倍くらいの差 プロパティアクセスが遅めなの⾃体は JIT でも変わらず、PHP 8 の今後に期待

Slide 94

Slide 94 text

将来の展望

Slide 95

Slide 95 text

C ⾔語側までトレースを取る ptrace システムコールで対象プロセスの実⾏を⼀瞬⽌めれば CPU 的な状態も取得できる C プログラムのデバッグ⽤情報を格納する DWARF 形式を使う 両⽅使えば更に細かい C ⾔語側のトレースまで取れる筈 PHP コードのどの関数のどの⾏のどのオペコードを実装して いるどの処理系内の C ⾔語関数が遅いのか 「なぜか echo に 7 秒かかる」等の性能調査や処理系⾃体の性能 改善に使えそう

Slide 96

Slide 96 text

ELF DWARF 時間を⽌める能⼒(ptrace) 別世界(別プロセス)の記憶(メモリー) なろう系では︖︖︖

Slide 97

Slide 97 text

今回話していないこと

Slide 98

Slide 98 text

ZTS 対応のため ELF の TLS からデータを引っ張り出す闇の技 ext-parallel 経由でのマルチスレッド利⽤ preloading とマルチスレッド amphp での⾮同期処理 静的型検査と Promise JIT でのトレース取得性能

Slide 99

Slide 99 text

⼀部は PHP カンファレンス 2020 オンラインで話す予定

Slide 100

Slide 100 text

おしまい