Skip to content

Latest commit

 

History

History
327 lines (221 loc) · 29.2 KB

File metadata and controls

327 lines (221 loc) · 29.2 KB

STATUS

1. 現在の結論

OPcache の共有ユーザーキャッシュ本体、request-local lookup cache、SafeDirectCache、共有グラフの read-mostly fetch、copy-on-mutate、RequestOverStatic は実装済みです。

Linux 上では、今回対象にしている主要経路は CLI / FPM / ZTS / ZTS+FPM の focused 検証で確認済みです。さらに、非 ZTS の Linux build では OPcache JIT を有効にした class-blob focused 検証も CLI / FPM の両方で確認済みです。未確認として大きく残っているのは、Windows 実機での ZTS helper compile/run です。

Zend/bench.php 比較は、FPM 経路を外し、変更前 commit 360ec2a38cf と最新ワークツリーを NTS / ZTS の CLI で再計測しました。OPcache 無効、OPcache 有効、OPcache 有効 + user cache 128MB、OPcache + JIT、OPcache + JIT + user cache 128MB の 5 条件をそれぞれ 30 回ずつ実行しています。各 iteration ごとに 4 ビルド × 5 条件をすべて 1 サイクル回す interleave 方式で計測しました。Total mean の差分は、NTS CLI が +3.43% / +1.54% / +1.21% / +6.09% / +0.62%、ZTS CLI が +5.64% / +2.62% / +1.22% / -0.30% / +0.08% でした。

RFC 添付用に追加した HTTP benchmark app も、NTS php-fpm と ZTS FrankenPHP の両方で最新ワークツリーを使って再計測済みです。shared_graph / repeat_fetch / shared_graph_mutate では user cache が APCu より NTS php-fpm で 29.73% / 47.98% / 42.49%、ZTS FrankenPHP で 34.89% / 35.96% / 31.56% 速いことを確認しました。request_over_static / persisted は explicit APCu path より NTS php-fpm で 67.80%、ZTS FrankenPHP で 66.02% 速く、local 再構築比ではそれぞれ 93.40% / 93.17% の worker time 削減です。

2. 実装状態

共有ユーザーキャッシュ本体

実装済みです。共有メモリ上の user cache に対して、明示 API と request-local な lookup cache が揃っています。

公開 API は次です。

  • OPcache\cache_store()
  • OPcache\cache_store_array()
  • OPcache\cache_fetch()
  • OPcache\cache_delete()
  • OPcache\cache_delete_array()
  • OPcache\cache_clear()
  • OPcache\cache_atomic_increment()
  • OPcache\cache_atomic_decrement()
  • OPcache\user_cache_info()

request-local lookup cache も実装済みで、同一リクエスト中の repeated read を軽くしつつ、store / delete / clear 後には正しく無効化されます。別プロセス、別 worker、別スレッドが同じ共有キャッシュに対して書き込みを行った場合でも、次の fetch で shared mutation epoch の差分を検知して stale entry を捨てます。

SafeDirectCache と shared graph

実装済みです。

  • SafeDirectCache 対象の内部クラスと安全なユーザーサブクラスは direct path に乗る
  • serialize hook を上書きした subclass は安全のため serializer fallback に落ちる
  • 一般の user object graph は shared graph として保持され、read-mostly な fetch を行う
  • mutation が起きた時だけ request-local copy を作る copy-on-mutate で動作する

この性質は、plain user object と SafeDirectCache property を内包した nested graph の両方で確認済みです。

RequestOverStatic

実装済みです。class attribute 付きクラスでは class-wide blob を使い、legacy な per-slot 経路と使い分けています。

確認済みの要点は次です。

  • class key は request_over_static_class:ClassName 形式で保存される
  • static property と method static は同じ class snapshot に乗る
  • class-blob 対象クラスでは旧 per-slot key は作られない
  • method-level attribute は、その method の static variables だけを per-slot key に保存する
  • property-level nested array write は即時 publish される
  • property-level nested object write も即時 publish される
  • class-wide blob の property / method static は request end で publish される
  • shared graph array-root の再 publish は fresh block に組み立てるため、cached class blob 由来の string key を自己上書きしない

特に class-blob まわりでは、dynamic initializer を持つ method static が NULL sentinel を壊さずに復元されること、class / property / method attribute の scope が混線しないこと、JIT 有効時にも同じ class snapshot semantics を維持することを確認済みです。

3. キャッシュ経路ごとの整理

ユーザーキャッシュは、値の種類と使い方に応じて異なる経路に乗ります。ここをまとめておくことで、共有ユーザーキャッシュ側と RequestOverStatic 側の挙動を同じ文書で追えます。

経路 A: request-local lookup cache

同一リクエスト中に同じキーを繰り返し読む場合は、最初の fetch で作られた request-local entry を再利用します。これは request 内最適化であり、共有の実体そのものではありません。

主な性質は次です。

  • repeated read を軽くする
  • 共有メモリ側の探索と変換の繰り返しを減らす
  • 同一 request 内の store / delete / clear 後は無効化される
  • 別プロセス / 別 worker / 別スレッドの書き込み後でも、次の fetch で shared mutation epoch の差分により stale entry が破棄される

経路 B: SafeDirectCache fast path

DateTime 系や一部 SPL 系、および安全条件を満たすユーザーサブクラスは、汎用 serializer を経由しない direct path に乗ります。

主な性質は次です。

  • DateTime / DateTimeImmutable / DateTimeZone / DateInterval を direct encoding / decoding できる
  • ArrayObject / ArrayIterator / RecursiveArrayIterator / SplFixedArray 系も direct path に乗る
  • 安全条件を崩す独自 serialize hook がある subclass は serializer fallback に落ちる

経路 C: 共有グラフの read-mostly fetch

一般の user object graph は shared graph として保存され、fetch 直後の read は可能な限り cheap path に留まります。

主な性質は次です。

  • eager deep copy を避ける
  • 大きい payload を読むだけのケースでメモリ増加を抑える
  • plain user object graph では read 時のメモリ増加が小さいことを確認済み

経路 D: copy-on-mutate

shared graph を fetch した後に mutation が起きた場合だけ、request-local copy を作ります。

主な性質は次です。

  • read-mostly workload では複製を遅延できる
  • mutation が必要な時だけコピーコストを払う
  • plain user object と nested graph の両方で確認済み

経路 E: SafeDirectCache property を含む nested graph

ユーザー定義クラスが SafeDirectCache 対象オブジェクトをプロパティに持つ場合でも、外側 graph は read-mostly fetch と copy-on-mutate の規則に従います。

主な性質は次です。

  • 内側の serializer コストを抑えられる
  • 外側 graph 全体が即 eager copy されるわけではない
  • mutation 時だけローカルコピーが作られる

4. focused テストの現状

共有ユーザーキャッシュ側

共有ユーザーキャッシュ側では、CLI fork / FPM / Linux ZTS に分けて次を確認済みです。

ext/opcache/tests/fpm/user_cache_fpm_006.phpt は通常 build に加えて ZTS+FPM build でも通過しており、cross-worker lookup cache invalidation の slice は両 build で確認済みです。

FPM suite 001-006 の内訳は次です。

RequestOverStatic CLI

次が通過済みです。

ext/opcache/tests/request_over_static_007.phpt は今回追加した JIT gate です。class-blob、dynamic method static initializer、旧 per-slot key 不在、2 回目の request 変更が 3 回目 request に publish されることを、JIT runtime が on の状態で固定しています。

ext/opcache/tests/request_over_static_008.phpt は、opcache.user_cache_memory_consumption=0 の既定経路では RequestOverStatic が無効のまま振る舞うことを固定しています。

ext/opcache/tests/request_over_static_009.phpt は、cached class blob を読むだけの request では shutdown publish が走らないことを固定しています。これにより、read-only cached request で巨大な class blob を毎回 serialize/store し直す退行を検出できます。

ext/opcache/tests/request_over_static_010.phpt は、class / property / method attribute を同じ script 内で使い、class-wide blob、property-scoped key、method-scoped key が分離されることと、class blob の再 publish 後も shared graph array-root の string key が壊れないことを固定しています。

ext/opcache/tests/request_over_static_invalidate_001.phptext/opcache/tests/request_over_static_reset_001.phpt は、opcache_invalidate() / opcache_reset() が RequestOverStatic の reserved key を削除し、通常の明示 user cache entry は保持することを固定しています。

RequestOverStatic FPM

次が通過済みです。

ext/opcache/tests/fpm/request_over_static_fpm_004.phpt は property-scoped nested object write の即時 publish を固定しています。

ext/opcache/tests/fpm/request_over_static_fpm_005.phpt は property-scoped nested array write が FPM の同一リクエスト中でも即時 publish されることを固定しています。

ext/opcache/tests/fpm/request_over_static_fpm_006.phpt は FPM 側の JIT gate です。class-blob、dynamic method static initializer、旧 per-slot key 不在、worker restart をまたいだ publish を、JIT runtime が on の状態で固定しています。

ext/opcache/tests/fpm/request_over_static_fpm_007.phpt は FPM 2 worker の同時 request で reader / writer を同期し、別 worker から class-wide blob と property-scoped state が更新されても stale read や key corruption が起きないこと、method-level state が writer request shutdown 後に次 request へ publish されることを固定しています。

RequestOverStatic ZTS

ZTS build でも request_over_static_001-010 と ext/opcache/tests/request_over_static_zts_threads_001.phpt を含む focused suite は通過済みです。初回に ZTS helper 3 本が skip されたのはソース不整合ではなく embed static library が未生成だったためで、ZTS build 側で libphp.la / libphp.a を生成した後は、git status で見える OPcache user cache / RequestOverStatic 系 PHPT 42 本が 42 PASS / 0 SKIP / 0 FAIL で通過しています。

ext/opcache/tests/request_over_static_zts_threads_001.phpt は embed helper 上で reader thread / writer thread を同期し、class-wide blob、property-scoped state、method-scoped state が別 thread の request shutdown 後に同じ共有ユーザーキャッシュから一貫して読めることを固定しています。

RequestOverStatic ZTS + FPM

ZTS out-of-tree build に php-fpm を追加して rebuild した後、request_over_static_fpm_001-007 の suite が通過しています。

つまり、RequestOverStatic の FPM focused regression は、通常 build と ZTS build の両方で 001-007 まで確認済みです。

Linux の通常 build で常用する expanded regression bundle は、CLI の ext/opcache/tests/request_over_static_001.phpt から ext/opcache/tests/request_over_static_010.phpt、FPM の ext/opcache/tests/fpm/request_over_static_fpm_001.phpt から ext/opcache/tests/fpm/request_over_static_fpm_007.phpt、ZTS build では ext/opcache/tests/request_over_static_zts_threads_001.phpt を最小 gate とします。

方針としては、毎回すべての ext/opcache テストを JIT 付きで広げるのではなく、JIT 影響が最も大きい class-blob slice を 007 と 006 で固定し、既存 focused suite に追加する運用にします。

5. 高速化の観点で見る現状

高速化の観点で見ると、今回の実装は次の点に効いています。

  • 同一リクエスト内で同じキーを何度も読む場合は request-local lookup cache が効く
  • SafeDirectCache 対象の内部クラスや安全なユーザーサブクラスでは generic serializer を避けられる
  • 大きな user object graph を読んでいるだけの間は eager deep copy を避けられる
  • mutation が入る場合でも copy-on-mutate により複製コストを遅延できる
  • nested graph に SafeDirectCache property が入っていても、外側 graph の eager copy を避けられる

逆に、安全条件を崩す serialize hook を持つ subclass は fallback し、mutation が起きた後はローカルコピーのコストを払います。設計としては「常に最速」ではなく、「安全を壊さない範囲で fast path に乗せる」方針です。

加えて、ZTS の OPcache 有効時の回帰に対しては、RequestOverStatic 用 hook を常時有効にしないための hot-path 最適化を入れました。主な内容は、user cache memory が 0 の時点で hook 自体を登録しないこと、request ごとの active flag で static access と nested mutation の hook を絞ること、array/object mutation の mark と publish を統合して余分な indirection を減らすこと、class-blob 向け static access を request-local filter 経由にすることです。

6. NTS / ZTS CLI ベンチマーク

今回の Zend Engine 変更については、変更前を clean worktree の commit 360ec2a38cf、変更後を最新ワークツリーを参照する out-of-tree build として、NTS / ZTS の両方で CLI のみを再計測しました。FPM の Zend/bench.php 結果は実行経路差が読みづらいため、今回の集計から外しています。configure 条件は CC=clang、CXX=clang++、--disable-all、--enable-opcache、--enable-fpm、--enable-cli、--enable-embed=static、--enable-pcntl を共通にし、ZTS build のみ --enable-zts を追加しています。ただし、本節の測定で起動したのは sapi/cli/php のみです。

最新の再計測条件は次です。

  • 対象: Zend/bench.php
  • 実行単位: baseline / current、NTS / ZTS、CLI、OPcache 無効 / OPcache 有効 / OPcache 有効 + user cache 128MB / OPcache + JIT / OPcache + JIT + user cache 128MB の各組み合わせを 30 回ずつ
  • 実行順序: 各 iteration ごとに 4 ビルド (baseline NTS / current NTS / baseline ZTS / current ZTS) × 5 条件をすべて 1 サイクル回す interleave 方式
  • 主指標: Zend/bench.php が出力する各項目と Total
  • OPcache 無効: opcache.enable=0、opcache.enable_cli=0
  • OPcache 有効: opcache.enable=1、opcache.enable_cli=1、opcache.jit=0、opcache.jit_buffer_size=0、opcache.user_cache_memory_consumption=0
  • OPcache 有効 + user cache 128MB: OPcache 有効条件に opcache.user_cache_memory_consumption=128 を追加
  • OPcache + JIT: opcache.enable=1、opcache.enable_cli=1、opcache.jit=1255、opcache.jit_buffer_size=64M、opcache.user_cache_memory_consumption=0
  • OPcache + JIT + user cache 128MB: OPcache + JIT 条件に opcache.user_cache_memory_consumption=128 を追加
  • baseline commit では opcache.user_cache_memory_consumption は未実装のため ini_get() は false になり、current では 128 として有効になることを確認済み
  • 差分式: (current - baseline) / baseline * 100。負の値は current が速いことを表します
  • 各項目で baseline / current の双方が 0.000s に丸められた場合、本来 0.00% の差分ですが分母が 0 になるため計算不能となります。本表ではそのケースを「0.00%」として表記しています

Total の mean / median 集計は次です。

Thread SAPI Mode Baseline Mean Current Mean Mean Diff Baseline Median Current Median Median Diff
NTS CLI OPcache 無効 0.1952s 0.2019s +3.43% 0.1950s 0.2015s +3.33%
NTS CLI OPcache 有効 0.1323s 0.1344s +1.54% 0.1320s 0.1310s -0.76%
NTS CLI OPcache 有効 + user cache 128MB 0.1318s 0.1334s +1.21% 0.1320s 0.1320s +0.00%
NTS CLI OPcache + JIT 0.0433s 0.0459s +6.09% 0.0430s 0.0430s +0.00%
NTS CLI OPcache + JIT + user cache 128MB 0.0431s 0.0434s +0.62% 0.0430s 0.0430s +0.00%
ZTS CLI OPcache 無効 0.2049s 0.2165s +5.64% 0.2030s 0.2050s +0.99%
ZTS CLI OPcache 有効 0.1372s 0.1408s +2.62% 0.1360s 0.1380s +1.47%
ZTS CLI OPcache 有効 + user cache 128MB 0.1366s 0.1383s +1.22% 0.1360s 0.1380s +1.47%
ZTS CLI OPcache + JIT 0.0444s 0.0443s -0.30% 0.0440s 0.0440s +0.00%
ZTS CLI OPcache + JIT + user cache 128MB 0.0441s 0.0441s +0.08% 0.0440s 0.0440s +0.00%

解釈としては次です。

  • Total mean は NTS CLI で +0.62% から +6.09%、ZTS CLI で -0.30% から +5.64% の範囲でした
  • OPcache 有効 + user cache 128MB の Total mean は NTS CLI で +1.21%、ZTS CLI で +1.22% でした
  • OPcache + JIT + user cache 128MB の Total mean は NTS CLI で +0.62%、ZTS CLI で +0.08% でした
  • NTS CLI / OPcache + JIT の Total mean は +6.09% ですが、median は baseline / current ともに 0.0430s で +0.00% です
  • NTS CLI / OPcache 無効や ZTS CLI / OPcache 無効では、simple、simplecall、mandel などの短時間項目が Total mean に大きく影響しています
  • 各項目の率変動は、simple、simplecall、simpleucall、sieve など 0.000s 〜 0.003s 帯の項目で大きく出ます。表中の +0.00% は baseline / current の双方が同じ 0.001s 単位に丸められた条件、特に baseline / current ともに 0.000s に丸められて分母 0 となるケースも +0.00% として記載しています
  • FPM の Zend/bench.php 比較は今回の集計から除外しました。FPM の挙動確認は focused PHPT 側の結果として扱い、性能比較は CLI のみで見ます
  • したがって、今回の Zend Engine 変更で広い意味の大きな性能悪化は確認されず、user cache 128MB を有効にした条件でも Total は横ばいから小幅な揺れの範囲です

HTTP benchmark app (NTS php-fpm / ZTS FrankenPHP)

RFC 添付用に追加した OPcache_UserCache_Benchmark では、large shared graph / repeated fetch / copy-on-mutate / RequestOverStatic を HTTP 経路で測定しました。NTS 側は php-fpm 1 worker + nginx、ZTS 側は php/frankenphp main の FrankenPHP 1 thread + Caddyfile で実行しています。NTS の結果は results/summary-20260428T120617Z.tsv、results/comparison-20260428T120617Z.tsv、results/apcu-comparison-20260428T120617Z.tsv、ZTS FrankenPHP の結果は results/summary-20260428T121324Z.tsv、results/comparison-20260428T121324Z.tsv、results/apcu-comparison-20260428T121324Z.tsv に保存しています。

mean worker_us は次です。

Case Backend NTS php-fpm ZTS FrankenPHP
shared_graph build 1491.63 1649.97
shared_graph serialize 3423.57 3640.87
shared_graph apcu 2802.33 3172.98
shared_graph user_cache 1969.12 2066.07
repeat_fetch serialize 104457.15 94200.90
repeat_fetch apcu 95774.78 85251.33
repeat_fetch user_cache 49822.87 54593.40
shared_graph_mutate serialize 2896.00 3067.93
shared_graph_mutate apcu 3567.10 2832.62
shared_graph_mutate user_cache 2051.27 1938.53
request_over_static local 42046.28 40759.22
request_over_static apcu 8620.87 8187.08
request_over_static persisted 2776.18 2782.18

主要な比較結果は次です。

  • shared_graph の user_cache vs apcu は NTS で -29.73%、ZTS FrankenPHP で -34.89%
  • repeat_fetch の user_cache vs apcu は NTS で -47.98%、ZTS FrankenPHP で -35.96%
  • shared_graph_mutate の user_cache vs apcu は NTS で -42.49%、ZTS FrankenPHP で -31.56%
  • request_over_static の persisted vs local は NTS で -93.40%、ZTS FrankenPHP で -93.17%
  • request_over_static の persisted vs apcu は NTS で -67.80%、ZTS FrankenPHP で -66.02%

解釈としては、shared graph 系 workload では user cache の request-local lookup cache と shared graph read-mostly path が HTTP 経路でも APCu より優位に出ています。request_over_static についても、class-wide blob の array root を shared graph に乗せ、read-only cached request の再 publish を避ける経路で、明示的な apcu_fetch()/apcu_store() path より NTS / ZTS の両方で明確に速くなっています。

7. テスト基盤の現状

ZTS helper は Zend 側の platform shim を使う構成に切り替え済みです。

  • thread lifecycle は Zend/zend_thread.h 経由で扱う
  • POSIX と Windows の差分は shim 側で吸収する
  • PHPT harness は Unix の libphp.a だけでなく Windows の php*embed.lib も探索する

したがって、Windows では helper 基盤が未実装なのではなく、最後の実機 compile/run だけが未確認です。

8. 実務上の注意点

今回の検証で再確認した注意点は次です。

  • stale binary は実装バグと同じ見え方をする
    • class key や property key が見つからない failure は build 遅れでも起きる
  • out-of-tree の ZTS build には php-fpm が入っていないことがある
    • その場合は ZTS build 側を --enable-fpm 付きで再 configure して rebuild する必要がある
  • RequestOverStatic の attribute target 制約は class / property / method で維持される
    • free function attribute ではなく、class attribute / property attribute / method attribute を前提にテストを書く必要がある

9. 残課題

現時点の残課題は次です。

  1. Windows 実機で ZTS helper の compile/run を確認すること
  2. Windows 実機が用意できた段階で、今回追加した JIT gate を同条件で追試すること
  3. php/frankenphp 側の build option と APCu 用の export-dynamic link 条件を継続的に確認すること

10. 現在の評価

共有ユーザーキャッシュ全体としては、fast path / fallback / copy-on-mutate / RequestOverStatic の主要機能が揃っており、Linux 上では必要な focused 挙動まで確認済みです。性能面では、clean rebuild した NTS / ZTS の CLI で Zend/bench.php を OPcache 無効、OPcache 有効、OPcache 有効 + user cache 128MB、OPcache + JIT、OPcache + JIT + user cache 128MB の 5 条件それぞれ 30 回ずつ比較しました。各 iteration ごとに 4 ビルド × 5 条件をすべて 1 サイクル回す interleave 方式で計測しています。Total mean は NTS CLI で +0.62% から +6.09%、ZTS CLI で -0.30% から +5.64%、median は NTS CLI で -0.76% から +3.33%、ZTS CLI で +0.00% から +1.47% に収まりました。OPcache 有効 + user cache 128MB は NTS CLI で +1.21%、ZTS CLI で +1.22%、OPcache + JIT + user cache 128MB は NTS CLI で +0.62%、ZTS CLI で +0.08% です。FPM の Zend/bench.php 比較は今回の評価対象から外し、FPM の確認は focused PHPT 側の結果として扱います。

HTTP benchmark app でも、shared_graph / repeat_fetch / shared_graph_mutate では user cache が APCu より一貫して速く、NTS php-fpm で 29.73% から 47.98%、ZTS FrankenPHP で 31.56% から 35.96% の改善が出ました。RequestOverStatic persisted も synthetic route metadata compile workload で APCu を上回り、NTS php-fpm で -67.80%、ZTS FrankenPHP で -66.02% の worker time 削減、local 再構築比では NTS で -93.40%、ZTS で -93.17% の削減を確認しています。

RequestOverStatic の class-wide blob 対応は focused scope では完成状態に近く、Linux の通常 build では JIT gate まで揃いました。残りは大きくコード本体の不足ではなく、Windows 実機確認と必要に応じたクロスプラットフォーム追試です。