Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

PHP と SAPI と ZendEngine3 と

4,387 views

Published on

PHPerKaigi2018

Published in: Technology
  • Be the first to comment

PHP と SAPI と ZendEngine3 と

  1. 1. PHP と SAPI と ZendEngine3 と 2018/03/09 PHPerKaigi 2018 do_aki
  2. 2. @do_aki @do_aki http://do-aki.net/
  3. 3. はじめに <?php declare(strict_types=1); function hello( string $name):void { echo "hello {$name}"; } hello('php'); PHP スクリプト PHP 実行環境 PHP プログラムの実行 hello php
  4. 4. はじめに <?php declare(strict_types=1); function hello( string $name):void { echo "hello {$name}"; } hello('php'); PHP スクリプト PHP 実行環境 PHP プログラムの実行 hello php トークする内容はここ
  5. 5. 目次 1. Server API (SAPI) 2. Zend Thread Safety (ZTS) 3. Zend Engine 3.1 Virtual Machine (PHP VM) 3.2 Memory Allocator & Garbage Collector
  6. 6. Server API (SAPI)
  7. 7. PHP プログラムの実行とは <?php declare(strict_types=1); function hello( string $name):void { echo "hello {$name}"; } hello('php'); PHP スクリプト (テキストファイル) PHP 実行環境 (ソフトウェア) 実行環境がなければ、 PHPスクリプトは ただのテキスト PHP プログラムの実行 hello php
  8. 8. Webサーバと組み合わせて実行 (Web Server起動 時にロード済み) Web Server run PHP Script HTTP Request Web Server 組み込み型 Web Server CGI Protocol run PHP Script HTTP Request CGI型 起動して実行 Web Server FastCGI Protocol run PHP Script HTTP Request FastCGI型 起動済み
  9. 9. PHP 単体で実行 Built-in WebServer (ex:php –S 127.0.0.1:3000) run PHP Script Webサーバと組み合わせずにスタンドアロンで動作 (Built-in Web Server は IO 多重化による、シングルプロセス実装なのでIO が Busy に なると並列処理できない。もし PHP Script 実行時にクラッシュすると、WebServer も巻 き込んで死ぬのであくまで開発用) HTTP Request Command Execution run PHP Script コマンド実行 (ex: php hoge.php) 起動して実行
  10. 10. 複数ある PHP の実体(バイナリ) 主なファイル名 説明 [SAPI] libphp7.so php7apache2_4.dll Apache httpd のモジュールとして実行される PHP(ラ イブラリ) いわゆる mod_php [apache2handler] php-cgi php-cgi.exe いわゆる CGI版 PHP。CGIプロトコルだけでなくFastCGI プロトコルも実装した PHP (実行ファイル) [cgi-fcgi] php-fpm 高負荷環境に対応したプロセス管理を備えた、FastCGI プロトコル実装の PHP (実行ファイル) [fpm-fcgi] php php.exe php コマンド。 built in web server もこれで起動で きる (実行ファイル) [cli] [cli-server] phpdbg phpdbg.exe PHP の組み込みデバッガ (実行ファイル) [phpdbg] libphp7.so php7embed.lib 他のアプリケーションに組み込んで PHP を実行すること ができるようになるライブラリ [embed] lsphp LiteSpeed の LSAPI を実装した PHP (実行ファイル) [litespeed]
  11. 11. SAPI と SAPI module PHP コア (Zend Engine) SAPI module SAPI Web Server or Shell apache2 handler or php-fpm or cli or etc… PHP Scri pt
  12. 12. mod_php の場合 PHP コア (Zend Engine) SAPI module SAPIApache httpd apache2 handler PHP Scri pt
  13. 13. php-fpm の場合 PHP コア (Zend Engine) SAPI module SAPI FastCGI supported Web Server (ex:nginx) fpm- fcgi PHP Scri pt
  14. 14. php コマンド の場合 PHP コア (Zend Engine) SAPI module SAPIShell cli PHP Scri pt
  15. 15. SAPI と SAPI module PHP コア (Zend Engine) SAPI module SAPI 様々な Web Server Web Server ごとの違 いを吸収 PHP Scri pt 異な る通 信方 式に 対応 PHPコアを起動するための ブートストラップでもある
  16. 16. Server API (SAPI) • Web サーバごとの違いを吸収して、様々な Webサーバで PHP 実行環境を利用可能にする仕組み • これを実装した SAPI module が PHP 実行環境のブートス トラップとなる – 以前は Web サーバごとに数多く存在していたが、現在は Web サーバ - アプリケーションサーバ間のプロトコルが共通化して きたため少数 – 現在は、ほとんど動作方式の違いでしかない – SAPI module のことを指して SAPI と呼ぶこともある
  17. 17. SAPI module (PHP7.2) • apache2handler (Apache 2.0 Handler) • cli (Command Line Interface) • cli-server (Built-in HTTP server) • phpdbg (phpdbg) • fpm-fcgi (FPM/FastCGI) • embed (PHP Embedded Library) • cgi-fcgi (CGI/FastCGI) • litespeed (LiteSpeed V6.11) ※括弧は pretty name
  18. 18. SAPI module (PHP5.6) apache2handler (Apache 2.0 Handler) / cli (Command Line Interface) / cli-server (Built-in HTTP server) / phpdbg (phpdbg) / fpm-fcgi (FPM/FastCGI) / embed (PHP Embedded Library) / cgi-fcgi (CGI/FastCGI) / litespeed (LiteSpeed V6.10) ↑ PHP7 にもある/↓ PHP7で公式リポジトリからは削除された aolserver (AOLserver) / apache (Apache) / apache_hooks (Apache1) / apache2filter (Apache 2.0 Filter) / isapi (IIS) / caudium (Caudium) / Continuity (Continuity Server Enterprise Edition) / milter (Sendmail Milter SAPI) / nsapi (NSAPI) / phttpd (PHTTPD) / pi3web (PI3WEB) / roxen (Roxen) / thttpd (thttpd) / tux (tux) / webjames (WebJames)
  19. 19. SAPI module の役割 • POSTやCookieデータの読み込み (read_post,read_cookie) • 環境変数の読み込み (getenv) • 出力制御 (ub_write,flush) • ヘッダ出力 (header_handler,send_headers, send_header) • 設定ファイル(php.ini)の扱い制御 – デフォルトパスの上書き (php_ini_path_override) – 読み込まない設定 (php_ini_ignore, php_ini_ignore_cwd) etc…
  20. 20. SAPI module の役割ではないこと • ネットワーク接続 – socket 接続 – DB 接続 • ファイル入出力 – ファイルシステムへのアクセス • プロセス間通信 – signal – pipe (shell_exec) 多くは Zend Engine のストリームAPI (一部は拡張が直接制御)
  21. 21. SAPI module による出力制御 <?php header("HTTP/1.0 418 I'm a tea pot"); echo "Hello World!"; Hello World! Hello World! • apache2handler (mod_php) の場合は apache httpd の関 数(ap_rwrite等) 呼び出し • fpm-fcgi (fpm) の場合は FastCGI protocol による socket への書き込み • cli の場合は write system call の呼び出し (header 出力は無視) PHP Script
  22. 22. 設定ファイル(php.ini) のデフォルトパス • SAPI module ごとに異なる – コンパイル時に指定できる • ディストリビューションのパッケージで提供されているもの はだいたい一貫してる • コンパイル済みのバイナリを利用する場合、それぞれ異なる php.ini ファイルを参照する可能性があるので注意 – cli と php-fpm とで違う php.ini を見ているとかありうる
  23. 23. 単一バイナリに 複数の SAPI module php(.exe) libphp.so php hoge.php php –S 127.0.0.1:3000 cli SAPI cli-server SAPI (--enable-cli) --enable-embed (unit SAPI) そのまま利用 一部利用して実装 [Any App] (embed SAPI)
  24. 24. 同じ SAPI module で異なる動作 php-cgi lsphp php-cgi hoge.php php-cgi –b 127.0.0.1:9000 CGI として実行 FastCGI (待ち受け) (--enable-cgi) --with-litespeed cgi-fcgi SAPI (内部的には別処理) lsphp hoge.php lsphp –b 127.0.0.1:3000 ファイルを指定して実行 litespeed SAPI (内部的には別処理) LSAPI Server mode
  25. 25. 利用している SAPI を知る方法 • (実行ファイルの場合) –v オプションを付けて実行 • php_sapi_name() の戻り値 あるいは PHP_SAPI 定数 – 実行しないとわからないけど確実 • phpinfo() の Server API 項目 – ただしこれは SAPI 名ではなく、SAPI の pretty name $ /usr/sbin/php-fpm -v PHP 7.2.2 (fpm-fcgi) (built: Feb 21 2018 08:31:14) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies with Zend OPcache v7.2.2, Copyright (c) 1999-2018, by Zend Technologies
  26. 26. SAPI まとめ • PHP 実行環境は同じバージョンでも複数の実体 (バ イナリ) がある • 実体の違いのほとんどは SAPI module の違い • SAPI module の違いによって、PHP Script の実行 の仕方や、出力方法等が異なる • ただし バイナリの違い !== SAPI の違い であるこ とに注意
  27. 27. Zend Thread Safety (ZTS)
  28. 28. Q. SAPI module によって 起動した PHPは次に何をする? A. リクエストを待つ リクエスト 受付中
  29. 29. Q. SAPI module によって 起動した PHP は次に何をする? A. リクエストを待つ リクエスト 受付中
  30. 30. PHP の life cycle SAPI Initialize PHP Module Initialize PHP Module Shutdown (Waiting) Request Initialize Request Shutdown PHP Script Execution SAPI Shutdown Request (実行する PHP File を指定) 基本的にすべてのリソースは、リクエスト終了 (Request Shutdown)時 にリセットされる
  31. 31. PHP の life cycle (前頁と同じ内容を別の書き方しただけ) SAPI Initialize SAPI Shutdown PHP module Initialize (MINIT) PHP module Shutdown (MSHUTDOWN) Request Initialize (RINIT) PHP Script Execution Request Initialize (RSHUTDOWN) Request Initialize (RINIT) PHP Script Execution Request Initialize (RSHUTDOWN) t i m e PHP の基本的 な実行モデル において、 一つのプロセ スで、同時に 複数のリクエ ストを処理す ることはでき ない
  32. 32. Prefork Model (Multi Process) SAPI Initialize SAPI Shutdown MINIT MSHUTDOWN RINIT PHP Script RSHUTDOWN RINIT PHP Script RSHUTDOWN MINIT MSHUTDOWN RINIT PHP Script RSHUTDOWN MINIT MSHUTDOWN RINIT PHP Script RSHUTDOWN ・・・ process リクエスト を並列処理 するために 複数のプロ セスを待機 させておく
  33. 33. Prefork Model (Multi Process) SAPI Initialize SAPI Shutdown MINIT MSHUTDOWN RINIT PHP Script RSHUTDOWN RINIT PHP Script RSHUTDOWN MINIT MSHUTDOWN RINIT PHP Script RSHUTDOWN MINIT MSHUTDOWN RINIT PHP Script RSHUTDOWN ・・・ process リクエスト を並列処理 するために 複数のプロ セスを待機 させておく 並行して起動する最大プロセス数 Apache httpd (mpm:prefork) : MaxClients/MaxRequestWorkers ディレクティブ cgi-fcgi (fcgi) : PHP_FCGI_CHILDREN 環境変数 php-fpm (1poolあたり) : pm.max_children 設定 Apache httpd (mpm:prefork) : MaxRequestsPerChild / MaxConnectionsPerChild ディレクティブ cgi-fcgi (fcgi) : PHP_FCGI_MAX_REQUESTS 環境変数 php-fpm : pm.max_requests 設定 1プロセスが処理する最大リクエスト数
  34. 34. 一部のWebサーバによる スレッドによるリクエスト処理に対応 (ZTS) SAPI Initialize SAPI Shutdown MINIT MSHUTDOWN RINIT PHP Script RSHUTDOWN RINIT PHP Script RSHUTDOWN RINIT PHP Script RSHUTDOWN RINIT PHP Script RSHUTDOWN thread 単一のプロセス に対し、異なる スレッドでリク エストを並列処 理することが可 能 (その代わり、 マルチスレッド 対応のコストが かかる)
  35. 35. ZTS (Zend Thread Safety) • Webサーバがスレッドでリクエストを処理するケースに備えて、 マルチスレッド環境下でもPHPの実行を可能にする特別対応のこと – ZTS を有効にしてコンパイル => TS(Thread Safe) 版 PHP – ZTS を無効にしてコンパイル => NTS (Non Thread Safe) 版 PHP • ZTS は PHP Script 上で マルチスレッドプログラミングをサ ポートするものではない – PHP Script 上での マルチスレッドはやめとけ (by pauli) • https://www.slideshare.net/jpauli/php-and-threads-zts#40 • TS版PHP でも、拡張が Thread Safe でないと破綻するので注意
  36. 36. ZTS が有効かどうかを知る方法 • php –v (CLI のみ!) • PHP_ZTS 定数 (int(1): 有効 / int(0): 無効) • phpinfo() の Thread Safety 項目 (enable / disable) $ php –v PHP 7.2.2 (cli) (built: Feb 21 2018 08:30:50) ( NTS ) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies with Zend OPcache v7.2.2, Copyright (c) 1999-2018, by Zend Technologies 有効な場合は (ZTS)
  37. 37. ZTS まとめ • PHP 実行環境は同じバージョンでも複数の実体があり、 さらに、TS版(ZTS有効) と NTS版(ZTS無効) がある • ZTS はスレッド化されたリクエストを処理するための機能 • PHP は 通常、 マルチプロセスによる並列化を想定している ので、特別な理由がない限りは NTS版を利用しよう • PHP Script での マルチスレッドプログラミングは避けた ほうが良い (ほかの言語使おう)
  38. 38. Zend Engine
  39. 39. PHP’s Outline Web Server or Shell SAPI module Zend Engine Extensions PHP Script OS
  40. 40. PHP 全体像 • SAPI module や Zend Engine が Extension とし て関数やクラスを登録 • PHP Script は登録された関数やクラスを介して、あ るいは、 Zend Engine が言語構造として提供する機 能を介してのみ、外の世界とつながることができる • 起点は常に SAPI module • この大枠は、PHP5(ZE2) から PHP7(ZE3) になって も変化していない
  41. 41. PHP の Source Code 階層 php-src |- TSRM/ .......... (Thread Safe Resource Manager) |- Zend/ .......... (Zend Engine 関連) |- ext/ .......... (各種拡張) |- opcache/ |- standard/ etc… |- main/ .......... (SAPI に共通するロジック群) |- sapi/ .......... (各種 SAPI module) |- apache2handler/ |- cli/ etc… |- win32/ .........(WIN32互換用)
  42. 42. Zend Directory の中身 (一部) PHP Script Compiler Lexer (zend_language_scanner.l) Parser (zend_language_parser.y) AST Generator (zend_ast.c) Opcode Generator (zend_compile.c) Memory Management Memory Allocator (zend_alloc.c) Cycles Collector (zend_gc.c) Stream API transport filter wrapper Virtual Machine Def (zend_vm_def.h,zend_vm_execute.skl) Gen (zend_vm_gen.php) Impl(zend_vm_execute.h,zend_execute.c,z end_execute_API.c,zend_vm_opcodes.c,zend_op code.c) Built-in Function (zend_builtin_functions.c) Class (zend_closures.c,zend_gener ators.c) Interface (zend_interfaces.c) Exception (zend_exceptions.c) Data Structures HashTable (zend_hash.c) zend_string (zend_interfaces.c) zend_list.c/zend_llist.c/zend_smart_str.c/zend_s mart_string.c/zend_stack.c/zend_ts_hash.c OOP zend_inheritance.c,zen d_objects.c,zend_objec t_handlers.c,zend_obje cts_API.c Algorithm Sort (zend_sort.c) ini File Parser zend_ini.c,zend_ini_parse r.y,zend_ini_scanner.l Signal Handling zend_signal.c Extension Management zend_extensions.c
  43. 43. Zend Engine とは • 主に Zend ディレクトリに含まれるPHPのコア機能 – これが Zend Engine である という定義は(たぶん)ない • PHP Script Compiler と PHP VM が主たる機能といえるだ ろうが、それは他の機能抜きには成り立たないもの – メモリ管理を行う Memory Manager / HashTable, zend_string といっ たデータ構造 / 設定ファイルを読み込むための ini File Parser – etc… • + PHP の基本機能 (を構成する要素) – 拡張管理 / PHP の組み込みクラス、関数、例外 / Stream API / Sort
  44. 44. Zend Engine 3 • 大枠は変わらず、個々の機能がブラッシュアップされた – Compiler, VM, Memory Manager,zend_string, HashTable, sort, object handler あたりの改良が主かなと • 最新が PHP 7.2 で Version 3.2 – PHP 7.0 で Version 3.0, PHP 7.1 で Version 3.1 – PHP 5 の時は Version 2.0 – 2.6 • C言語上で ZEND_VERSION 定数として定義(Zend/zend.h) – PHP Script からは zend_version() 関数で参照可能 • おそらく、バージョン番号にあまり意味はないのでは
  45. 45. Virtual Machine (PHP VM)
  46. 46. PHP Script は 中間コード に 変換されてから実行される <?php $name = "php"; echo "hello {$name}"; $_main: ; (lines=5, args=0, vars=1, tmps=2) ; (before optimizer) ; hello.php:1-5 L0 (3): ASSIGN CV0($name) string("php") L1 (4): NOP L2 (4): T2 = FAST_CONCAT string("hello ") CV0($name) L3 (4): ECHO T2 L4 (5): RETURN int(1) hello.php php -d opcache.opt_debug_level=0x10000 -d opcache.enable_cli hello.php 1行1行が opcode という中間コード (一連のopcode は op_array という塊で管理される)
  47. 47. phpdbg での出力例 (表現の仕方が異なるだけで同じもの) <?php $name = "php"; echo "hello {$name}"; function name: (null) L1-5 {main}() hello.php - 5 ops L3 #0 ASSIGN $name "php" L4 #1 NOP L4 #2 FAST_CONCAT "hello " $name ~1 L4 #3 ECHO ~1 L5 #4 RETURN<-1> 1 hello.php phpdbg -p* hello.php
  48. 48. PHP Compiler & VM PHP Script Opcode Request Output Compiler Lexing Parsing Compilation VM Execution [INCLUDE_OR_EVAL] requireやinclude, eval を実行すると、そ のタイミングでコンパイ ルされる (例えばオートロード時) PHP Script
  49. 49. 実行例 <?php echo 1; require 'f.php'; foo(); echo 5; <?php echo 2; function foo() { echo 4; } echo 3; run.php f.php
  50. 50. 実行例 <?php echo 1; require 'f.php'; foo(); echo 5; <?php echo 2; function foo() { echo 4; } echo 3; run.php f.php ECHO int(1) INCLUDE_OR_EVAL (require) string("f.php") INIT_FCALL_BY_NAME 0 string("foo") DO_FCALL_BY_NAME ECHO int(5) RETURN int(1) run.php
  51. 51. 実行例 <?php echo 1; require 'f.php'; foo(); echo 5; <?php echo 2; function foo() { echo 4; } echo 3; run.php f.php ECHO int(1) INCLUDE_OR_EVAL (require) string("f.php") INIT_FCALL_BY_NAME 0 string("foo") DO_FCALL_BY_NAME ECHO int(5) RETURN int(1) run.php
  52. 52. 実行例 <?php echo 1; require 'f.php'; foo(); echo 5; <?php echo 2; function foo() { echo 4; } echo 3; run.php f.php ECHO int(1) INCLUDE_OR_EVAL (require) string("f.php") INIT_FCALL_BY_NAME 0 string("foo") DO_FCALL_BY_NAME ECHO int(5) RETURN int(1) run.php
  53. 53. 実行例 <?php echo 1; require 'f.php'; foo(); echo 5; <?php echo 2; function foo() { echo 4; } echo 3; run.php f.php ECHO int(1) INCLUDE_OR_EVAL (require) string("f.php") INIT_FCALL_BY_NAME 0 string("foo") DO_FCALL_BY_NAME ECHO int(5) RETURN int(1) run.php ECHO int(2) NOP ECHO int(3) RETURN int(1) ECHO int(4) RETURN null f.php foo()
  54. 54. 実行例 <?php echo 1; require 'f.php'; foo(); echo 5; <?php echo 2; function foo() { echo 4; } echo 3; run.php f.php ECHO int(1) INCLUDE_OR_EVAL (require) string("f.php") INIT_FCALL_BY_NAME 0 string("foo") DO_FCALL_BY_NAME ECHO int(5) RETURN int(1) run.php ECHO int(2) NOP ECHO int(3) RETURN int(1) ECHO int(4) RETURN null f.php foo()
  55. 55. 実行例 <?php echo 1; require 'f.php'; foo(); echo 5; <?php echo 2; function foo() { echo 4; } echo 3; run.php f.php ECHO int(1) INCLUDE_OR_EVAL (require) string("f.php") INIT_FCALL_BY_NAME 0 string("foo") DO_FCALL_BY_NAME ECHO int(5) RETURN int(1) run.php ECHO int(2) NOP ECHO int(3) RETURN int(1) ECHO int(4) RETURN null f.php foo()
  56. 56. 実行例 <?php echo 1; require 'f.php'; foo(); echo 5; <?php echo 2; function foo() { echo 4; } echo 3; run.php f.php ECHO int(1) INCLUDE_OR_EVAL (require) string("f.php") INIT_FCALL_BY_NAME 0 string("foo") DO_FCALL_BY_NAME ECHO int(5) RETURN int(1) run.php ECHO int(2) NOP ECHO int(3) RETURN int(1) ECHO int(4) RETURN null f.php foo()
  57. 57. 実行例 <?php echo 1; require 'f.php'; foo(); echo 5; <?php echo 2; function foo() { echo 4; } echo 3; run.php f.php ECHO int(1) INCLUDE_OR_EVAL (require) string("f.php") INIT_FCALL_BY_NAME 0 string("foo") DO_FCALL_BY_NAME ECHO int(5) RETURN int(1) run.php ECHO int(2) NOP ECHO int(3) RETURN int(1) ECHO int(4) RETURN null f.php foo()
  58. 58. 実行例 <?php echo 1; require 'f.php'; foo(); echo 5; <?php echo 2; function foo() { echo 4; } echo 3; run.php f.php ECHO int(1) INCLUDE_OR_EVAL (require) string("f.php") INIT_FCALL_BY_NAME 0 string("foo") DO_FCALL_BY_NAME ECHO int(5) RETURN int(1) run.php ECHO int(2) NOP ECHO int(3) RETURN int(1) ECHO int(4) RETURN null f.php foo()
  59. 59. 実行例 <?php echo 1; require 'f.php'; foo(); echo 5; <?php echo 2; function foo() { echo 4; } echo 3; run.php f.php ECHO int(1) INCLUDE_OR_EVAL (require) string("f.php") INIT_FCALL_BY_NAME 0 string("foo") DO_FCALL_BY_NAME ECHO int(5) RETURN int(1) run.php ECHO int(2) NOP ECHO int(3) RETURN int(1) ECHO int(4) RETURN null f.php foo()
  60. 60. 実行例 <?php echo 1; require 'f.php'; foo(); echo 5; <?php echo 2; function foo() { echo 4; } echo 3; run.php f.php ECHO int(1) INCLUDE_OR_EVAL (require) string("f.php") INIT_FCALL_BY_NAME 0 string("foo") DO_FCALL_BY_NAME ECHO int(5) RETURN int(1) run.php ECHO int(2) NOP ECHO int(3) RETURN int(1) ECHO int(4) RETURN null f.php foo()
  61. 61. 実行例 <?php echo 1; require 'f.php'; foo(); echo 5; <?php echo 2; function foo() { echo 4; } echo 3; run.php f.php ECHO int(1) INCLUDE_OR_EVAL (require) string("f.php") INIT_FCALL_BY_NAME 0 string("foo") DO_FCALL_BY_NAME ECHO int(5) RETURN int(1) run.php ECHO int(2) NOP ECHO int(3) RETURN int(1) ECHO int(4) RETURN null f.php foo()
  62. 62. コンパイル時 と 実行時 • コンパイル時に解決されること – 定数計算 (PHP>=7.0) – ::class (クラスが存在するかどうか関係ない) – __LINE__ 等のマジック定数 (ただし trait 内の __CLASS__ は実行時に決定) – トップレベルの関数・クラス定義 (early binding) • 実行時に解決されること – その他すべて
  63. 63. early binding <?php func(); new Cls(); function func() {} class Cls {} 定義よりも前に利用可能 コンパイル後、実行直前にトップレベルに定義されたクラスや関数を利用可 能に(名前付け)する処理 (クラスが継承、あるいは interface を実装している場合、このタイミング でオートロードが走る)
  64. 64. early binding なし <?php func(); // Call to undefined function func() new Cls(); // Class 'Cls' not found if (1) { function func() {} class Cls {} } func(); // OK new Cls(); // OK 定義がトップレベルではないの で early binding されない
  65. 65. Compiler & VM PHP Script Opcode Request Output Compiler Lexing Parsing Compilation VM Execution [INCLUDE_OR_EVAL] requireやinclude, eval を実行すると、そ のタイミングでコンパイ ルされる (例えばオートロード時) PHP Script 通常リクエスト終 了時に破棄される (opcache はこれ を再利用)
  66. 66. opcode • PHP VM によって実行される命令セット • 3アドレスコード – 命令 + 引数1 + (引数2) + (戻り値格納先) • CV:PHP Script 上の変数 • CONST: 定数(リテラル等) • TMPVAR: PHPスクリプトには現れない一時的な変数 – ex: echo $a + $b + 3 の場合 • 命令の種類は増加傾向にある – PHP5.6 150命令 – PHP7.0 168命令 – PHP7.1 185命令 – PHP7.2 196命令 • count や get_class 等の関数が命令 (ZEND_COUNT, ZEND_GET_CLASS) となることで関数呼び出しのコスト減 ADD $a $b TMP1 ADD TMP1 3 TMP2 ECHO TMP2 (みたいな感じ) (とはいえ、opcode は 内部では unsigned char なので、増えても 255までだと思う)
  67. 67. Threading Model • threaded code と呼ばれるコードを生成する手段 – (multi-thread とは関係ない言葉であることに注意) – PHP においては opcode の処理の仕方 (分岐方式) • 4つの中から選択することができる (自前でPHPをコンパイルする場合) – SWITCH (シンプル、ただし遅い) – CALL (7.1 までのデフォルト) – GOTO (速い、ただし一部の環境では動かない?) – HYBRID (CALL をベースに GOTO も利用 7.2 からのデフォルト) HYBRID はマイクロベンチで1.5倍、一般的なアプリケーション でも数パーセント速度向上するとのこと
  68. 68. VM の生成方法 $ cd Zend $ php zend_vm_gen.php --with-vm-kind=GOTO zend_vm_opcodes.h generated successfully. zend_vm_opcodes.c generated successfully. zend_vm_execute.h generated successfully. zend_vm_execute.sklzend_vm_def.h VM の本体である zend_execute 関数が ある (.h だけど実装が含まれる) opcode の実装 テンプレート zend_vm_execute.h
  69. 69. zend_vm_def.h • 各 opcode の実装がここに記述されている – ex:ZEND_JMP • 実行する opcode の位置を変更するだけ – ex:ZEND_CLONE • object で __clone があれば呼び出し、なければ組み込みのコピー処 理を実行 • opcode によって実行コストが全く違う – 単純に "opcode が少ない" === "速い" というわけではない
  70. 70. ZEND_VM_INTERRUPT • 7.1 で導入された、割込み処理を低コストで実現する仕組み – opcode を処理するたびに EG(vm_interrupt) を確認 – 割込み処理は非同期で EG(vm_interrupt) を 1 にする – 割込みがあれば zend_interrupt_function や zend_timeout が実行される • signal や timeout (max_execution_time) で利用 • 割込み処理が早くなっただけでなく、タイムアウト時の挙動 が従来よりも安定している
  71. 71. Memory Allocator & Garbage Collector
  72. 72. PHP のメモリ管理 • PHP7になったときに大幅に変更 (高速化の主因) – zval (変数の内部表現) の扱いとメモリレイアウト変更 • よりCPUキャッシュに乗りやすく – アロケータ (Zend Memory Manager) の刷新 • 基本確保単位が変更 Segment:256KB(PHP5) -> Chunk:2MB(PHP7) • GC が 参照カウンタ + 循環参照コレクタ というの は変わらない
  73. 73. Zend Memory Manager (Zend/zend_alloc.c) • マネージャという名前だけど、実質的にはメモリアロケータ • 通常割り当てられたメモリはすべてリクエスト終了時に破棄 – ZMM により、リクエストを越えてリークする可能性はない – ただし、永続的な割り当ては管轄外(システムアロケータに委譲) • メモリ解放漏れのチェック機構 – PHP Script の組み方によってリークするケースは循環参照のみ – 拡張のメモリリークチェックに便利 • memory_limit の管理も仕事の一つ – get_memory_usage(false) は 要求されたメモリ量 – get_memory_usage(true) は 実際に確保したメモリ量
  74. 74. Zend Memory Manager (PHP7) <?php $str = "PHP Script"; emalloc() zend_mm_heap chunk chunk chunk zend_mm_mmap() mmap or VitualAlloc (>=2MB) page page page page OS chunk 単位で確保slot slot slot
  75. 75. Zend Memory Manager (PHP5) OS <?php $str = "PHP Script"; emalloc() zend_mm_heap segment segment segment win32 mmap_zeromalloc mmap_anon storage_handler (ZEND_MM_MEM_TYPEで変更可) (>=256KB / ZEND_MM_SEG_SIZEで変更可) segment 単位で確保 block block block
  76. 76. 廃止されたZMM関連の環境変数 (PHP5 では利用可能だったが PHP7では使えない) • ZEND_MM_SEG_SIZE – メモリ確保の基本単位(default:256KB) – ある程度大きくすることで、メモリのフラグメントが防げるはず • ZEND_MM_MEM_TYPE – storage handler の指定 – 指定が無効な場合は、有効な名前を列挙してプログラム終了 • ZEND_MM_COMPACT (WIN32環境にのみ影響) – メモリコンパクションを発動するサイズ (default:2MB) – mmap系ではコンパクション実装がダミーになっているので意味がない
  77. 77. ZMM関連の環境変数 (PHP7) • USE_ZEND_ALLOC – PHP5 でも利用可能 – 0 を指定することで、emallocがシステムアロケータをそのまま利用す ることになる – Zend Memory Manager が無効になるので、memory_limit 等も使え なくなる • USE_ZEND_ALLOC_HUGE_PAGES (PHP>=7.0.6) – MAP_HUGETLB マクロが有効な場合のみ利用可能 (実質的に!WIN32) – 0 以外を指定することで、mmap のフラグに MAP_HUGETLB を付与 – (メモリを大量に使うプログラムで) Kernel の Huge Page が有効な らば、TLB ミス軽減による高速化を期待できる
  78. 78. 参照カウント 解放 'php' (ref:1) $a 'php' (ref:2) $a $b 'php' (ref:1) $a $b 'php' (ref:0)$b $a = 'php'; $b = $a; unset($a); unset($b); 生成
  79. 79. 参照カウント(附記) • メモリレイアウト変更により PHP7 でカウントの仕方が一部変更になった が、参照カウント方式を利用していることは変わらない そのため、循環参照コレクタも変わらず存在している • 公式のドキュメント「参照カウント法の原理」 は PHP5 の内部をベースと した解説であることに注意 – http://php.net/manual/ja/features.gc.refcounting-basics.php – PHP7 においては、カウントの仕方が若干異なる • The secret of PHP7's Performance を読むとPHP5とPHP7との違いが分 かりやすい (https://www.slideshare.net/laruence/the-secret-of-php7s- performance#28 以降)
  80. 80. 循環参照 $a = new stdClass(); $b = new stdClass(); $a->r = $b; $b->r = $a; unset($a); unset($b); $a $b stdClass(ref:1) stdClass(ref:1) $a $b stdClass(ref:2) r stdClass(ref:2) r $a $b stdClass(ref:1) r stdClass(ref:2) r $b stdClass(ref:1) r stdClass(ref:1) r 解放されない
  81. 81. 循環参照コレクタ (Zend/zend_gc.c) • PHP + GC というコンテキストにおいて指すのはだいたいこれ – 参照カウントも GC の一つではあるのだけど、 PHP Script 上でメモ リリークする可能性があるのは循環参照のみだからだと思う • PHP Script 上で実行を制御可能 – gc_enable / gc_diable / gc_enabled / gc_collect_cycles – ちなみに gc_mm_caches (PHP>=7.0)は Zend Memory Manager にお いて空き領域を再利用するための関数 (循環参照コレクタとは無関係) • 循環参照コレクタの動作については y_uti さんの「PHPのGCの 話」が分かりやすい (https://www.slideshare.net/y-uti/php-gc)
  82. 82. 循環参照コレクタの改善 • PHP7.3 で導入予定 (master マージ済み) – https://github.com/php/php-src/pull/3165 • 大量に object がある場合のGC速度が大幅に改善された – composer 速くなるかも – 動作原理は大きく変わってない (と思う) – ちゃんとは追えてない。詳しい人誰か • ルートバッファ(リークする可能性のあるオブジェクトを記録したもの)の 最大数が(実質)撤廃 – 従来は 10,000 件を超えて記録できなくなるとリークしてた – 初期 16,384件 (128KB) から、倍々あるいは 1MB 単位で最大 1GB まで増加 – 双方向リンクドリストからベクタに (これが高速化の主因?)
  83. 83. まとめ • PHP プログラムの実行を支える仕組みの一部を解説しました – PHP7.0 以降もさらに改善が進んでいる • とはいえ、表面の解説をしただけで詳細はもっと深く、また この資料で触れてない仕組みもまだまだたくさんあります • これを機に PHP の内部に興味を持ってくれたら幸い
  84. 84. Any Question?
  85. 85. (blank)
  86. 86. Extensions (Appendix.1)
  87. 87. 関数は拡張に属している • PHP Script から呼び出し可能な組み込みの関数は、すべてなにか しらの拡張に属している – 拡張の一覧は get_loaded_extensions 関数で取得可能 – 指定した拡張に属する関数の一覧は get_extension_funcs 関数で取 得可能 • Zend Engine が組み込みで提供する拡張 – Core Extension (strlen, define 等 Zend/zend_builtin_functions.c) – Standard Extension (trim, constant 等 ext/standard/basic_functions.c) – 他、 date, pcre, reflection, spl も – (Core のみ特別に読み込んでいるが、この挙動はちょっと謎)
  88. 88. 2種類の拡張 • Zend Extension – Zend Engine を "拡張" する – PHP Extension としても動作可能 • PHP Extension – PHP Script を "拡張" する – 内部的には PHP Module という名称 • 内部的な違いであって、利用者が意識することは少ない – ini ファイルでの指定の仕方くらい • Zend Extension は "zend_extension=xxx.so" • PHP Extension は "extension=xxx.so"
  89. 89. 外から見分ける方法 ライブラリの読み込みに利用するシンボルの違いで判別可能 $ nm ―D opcache.so | grep zend_extension_entry 000000000026a700 D zend_extension_entry $ nm -D pdo.so | grep get_module 00000000000059b0 T get_module Zend Extension の場合は必ず zend_extension_entry シンボル が存在 (頭に _がつくケースもアリ) PHP Extension の場合は zend_extension_entry はなく、 get_module シンボルが存在 (頭に _がつくケースもアリ)
  90. 90. Compile (Appendix.2)
  91. 91. PHP Script <?php function hello ( $name ) { echo "HELLO $name" ; } hello ( "php" ) ;
  92. 92. 字句解析 (Zend/zend_language_scanner.l / re2c) <?php function hello ( $name ) { echo "HELLO $name" ; } hello ( "php" ) ; T_FUNCTION T_STRING ( ) { } T_ECHO T_ENCAPSED_AND _WHITESPACE ; T_STRING ( ) ; T_OPEN_TAG T_VARIABLE T_VARIABLE T_VARIABLE" " ひとつひとつがトークン (意味を持つ最小の単位)
  93. 93. 構文解析 (Zend/zend_language_parser.y / bison) T_FUNCTION T_STRING ( ) { } T_ECHO T_ENCAPSED_AND_WHITESPACE ; T_STRING ( ) ; T_OPEN_TAG T_VARIABLE T_VARIABLE function declaration function call
  94. 94. AST構築 (Zend/zend_ast.c) generated by https://dooakitestapp.hero kuapp.com/phpast/webapp/
  95. 95. Opcodeへの変換 (Zend/zend_compile.c) $_main: ; (lines=5, args=0, vars=0, tmps=1) ; (before optimizer) ; hello.php:1-10 L0 (3): NOP L1 (8): INIT_FCALL 1 112 string("hello") L2 (8): SEND_VAL string("php") 1 L3 (8): DO_UCALL L4 (10): RETURN int(1) hello: ; (lines=5, args=1, vars=1, tmps=1) ; (before optimizer) ; hello.php:3-6 L0 (3): CV0($name) = RECV 1 L1 (4): NOP L2 (4): T1 = FAST_CONCAT string("HELLO ") CV0($name) L3 (4): ECHO T1 L4 (6): RETURN null call hello() declare hello() $ php -d opcache.opt_debug_level=0x10000 -d opcache.enable_cli hello.php

×