In PHP 7.4, support for preloading was added, a feature that can significantly improve code performance.
In a nutshell, this is how it works:
● In order to preload the file, you need to write a custom PHP script
● This script starts on the server Executed once
● All preloaded files are available in memory for all requests
● Changes to preloaded files will have no impact until the server is restarted
Let’s understand it in depth.
#Opcache
Although preloading is built on top of opcache, it is not exactly the same. Opcache will take your PHP source files, compile them into "opcodes", and then store these compiled files on disk.
You can think of opcodes as low-level representations of code that are easily interpreted at runtime. Therefore, opcache skips the conversion step between the source file and what the PHP interpreter actually needs at runtime. A huge victory!
But we have more to gain. Opcached files don't know about other files. If class A extends from class B, they still need to be linked together at runtime. Additionally, opcache performs a check to see if the source file has been modified and will invalidate its cache based on this.
So, this is where preloading comes into play: it not only compiles source files into opcodes, but also links related classes, traits, and interfaces together. It then saves this "compiled" blob of runnable code (ie: code that the PHP interpreter can use) in memory.
Now when a request arrives at the server, it can use parts of the code base that are already loaded into memory without incurring any overhead.
So, what do we mean by "part of the code base"?
# Preloading in practice
In order to preload , the developer must tell the server which files to load. This was done with a simple PHP script and there was really nothing difficult about it.
The rules are simple:
● You provide a preload script and link it into your php.ini file using the opcache.preload command.
● Every PHP file you want to preload should be passed to opcache_compile_file(), or only once in the preloading script.
Suppose you want to preload a framework, such as Laravel. Your script must go through all the PHP files in the vendor/laravel directory and add them one after the other.
Link to this script in php.ini as follows:
opcache.preload=/path/to/project/preload.php
This is a dummy implementation:
$files = /* An array of files you want to preload */; foreach ($files as $file) { opcache_compile_file($file); }
# Warning: Unable to preload Linked classes
and so on, there is a warning! In order to preload files, their dependencies (interfaces, traits and parent classes) must also be preloaded.
If there are any issues with class dependencies, you will be notified when the server starts:
Can't preload unlinked class Illuminate\Database\Query\JoinClause: Unknown parent Illuminate\Database\Query\Builder
See, opcache_compile_file() will parse a file but not execute it. This means that if a class has dependencies that are not preloaded, it cannot be preloaded itself.
This is not a fatal problem and your server can work normally. But you won't get all the preloaded files you want.
Fortunately, there is a way to ensure that the linked file is also loaded: you can use require_once instead of opcache_compile_file and let the registered autoloader (probably composer's) take care of the rest.
$files = /* All files in eg. vendor/laravel */; foreach ($files as $file) { require_once($file); }
There are still some things to note. For example, if you try to preload Laravel, some classes in the framework depend on other classes that don't exist yet. For example, the filesystem cache class \lighting\filesystem\cache depends on \League\Flysystem\Cached\Storage\AbstractCache, and if you have never used filesystem cache, you may not be able to install it into your project.
You may encounter "class not found" errors when trying to preload everything. Fortunately, in a default Laravel installation, there are only a few of these classes and can be easily ignored. For convenience, I wrote a little preloader class to make ignoring files easier, like this:
class Preloader { private array $ignores = []; private static int $count = 0; private array $paths; private array $fileMap; public function __construct(string ...$paths) { $this->paths = $paths; // We'll use composer's classmap // to easily find which classes to autoload, // based on their filename $classMap = require __DIR__ . '/vendor/composer/autoload_classmap.php'; $this->fileMap = array_flip($classMap); } public function paths(string ...$paths): Preloader { $this->paths = array_merge( $this->paths, $paths ); return $this; } public function ignore(string ...$names): Preloader { $this->ignores = array_merge( $this->ignores, $names ); return $this; } public function load(): void { // We'll loop over all registered paths // and load them one by one foreach ($this->paths as $path) { $this->loadPath(rtrim($path, '/')); } $count = self::$count; echo "[Preloader] Preloaded {$count} classes" . PHP_EOL; } private function loadPath(string $path): void { // If the current path is a directory, // we'll load all files in it if (is_dir($path)) { $this->loadDir($path); return; } // Otherwise we'll just load this one file $this->loadFile($path); } private function loadDir(string $path): void { $handle = opendir($path); // We'll loop over all files and directories // in the current path, // and load them one by one while ($file = readdir($handle)) { if (in_array($file, ['.', '..'])) { continue; } $this->loadPath("{$path}/{$file}"); } closedir($handle); } private function loadFile(string $path): void { // We resolve the classname from composer's autoload mapping $class = $this->fileMap[$path] ?? null; // And use it to make sure the class shouldn't be ignored if ($this->shouldIgnore($class)) { return; } // Finally we require the path, // causing all its dependencies to be loaded as well require_once($path); self::$count++; echo "[Preloader] Preloaded `{$class}`" . PHP_EOL; } private function shouldIgnore(?string $name): bool { if ($name === null) { return true; } foreach ($this->ignores as $ignore) { if (strpos($name, $ignore) === 0) { return true; } } return false; } }
By adding this class in the same preload script, we can now load like this The entire Laravel framework:
// … (new Preloader()) ->paths(__DIR__ . '/vendor/laravel') ->ignore( \Illuminate\Filesystem\Cache::class, \Illuminate\Log\LogManager::class, \Illuminate\Http\Testing\File::class, \Illuminate\Http\UploadedFile::class, \Illuminate\Support\Carbon::class, ) ->load();
# works?
This is of course the most important question: are all the files loaded correctly? You can test this simply by restarting the server and then dumping the output of opcache_get_status() into a PHP script . You'll see that it has a key called preload_statistics, which will list all preloaded functions, classes, and scripts; along with the memory consumed by the preloaded files.
# Composer Support
One promising feature could be the composer-based auto-preloading solution, which is already used by most modern PHP projects. People are working on adding a preload configuration option in composer.json which will generate the preload files for you! Currently, this feature is still under development, but you can follow it here.
#Server Requirements
There are two more important things to mention regarding devops when using preloading.
As you already know, you need to specify an entry in php.ini for preloading. This means that if you use shared hosting, you will not be able to configure PHP freely. In practice, you need a dedicated (virtual) server to be able to optimize preloaded files for individual projects. Remember this.
Also remember that you need to restart the server every time you need to reload the memory file (this is enough if using php-fpm). This may seem obvious to most people, but it's still worth mentioning.
#Performance
Now to the most important question: Does preloading really improve performance?
The answer is yes: Shared by Ben Morel Here are some benchmarks, which can be found in the same composer question linked previously.
Interestingly, you can decide to preload only "hot classes", which are classes that are frequently used in your code base. Ben's benchmarks show that loading only about 100 popular classes actually yields better performance gains than preloading all classes. This is the difference between a 13% and a 17% performance increase.
Of course, which classes should be preloaded depends on your specific project. It's wise to preload as much as possible at the beginning. If you do need a small percentage increase, you will have to monitor your code at runtime.
Of course, all of this work can be automated, and it may be possible in the future.
Now, the most important thing to remember is that composer will add support so that you don't have to make the preload files yourself, and it will be easy to set up this feature on your server as long as you have full control over it.
Translation: https://stitcher.io/blog/preloading-in-php-74
The above is the detailed content of Preloading (Opcache Preloading) in PHP 7.4. For more information, please follow other related articles on the PHP Chinese website!