PHP性能提升最高超过30%的提案已通过—预加载

介绍

PHP一直使用操作码缓存(APC,Turck MMCache,Zend OpCache)。它们通过“ALMOST”实现了显着的性能提升,完全消除了PHP代码重新编译的开销。使用操作码缓存,文件将被编译一次(在使用它们的第一个请求上),然后存储在共享内存中。以下所有HTTP请求都使用共享内存中缓存的表示形式。

这个提议是关于上面提到的“ALMOST”。将文件存储在操作码缓存中消除了编译开销 - 仍然存在从缓存中获取文件和特定请求的上下文相关的成本。我们仍然需要检查源文件是否被修改,将类和函数的某些部分从共享内存缓存复制到进程内存等。值得注意的是,由于每个PHP文件都是完全独立于任何其他文件进行编译和缓存的,因此当我们将文件存储在操作码缓存中时,我们无法解析存储在不同文件中的类之间的依赖关系,并且必须在每个请求的运行时重新链接类依赖项。

该提案的灵感来自为Java HotSpot VM设计的“Class Data Sharing”技术。它的目标是为用户提供一些传统PHP模型所提供的灵活性,从而提高性能。在服务器启动时 - 在运行任何应用程序代码之前 - 我们可以将一组PHP文件加载到内存中 - 并使其内容“permanently available”到该服务器将服务的所有后续请求。这些文件中定义的所有函数和类将可用于开箱即用的请求,与内部实体完全相同(例如strlen()方法或Exception类)。通过这种方式,我们可以预加载整个或部分框架,甚至整个应用程序类库。它还允许引入用PHP编写的“内置”函数(类似于HHVM的sytemlib)。交换进来的灵活性将包括一旦服务器启动就不能更新这些文件(在文件系统上更新这些文件什么也做不了;应用这些更改需要服务器重新启动);而且,这种方法不会兼容托管多个应用程序的服务器,或应用程序的多个版本,因为相同的类名可以会有不同的实现,如果这样的代码库的类加载一个应用程序,它将与从其他应用程序加载的类出现冲突。

提案

只需一个新的php.ini指令 - opcache.preload即可控制预加载。使用此指令,我们将指定一个PHP文件 - 它将执行预加载任务。一旦加载,该文件就会被完全执行 - 并且可以通过包含它们或使用opcache_compile_file()函数来预加载其他文件。以前,我尝试使用丰富的DSL来指定要加载哪些文件,使用模式匹配等来忽略哪些文件,但后来意识到在PHP中编写预加载方案本身更简单,更灵活。

例如,以下脚本引入了一个辅助函数,并使用它来预加载整个Zend Framework。

PHP性能提升最高超过30%的提案已通过—预加载

PHP预加载提案-示例代码

如上所述,预加载的文件将永远缓存在opcache内存中。如果没有其他服务器重新启动,修改其相应的源文件将不起任何作用。这些文件中定义的所有函数和大多数类将永久加载到PHP的函数和类表中,并在将来的任何请求的上下文中永久可用。在预加载期间,PHP还解析了类依赖关系以及与父,接口和特征的链接。它还会删除不必要的包含并执行其他一些优化。

opcache_reset()不会重新加载预加载的文件。使用当前的opcache设计是不可能的,因为在重启期间,某些进程可能会使用它们,任何修改都可能导致崩溃。

扩展opcache_get_status以提供有关“preload_statistics”索引下的预加载函数,类和脚本的信息。

静态成员和静态变量

为了避免误解,很明显预加载不会改变静态类成员和静态变量的行为。它们的值不会重设请求边界。

预加载限制

只有没有未定义的父类、接口、特征和常量值的类才能被预加载。如果一个类不满足这个条件,它将作为相应PHP脚本的一部分存储在opcache SHM中,就像不进行预加载一样。此外,只有不嵌套在控制结构中的顶级实体(例如if()…)才可能被预加载。

在Windows上,也无法预加载从内部继承的类。 Windows ASLR和fork()的缺失不允许保证不同进程中内部类的相同地址。

实施细节

预加载作为opcache的一部分在另一个(已经提交的)补丁上实现,这个补丁引入了“不可变的”类和函数。他们假设不可变的部分只存储在共享内存中一次(对于所有进程),并且从不复制到进程内存中,但是变量部分是针对每个进程的。这个补丁引入了MAP_PTR指针数据结构,允许SHM中的指针处理内存。

向后不兼容的变化

预加载不影响任何功能,除非显式使用。但是,如果使用它,它可能会破坏一些应用程序行为,因为预加载的类和函数总是可用的,而function_exists()或class_exists()检查将返回TRUE,从而防止执行预期的代码路径。正如上面所提到的,如果服务器上有多个应用程序,不正确的使用也会导致失败。由于不同的应用程序(或同一应用程序的不同版本)在不同的文件中可能有相同的类/函数名,如果类的一个版本是预加载的——它将防止加载在不同文件中定义的该类的任何其他版本。

提案的PHP版本

PHP7.4

RFC影响

  • 预加载是作为opcache的一部分实现的
  • opcache.preload - 指定将在服务器启动时编译和执行的PHP脚本。

性能

在ZF1_HelloWorld (3620 req/sec vs 2650 req/sec)和ZF2Test (1300 req/sec vs 670 req/sec)的参考应用程序中,使用预加载而无需任何代码修改,我的速度提高了大约30%。然而,真实世界的收益将取决于代码的引导开销与代码的运行时之间的比率,而且可能会更低。这可能会为具有非常短的运行时(如微服务)的请求提供最显著的好处。

展望

  • 预加载可以在HHVM中用作systemlib,以在PHP中定义“标准”函数/类
  • 可以预编译预加载脚本并使用二进制格式(甚至可以是原生的.so或.dll)来加速服务器启动。
  • 与ext / FFI(危险扩展)一起使用时,我们可能只允许在预加载的PHP文件中使用FFI功能,但不能在常规PHP文件中使用FFI功能
  • 可以执行更积极的优化并为预加载的函数和类生成更好的JIT代码(类似于HHVM中的HHVM Repo权威模式)
  • 使用某种部署机制扩展预加载,更新预加载的bundle而不重启服务器

提议的投票表决

PHP性能提升最高超过30%的提案已通过—预加载

PHP预加载提案的表决结果

补丁和测试

RFS的拉取请求: https://github.com/php/php-src/pull/3538

参考

  • Java HotSpot Class Data Sharing
  • Class Data Sharing Presentation by Volker Simonis
  • Code Sharing among Virtual Machines
  • Repo Authoritative mode in HHVM
  • Immutable Classes implementation in PHP

相关推荐