How to make PHP 2000 times faster
We’ll discuss why PHP is an awesome programming language, how to make your applications faster in means of performance and development.
Why PHP is an awesome language?
- It’s reliable, — you install it and it works.
- It’s stable, — updating between major versions requires little changes in your code and brings joy seeing performance increase and new features, same is inherited by developers who make libraries for PHP, and solutions that you find on stack overflow.
- It’s designed for web, — built-in templates, fetching, regular expressions, JSON, XML, various DB and web server support and it had server-side rendering (SSR) before it was invented.
- It’s supported, — check the list of popular CMS — they are mostly built with PHP, see plugins on vendor websites: Shopify, Stripe, Twilio and other vendors have SDK for PHP.
- It’s feature rich, — it supports latest technologies: Docker, NoSQL, WebSockets, Multi-tenancy, Image processing, Foreign Function Interface, frameworks like Laravel, single language on frontend and backend, and more.
What about speed?
Why I didn’t mention that PHP is fast? Let’s first define what means fast for web development. With my experience, bottle neck in web applications is a database, so how fast does language respond doing nothing but saying “Hello, world!” with grammar errors is not the thing you should compare.
There are simple ways to make your web application work faster — purchasing more powerful server. Even if there are great hosting companies like HostZealot, which allow you to get a high-performant dedicated server at an affordable price, and companies like Cloudflare which allow you to save your server’s CPU and RAM by reducing load made by bots and crawlers, improvements in the application sources can bring you much more impressive results.
Among other things, your site performance is important for SEO and thus for your business revenue, so I have an article, describing how to pass Google’s PageSpeed asessment.
For me as a developer, time wasted on finding working solutions, debugging, and compilation is most important when selecting a favorite language.
So let’s see what we can improve in terms of rapid application development.
What’s better than frontend and backend?
The current trend of web application development is using frontend and backend applications. This is good from standpoint of splitting work between 2 programmers: one should know how to work with databases and third-party APIs effectively, another should know how to build fancy interfaces.
What’s wrong with this approach?
- When an application goes to the sustaining stage you star searching for a full-stack developer who needs to work as two people mentioned above.
- Building 2 applications and integrating them with API doesn’t bring you value, but from my experience it often brings threats: you have an API which by mistake might allow reading data from your database, which you don’t want to share, for example your competitors could grab your user emails if it’s not prohibited explicitly.
- Fancy Interface increases size of your application, your users wait a lot, and Google considered this in search ranking. I have experience optimizing node.js application that had 1.6 GB of libraries and had page load time over 6.5 seconds and took several minutes to compile.
- Node.js means compilation, and it is eating time of a developer. JavaScript was designed to run without compilation, but node.js brought old times back, and to compensate this and other problems it brought, a lots of things were invented: hot module replacement (HMR), server-side rendering (SSR), compiler written in Go, and so on, among they are not needed in another application design, they are often not stable.
Is there another solution that can bring more profit? Imagine you can write backend and frontend parts in a single application and even in a same language, yet having modern interactive application. This is what Laravel Livewire framework lets you to have.
Laravel is not the only framework that allows this approach when creating full-stack applications. If you are open-minded enough, try Elixir programming language with its Phoenix framework and LiveView templates.
When language speed is important?
If your application uses math intensively, for example it’s image or video processing app, compressing or encryption algorithm, 3D, cryptocurrency or AI application, you are definitely interested in a language that allows building an application that uses CPU and RAM most efficiently.
Here PHP is not good at, it takes 40 bytes to store one integer in an array, and it makes algorithms like mentioned above work extremely slow.
The obvious solution here is choosing another language.
You definitely don’t want to write a more or less complex and sustainable application in Assembler, but which options do you have? Fortran is fast, but it’s very niche language, C++ is great, but it’s sometimes too powerful and it’s very simple to make memory leak with it. From fast modern languages there are Rust, made in Mozilla and Go made in Google, where Rust wins in calculating fractals.
How to make PHP be good at math?
“But wait, you said you can make PHP run faster!” — you’ll say.
Yes and no. We will make a PHP application run fast by making it execute CPU/RAM intensive operations using a library, written in a language that fits for that better.
There are various options of doing so:
- You can write an app and execute it using shell_exec() function, which is suitable if you need to run something large once.
- You can write a PHP extension and include it into PHP, which is great if you want to provide your functionality for the community.
- Use Foreign Function Interface (FFI) to include a library (.so for Linux, .dll for Windows, or .dylib for MacOS).
Foreign Function Interface (FFI)
FFI is what made Python so popular. It allows including any library into your application and get its functionality available. For example, to create a desktop application in Python you can use PyQt library, which is actually not a library itself, its a binding to Qt library.
In PHP this feature was introduced in PHP 7.4 in the end of 2018.
I believe you already got what are benefits of using FFI comparing to waiting for PHP extension to be created by a library vendor. It’s not very much more complicated comparing to writing an own PHP extension due to great PHP documentation and community, but it’s still psychologically simpler.
Live example
A live example is included so you can do it yourself and get your performance boost.
Preparing infrastructure
You can’t imagine working on web applications without using Docker, like I do, right? :)
We will not install PHP or Rust on your machine, instead, we’re going to use simplest way to run PHP in Docker, docker-compose and official PHP+Apache docker image.
Create docker-compose.yml file in your project folder with following content:
version: '3.5'
services:
php:
container_name: php_2000x_faster
build:
context: .
dockerfile: Dockerfile
ports:
- 5080:80
volumes:
- './app:/var/www/html'
- './mylib:/var/www/mylib'
Create Dockerfile file in your project folder with following content:
FROM php:8.2-apache
# Installing Rust.
RUN apt-get update &&\
apt-get install -y cargo
# Installing OPcache.
RUN set -eux; \
docker-php-ext-configure opcache; \
docker-php-ext-install opcache;
# Enabling OPcache and JIT.
RUN echo "opcache.enable=1" >> $PHP_INI_DIR/conf.d/opcache.ini &&\
echo "opcache.jit=on" >> $PHP_INI_DIR/conf.d/opcache.ini
# Installing FFI.
RUN apt-get update &&\
apt-get install -y libffi-dev &&\
docker-php-ext-configure ffi --with-ffi &&\
docker-php-ext-install ffi
# Configuring FFI to preload a given file.
RUN echo "ffi.enable=preload" > $PHP_INI_DIR/conf.d/ffi.ini &&\
echo "opcache.preload=/var/www/html/preload.php" >> $PHP_INI_DIR/conf.d/ffi.ini
# WARNING! For development purposes only.
# Making www-data user match your user to be able to edit files mapped from your local system.
RUN usermod -u 1000 www-data
# Setting a user to be default apache user.
USER www-data
ENV USER=www-data
# Running.
CMD echo 'Building a library.' &&\
# TODO: uncomment when library is created.
# cd /var/www/mylib &&\
# rustc --crate-type=cdylib -O -o ./mylib.so ./src/lib.rs &&\
echo 'Starting apache' &&\
apache2-foreground
Create folders for your project.
mkdir app
mkdir mylib
Start the docker container by running in your project folder:
docker-compose up --build
Creating a Rust library project
Now we have required dependencies installed and can create a Rust project.
docker exec php_2000x_faster cargo init ../mylib
Remove git folder if you like to have both apps in your repository.
docker exec php_2000x_faster rm -rf ../mylib/.git
Library file is by default named lib.rs, let’s create this file in mylib/src folder. It will contain simple, beloved by programmers, Fibonacci number calculation function and also a function that allows running multiple times Fibonacci function, which we will use for benchmark below:
#[no_mangle]
pub extern fn fibonacci(number: i64) -> i64 {
return match number {
0 => 0,
1 => 1,
number => fibonacci(number - 1) + fibonacci(number - 2)
}
}
#[no_mangle]
pub extern fn fibonacci_loop(repeat: i64, number: i64) -> i64 {
let mut result = number;
for _ in 0..repeat {
result = fibonacci(number);
}
return result
}
Now you can uncomment following lines in Dockerfile to enable compiling the library:
...
# Running.
CMD echo 'Building a library.' &&\
# TODO: uncomment when library is created.
cd /var/www/mylib &&\
rustc --crate-type=cdylib -O -o ./mylib.so ./src/lib.rs &&\
echo 'Starting apache' &&\
apache2-foreground
Importing Rust library into PHP application
We have already enabled FFI for PHP, we have also instructed OPcache to preload a library from preload.php file in app folder. What we need to do is importing the library using this mechanism.
Create preload.php file in app folder with following content:
<?php
FFI::load(dirname(__DIR__) . "/mylib/mylib.h");
opcache_compile_file(__DIR__ . "/mylib.php");
The important thing is that you need C header file that describes your library, this file contains definitions for functions that we are going to use. For this, create mylib.h file in mylib folder with following content:
#define FFI_SCOPE "MyLib"
#define FFI_LIB "./mylib.so"
int64_t fibonacci(int64_t n);
int64_t fibonacci_loop(int64_t total, int64_t n);
What is left is creating a class that will use this library. For this, create mylib.php file in app folder with following content:
<?php
final class MyLib {
private static $ffi = null;
function __construct() {
if (is_null(self::$ffi)) {
self::$ffi = FFI::scope("MyLib");
}
}
function fibonacci($number) {
return (int)self::$ffi->fibonacci($number);
}
function fibonacci_loop($repeat, $number) {
return (int)self::$ffi->fibonacci_loop($repeat, $number);
}
}
This class we will use to run functions from the Rust library.
Running benchmark
Create index.php file in app folder with following content:
<?php
function fibonacci($number)
{
if ($number == 0) {
return 0;
} elseif ($number == 1) {
return 1;
} else {
return fibonacci($number - 1) + fibonacci($number - 2);
}
}
function fibonacci_loop($repeat, $number)
{
$result = 0;
for ($i = 0; $i < $repeat; $i++) {
$result = fibonacci($number);
}
return $result;
}
require_once "mylib.php";
$myLib = new MyLib();
echo '<pre>';
$repeat = 50;
$number = 32;
$startTime = microtime(true);
$myLib->fibonacci_loop($repeat, $number);
$executionTimeRust = microtime(true) - $startTime;
echo "Running fibonacci number calculation $repeat times for $number number."."\n";
echo "Fully on Rust execution time: <strong>$executionTimeRust</strong> sec."."\n";
$startTime = microtime(true);
for ($i = 0; $i < $repeat; $i++) {
$myLib->fibonacci($number);
}
$executionTimeRustPhp = microtime(true) - $startTime;
echo "Calling Rust function in PHP loop execution time: <strong>$executionTimeRustPhp</strong> sec."."\n";
$startTime = microtime(true);
fibonacci_loop($repeat, $number);
$executionTimePhp = microtime(true) - $startTime;
echo "Fully on PHP execution time: <strong>$executionTimePhp</strong> sec."."\n";
$boostFactor = $executionTimePhp/$executionTimeRust;
echo "Total boost: <strong>$boostFactor</strong> times." . "\n";
echo '</pre>';
Stop and restart docker container:
Ctrl+C
docker-compose up --build
Open the application in web browser http://localhost:5080/ here is what you will see:
Sources on GitHub
Below are the sources and instructions how to run them:
Conclusion
As you can see, rewriting CPU-intensive calculations in Rust gave over 2000x performance increase. As I mentioned above, PHP is not very much optimal for large arrays like making custom algorithms for image processing, so here you can also have great benefits.
Note that -O option in rustc command is highly important, because without it it generates unoptimized code which runs extremely slow.
About the author
Timofey Bugaevsky is a software engineer with almost 20 years of experience, working with various programming languages and frameworks. He’s best at sustaining and adding new features to existing in-house built web systems. If you’re interested, you’re welcome to hire.