扩展系统、OPcache 与 JIT:PHP 的扩展机制与性能优化
前置知识
- ›第 1–4 篇:完整的引擎知识体系(架构、类型、编译、VM)
- ›了解共享内存与进程间通信
- ›具备 JIT 编译的基础概念
扩展系统、OPcache 与 JIT:PHP 的扩展机制与性能优化
在前四篇文章中,我们从 SAPI 入口点出发,依次剖析了 PHP 的类型系统、编译流程和虚拟机。然而,仅凭 Zend Engine 本身还不足以构建一个完整的 Web 应用。正是扩展系统,将语言引擎升级为功能完备的运行时——连接数据库、HTTP、JSON、加密、文件系统以及开发者所需的一切。
在这最后一篇文章中,我们将研究 72 个以上内置扩展所共用的扩展 API,然后重点介绍三个在架构层面最具价值的子系统:OPcache(通过将编译结果缓存至共享内存来提升性能)、JIT 编译器(更进一步,直接生成原生机器码),以及 Fibers(为 PHP 引入协作式并发)。最后,我们还会介绍流 I/O 抽象层和 TSRM 线程安全层。
扩展 API
每一个 PHP 扩展——无论是 ext/ 目录下的内置扩展,还是通过 PECL 安装的扩展——都通过 Zend/zend_modules.h 中定义的 zend_module_entry 结构体向引擎注册自身:
classDiagram
class zend_module_entry {
+char *name
+zend_function_entry *functions
+module_startup_func MINIT
+module_shutdown_func MSHUTDOWN
+request_startup_func RINIT
+request_shutdown_func RSHUTDOWN
+info_func MINFO
+char *version
+globals_size
+globals_ctor GINIT
+globals_dtor GDTOR
+post_deactivate_func
+deps: zend_module_dep*
}
ext/json/json.c 中的 JSON 扩展是一个绝佳的极简示例。它的 module entry 注册了一个 MINIT 函数、一张函数表,以及用于 phpinfo() 输出的 MINFO 函数。五分钟就能读完,却完整展示了扩展的注册模式。
functions 字段指向一个 zend_function_entry 结构体数组,每个元素将一个 PHP 函数名映射到对应的 C 处理函数和参数信息。自 PHP 8.0 起,参数信息改由 .stub.php 文件生成——这些文件使用 PHP 语法声明函数签名,构建工具会将其转换为 _arginfo.h 头文件。例如,ext/json/json_arginfo.h 就是由 ext/json/json.stub.php 自动生成的。
提示: 开发新扩展时,最好的起点是复制一个简单的扩展(如
ext/json)再进行修改。stub 文件系统(*.stub.php→*_arginfo.h)消除了扩展开发中最容易出错的环节:手动编写参数信息结构体。
扩展的生命周期钩子
如第 1 篇所述,PHP 的生命周期分为模块级和请求级两个阶段。扩展通过 zend_module_entry 中的回调函数接入这些阶段:
flowchart TD
START["Process Start"] --> GINIT["GINIT<br/>Initialize extension globals struct<br/>(called once per thread in ZTS)"]
GINIT --> MINIT["MINIT<br/>Module initialization:<br/>register classes, constants,<br/>INI entries, resources"]
MINIT --> LOOP["Request Loop"]
LOOP --> RINIT["RINIT<br/>Per-request setup:<br/>reset counters, open connections"]
RINIT --> EXEC["Script Execution"]
EXEC --> RSHUTDOWN["RSHUTDOWN<br/>Per-request teardown:<br/>close connections, flush buffers"]
RSHUTDOWN --> POST["post_deactivate_func<br/>Late cleanup after output sent"]
POST --> LOOP
LOOP -->|"Process exit"| MSHUTDOWN["MSHUTDOWN<br/>Module teardown:<br/>unregister, free persistent memory"]
MSHUTDOWN --> GDTOR["GDTOR<br/>Destroy extension globals struct"]
GDTOR --> END["Process End"]
各阶段的职责划分对正确性至关重要:
- GINIT/GDTOR:初始化和销毁扩展的全局结构体。在 ZTS 构建中,每个线程各执行一次。全局结构体保存的是模块级状态,而非请求级状态。
- MINIT:注册所有跨请求持久存在的内容——类、函数、常量、INI 配置项。此处应使用
pemalloc()(持久分配),而不是emalloc()。 - RINIT:初始化请求级状态。例如,session 扩展就在这里打开 session 文件。
- RSHUTDOWN:清理请求级状态,在输出缓冲区刷新之前调用。
- MSHUTDOWN:清理模块级资源,释放持久分配,注销处理器。
注册内部函数
每一个用 C 实现的 PHP 内部函数,都通过 Zend/zend.h 中定义的 INTERNAL_FUNCTION_PARAMETERS 宏接收参数:
#define INTERNAL_FUNCTION_PARAMETERS zend_execute_data *execute_data, zval *return_value
所有内部函数的签名都是 void fn_name(zend_execute_data *execute_data, zval *return_value)。函数通过 zend_parse_parameters() 或更高效的 ZEND_PARSE_PARAMETERS_* 快速路径宏从 execute_data 中解析参数,并将返回值写入 return_value。
flowchart LR
STUB["json.stub.php<br/>PHP-syntax declarations:<br/>function json_encode(mixed $value, int $flags = 0): string|false"]
GEN["gen_stub.php<br/>Build-time generator"]
ARGINFO["json_arginfo.h<br/>Generated zend_function_entry[]<br/>+ ZEND_ARG_INFO structs"]
IMPL["json.c<br/>PHP_FUNCTION(json_encode)<br/>{ ... C implementation ... }"]
STUB --> GEN
GEN --> ARGINFO
ARGINFO --> |"Linked at compile"| IMPL
在 zend_function 联合体层面(如第 3 篇所述),内部函数使用的是 zend_internal_function 而非 zend_op_array。由于两者共用相同的公共头部,VM 可以统一处理——但在调度内部函数时,它会直接通过 zend_execute_internal 调用 C 处理函数,而不会进入 opcode 解释器。
OPcache:共享内存 Opcode 缓存
OPcache 是 PHP 最重要的性能扩展。没有它,每个请求都要重新编译所有 PHP 文件;有了它,编译后的 op_array 会被存入共享内存,供所有 worker 进程复用。
OPcache 的核心逻辑位于 ext/opcache/ZendAccelerator.c。在 MINIT 阶段,它将 zend_compile_file 函数指针(即第 4 篇介绍的可挂钩指针模式)替换为自己的 persistent_compile_file。拦截逻辑如下:
- 请求到来,PHP 调用
zend_compile_file("script.php") - OPcache 的替换函数检查:该文件是否已在共享内存缓存中?
- 缓存命中:直接返回指向已缓存
zend_op_array的指针,完全跳过编译。 - 缓存未命中:调用原始的
compile_file,再将结果持久化到共享内存。
flowchart TD
subgraph shm["Shared Memory (SHM)"]
direction TB
ALLOC["SHM Allocator<br/>(mmap / shm / posix)"]
STRINGS["Interned Strings Table<br/>(shared across processes)"]
CACHE["Opcode Cache<br/>filename → cached_script"]
CACHED["Cached Script:<br/>op_array, class_table,<br/>function_table"]
end
subgraph workers["FPM Worker Processes"]
W1["Worker 1"]
W2["Worker 2"]
W3["Worker 3"]
end
W1 -->|"Read-only access"| CACHE
W2 -->|"Read-only access"| CACHE
W3 -->|"Read-only access"| CACHE
W1 -->|"First compile"| CACHED
持久化层 ext/opcache/zend_persist.c 是其中最复杂的部分。op_array 内部包含大量指针——指向字符串、字面量、类条目、其他 op_array。将其复制到共享内存时,所有指针都必须调整为指向共享内存中的对应版本。字符串会被驻留到共享驻留字符串表中,嵌套结构(如类条目中的函数表)则会被递归持久化。
共享内存的分配由 ext/opcache/zend_shared_alloc.c 负责,支持多种后端:mmap(Linux 上的默认方式)、shm(System V 共享内存)和 posix(POSIX 共享内存)。分配区域的大小由 opcache.memory_consumption 控制,默认为 128MB。
OPcache 还支持预加载(opcache.preload):一个在服务器启动时执行一次的 PHP 脚本,将类和函数永久加载到共享内存中。预加载的代码既不会失效,也不会被重新编译,因此访问速度最快。
JIT 编译器
PHP 8.0 引入了 JIT(即时编译)编译器,可将热点 opcode 直接转译为原生机器码。JIT 内嵌于 OPcache 之中,并基于 SSA 优化器的类型推断结果(见第 4 篇)进行工作。
JIT 的入口位于 ext/opcache/jit/zend_jit.c,支持两种工作模式:
Function JIT 将整个函数编译为原生代码。当某个函数的执行次数超过阈值时,JIT 会对其进行编译,并将 op_array 的处理器指针修改为直接跳转到原生代码。
Tracing JIT(位于 ext/opcache/jit/zend_jit_trace.c)则更为精密。它记录执行轨迹——即穿越热点循环的线性 opcode 序列——并将这些轨迹编译为原生代码。轨迹可以跨越函数边界,通过内联函数调用来捕获真实的热点路径。
flowchart TD
OPCODE["Opcode execution"] --> COUNT{"Execution count<br/>> threshold?"}
COUNT -->|"No"| INTERP["Continue interpreting"]
COUNT -->|"Yes"| MODE{"JIT mode?"}
MODE -->|"Function JIT"| FJIT["Compile entire function<br/>to native code"]
MODE -->|"Tracing JIT"| RECORD["Record trace<br/>(linear opcode path)"]
RECORD --> TRACE_END{"Loop back or<br/>return?"}
TRACE_END -->|"Loop"| COMPILE["Compile trace"]
TRACE_END -->|"Side exit"| LINK["Link to other traces<br/>or back to interpreter"]
FJIT --> PATCH["Patch handler<br/>to native entry point"]
COMPILE --> PATCH
PATCH --> NATIVE["Execute native code<br/>Type guards inline"]
NATIVE --> DEOPT{"Type guard<br/>fails?"}
DEOPT -->|"Yes"| INTERP
DEOPT -->|"No"| NATIVE
原生代码的生成使用了 ext/opcache/jit/ir/ 目录下的 IR 框架。IR(中间表示)是一套自研的编译器框架,提供基于 SSA 的 IR 构建、优化 pass(常量折叠、复制传播、寄存器分配)以及面向 x86_64 和 AArch64 的代码生成能力。
ext/opcache/jit/zend_jit_ir.c 中的 JIT IR 流水线负责将 Zend opcode 转换为 IR 指令。类型守卫基于优化器的 SSA 类型推断插入——若某变量被推断为 IS_LONG,JIT 会在轨迹入口处生成类型检查,并在轨迹体内生成仅针对整数的算术指令。如果类型守卫在运行时失败,轨迹将反优化回解释器继续执行。
JIT 的行为由 opcache.jit(一个四位位掩码)和 opcache.jit_buffer_size 控制。PHP 8.4 默认启用 Tracing JIT。
提示: JIT 对 CPU 密集型代码(数学计算、数据处理)的提升最为显著。对于典型的 I/O 密集型 Web 应用,仅凭 OPcache 就能获得大部分性能收益。建议先做性能分析,再决定是否启用 JIT——它会占用额外内存,也可能增加启动时间。
Fibers:协作式并发
PHP 8.1 引入了 Fibers,用于实现协作式多任务。Fiber 是一个轻量级执行上下文,拥有独立的调用栈,可以被挂起和恢复。其实现位于 Zend/zend_fibers.h 和 Zend/zend_fibers.c。
sequenceDiagram
participant Main as Main Context
participant Fiber as Fiber Context
Main->>Fiber: $fiber->start()
Note over Fiber: Execute callback<br/>on Fiber's own C stack
Fiber->>Main: Fiber::suspend($value)
Note over Main: Context switch back<br/>$fiber->start() returns $value
Main->>Fiber: $fiber->resume($sent)
Note over Fiber: Fiber::suspend() returns $sent
Fiber->>Main: return $result
Note over Main: $fiber->getReturn() → $result
每个 Fiber 都拥有独立的 C 栈(由 zend_fiber_stack_allocate 分配,通常通过带有保护页的 mmap 分配约 512KB)。上下文切换通过保存和恢复 CPU 寄存器、切换栈指针来完成,使用的是平台相关的汇编代码(基于 boost.context 派生的实现,支持 x86_64、ARM64 等架构)。
Fibers 与 VM 的 execute_data 链深度集成。当 Fiber 挂起时,其 execute_data 链会被完整保留——所有 VM 帧、CV 槽和临时变量都留在 Fiber 的栈上。恢复后,VM 从中断处继续执行。
Fibers 的核心设计是协作式而非抢占式:Fiber 会持续运行,直到显式调用 Fiber::suspend()。这消除了对锁或原子操作的需求——同一时刻只有一个 Fiber 在执行。ReactPHP 和 Revolt 等库正是在底层借助 Fibers 实现了针对 I/O 密集型操作的 async/await 模式。
流:统一 I/O 抽象
PHP 的流层以 main/streams/streams.c 为核心,为所有 I/O 操作提供统一接口。当你调用 fopen()、file_get_contents() 或 fread() 时,底层都会经过流层。
classDiagram
class php_stream {
+ops: php_stream_ops*
+readbuf: char*
+readbuflen: size_t
+wrapper: php_stream_wrapper*
+context: php_stream_context*
}
class php_stream_ops {
+write(stream, buf, count) ssize_t
+read(stream, buf, count) ssize_t
+close(stream) int
+flush(stream) int
+seek(stream, offset, whence) int
+cast(stream, castas, ret) int
+stat(stream, ssb) int
+label: char*
}
class php_stream_wrapper {
+wops: php_stream_wrapper_ops*
+abstract: void*
+is_url: int
}
class php_stream_wrapper_ops {
+stream_opener()
+stream_closer()
+url_stat()
+dir_opener()
+unlink()
+rename()
+stream_mkdir()
+label: char*
}
php_stream --> php_stream_ops : ops
php_stream --> php_stream_wrapper : wrapper
php_stream_wrapper --> php_stream_wrapper_ops : wops
wrapper 系统让 fopen("http://example.com/file.txt") 这样的调用成为可能。每种 URL scheme 都由一个已注册的 wrapper 负责处理:
| Wrapper | Scheme | 实现文件 |
|---|---|---|
| 普通文件 | file:// |
main/streams/plain_wrapper.c |
| HTTP | http://, https:// |
ext/standard/http_fopen_wrapper.c |
| FTP | ftp:// |
ext/standard/ftp_fopen_wrapper.c |
| PHP | php://stdin, php://memory 等 |
ext/standard/php_fopen_wrapper.c |
| 压缩 | compress.zlib:// |
ext/zlib/zlib_fopen_wrapper.c |
| 用户空间 | 自定义 scheme | 通过 stream_wrapper_register() 注册 |
传输层(main/streams/xp_socket.c)负责处理 socket I/O,涵盖 TCP、UDP、Unix domain socket 以及 SSL/TLS 连接。stream_socket_client() 和 stream_socket_server() 函数都经由这一层实现。
TSRM:线程安全
TSRM/TSRM.h 和 TSRM/TSRM.c 中的线程安全资源管理器(TSRM)解决了一个根本性问题:PHP 引擎大量依赖全局状态(编译器全局变量、执行器全局变量、SAPI 全局变量、各扩展的全局变量),而在 ZTS(Zend Thread Safety)构建中,多个线程可能同时执行 PHP 代码。
TSRM 为所有全局状态提供线程本地存储。每个模块通过 ts_allocate_id() 申请一个"资源 ID"(即整数索引)。在运行时,全局访问宏通过 TSRM 进行解析:
| 构建类型 | EG(current_execute_data) 展开为 |
|---|---|
| 非 ZTS | executor_globals.current_execute_data — 直接访问结构体 |
| ZTS | ((zend_executor_globals*)tsrm_get_ls_cache())->current_execute_data — 线程本地查找 |
在非 ZTS 构建(即 FPM 的常见场景)中,TSRM 会被完全编译掉,宏直接展开为结构体访问,零开销。这也是为什么 PHP 发行版会分别提供 ZTS 和非 ZTS 两种构建——非 ZTS 构建的性能可以测量到明显更优。
以下场景需要 ZTS 构建:
- Windows IIS 使用多线程处理 PHP 请求
- 使用线程的事件驱动 SAPI
- 用于真正多线程的
parallel扩展
大多数生产环境的 PHP 部署使用非 ZTS FPM,通过进程级隔离实现"线程安全"——每个 worker 拥有独立的地址空间。
系列总结
在这五篇文章中,我们从 PHP 的顶层目录结构出发,逐一深入每个核心子系统:
- 架构与生命周期 — 四层架构、SAPI 契约、请求生命周期
- zval 与内存模型 — 16 字节的值表示、引用计数、COW、内存分配器与 GC
- 编译流程 — 词法分析器、解析器、AST、编译器、opcode 及标志系统
- 虚拟机 — 代码生成、调度模式、寄存器固定、优化器
- 扩展、OPcache 与 JIT — 扩展 API、共享内存缓存、原生代码生成、Fibers、流与 TSRM
php-src 的代码库体量庞大,但结构清晰。架构边界明确,命名规范统一,设计模式(可挂钩函数指针、生命周期钩子、函数指针 vtable)贯穿始终。一旦掌握了这些模式,即便面对陌生的代码角落,也能从容导航。
提示: 加深理解最好的方式,是挑选一个你日常使用的 PHP 函数——比如
array_map()或json_decode()——从头到尾读完它的实现。现在你已经具备了读懂每一行代码所需的全部背景知识。