New Goodness in PHP8: Fibers, Unions, and More!
Let’s Do Something New
If you’ve ever tried to migrate from one major version of PHP to another, it can be annoying that it introduces breaking changes. Maybe someday all languages will do things like Rust so that breaking changes are no longer possible, but until then…we just have to roll with things!
PHP8 brings a number of nifty new goodies; let’s look at what some of these new features are and how they work.
JIT Compilation in PHP
I’m sure that you’ve seen the phrase “just in time compilation” somewhere before as a developer. PHP8 is a bit slow to the game (Chrome sported a JIT compiler for JavaScript in 2008), but the docs indicate that JIT can lead to a 3x increase in speed during lab tests.
It also notes that JIT compilation is not likely to make a difference in terms of real-world application performance.
We learned about PHP’s compilation in a previous article, so we’re already familiar with tokenization, building an AST, and translating that into Zend Opcodes. We know this happens with every request because PHP is an interpreted language.
With Opcache, we learned that PHP can save some time by “remembering” opcodes, speeding up this phase of the application by skipping work that’s already been done before (tokenization, building the AST, etc.).
Note that opcache is not optional if you want to use JIT compilation; PHP’s JIT compilation requires opcache to work and is implemented as a part of opcache. Understanding a bit about how compilation in PHP works, we can understand why.
So…how is JIT different from Opcache? Both are intended to reduce the time we spend compiling code to increase performance. We first have to understand that creating zend opcodes isn’t actually the end of the process. Those opcodes still have to be translated into actual machine code.
In other words, the Zend Engine works like a virtual machine -- enabling us to pass instructions to the CPU without worrying about the underlying architecture.
JIT uses something called DynASM to generate assembly directly from PHP, skipping compilation entirely. In other words, JIT enables us to send some instructions straight to the CPU without the Zend VM or compiling, which is obviously much faster!
As with many concepts in computing, the idea of JIT is not new and originates in the 1960s. Yeah, it’s been a long journey for PHP to adopt this idea, but this is a fantastic feature. For many systems, enabling it is as simple as adding this to a configuration (assuming opcache is already enabled):
opcache.jit_buffer_size=256M
Of course you may want to change that buffer size, but it’s very simple to enable.
All this being said, many real-world applications aren’t having bad performance because of poor CPU utilization, especially with how cheap the CPU really is. Still, as we saw in tech myths about efficiency, any gain in efficiency is a good win for humanity in general. Also, some workloads will gain huge increases with this, saving you money on hardware.
Reading this, you might think JIT is a win-win-win and that you should obviously enable it. Many articles might say something similar, but this isn’t so simple. Again, you won’t likely see any real benefit on real world apps -- or the speed benefit will be minor.
More importantly, JIT has some security considerations and (like most new-ish features) can be a source of bugs and caveats. Consider how JIT works: we compile PHP bytecode into machine code, then remember it…and feed that memorized machine code directly into the CPU if needed.
An attacker can take advantage of this -- and there’s a class of exploits called “JIT Spraying” that describe how this works. Unfortunately, PHP’s version of JIT (as of this writing) does not have specific protections in place to mitigate these attacks.
Also, you’ll see more benefits if your code is strictly typed -- if you’re migrating from an older codebase, slapping JIT on won’t yield as much benefit.
Overall? If you’re on the fence, don’t enable JIT. I’m not aware of any JIT spraying incidents or exploits just yet, but it’s still a plausible flaw. And for what? A very minor speed increase if any? As with all things, the performance benefit you get will depend on the application and use case, but it may not be worth it just yet.
Fibers: Async PHP ftw?
Fibers were introduced in PHP 8.1 and enable us to write “async PHP” from a code-level (compared to compiling PHP with ZTS, Zend Thread Safety -- multithreaded PHP that even its author doesn’t endorse). Conceptually, this is similar to the async/await pattern in Node or multithreading patterns in C#.
In a similar way, we can make a Fiber with some functionality defined. When we start the fiber, the code runs as we’d expect. It only becomes interesting when it hits a “Fiber::suspend” call. Similar to “await”, this is saying “go do other stuff for now”.
The code will continue from where it left off (i.e. right after you called fiber “start”) until you call “resume” on the Fiber. This lets you write asynchronous code, and can be very handy for a variety of use cases. Here is one pattern that hopefully makes its power more clear:
$fiber = new Fiber(function(array $things): void { foreach($things as $thing) { // do an operation with $thing, like copy a file, FFMPEG, cURL, etc. Fiber::suspend($thing); } }); // Start the operation $result = $fiber->start($things); while(!$fiber->isTerminated()) { // log the status of the operation here $result = $fiber->resume(); }
In this way, you could easily create a progress indicator that shows status as an operation happens. Very nifty!
Another (huge) benefit is moving PHP more toward asynchronous programming and non-blocking I/O in general. I hope I don’t need to say…but Fibers aren’t “multithreaded” PHP anymore than async/await is “multithreaded” JavaScript.
As developers, we aren’t expected to even use Fibers! The RFC says this rather explicitly: “The Fiber API is not expected to be used directly in application-level code” -- in other words, they expect people to leverage this as they create libraries and frameworks, but don’t expect the “average” day-to-day PHP dev to use them.
Overall, Fibers are a great addition, but aren’t yet full-featured enough to build an entire app off. Over time, I believe this modality of asynchronous programming will become the standard because it offers non-blocking I/O (ReactPHP and AMP already focus on this and are worth checking out!).
Unions and Intersections
Using strict types with PHP is the best practice, but there’s often use cases where a given argument could be more than one type. While we could handle nullable types in PHP7, now we have support for actual unions (without having to rely on PHPDoc comments):
private int|float $number
The only restriction is that you (obviously?) can’t use “void” as a union type, e.g. to specify a function that might return either a string or nothing. That’s hardly a big limit. Either a function returns something or it doesn’t, that seems fair doesn’t it?!
The only other potential caveat is that you’re allowed to use “false” as a union type (distinct from bool). This is because there’s many PHP functions that return “either some value, or false”, like strpos. As of 8.2, you can use “true” in the same manner, too.
For inheritance, unions do honor LSP, so you can specify a parent class or interface in the Union and a child class or class that implements the interface will still work.
There’s also a related concept in PHP 8.1 called “intersection types” that are basically the opposite of Unions. For example, instead of allowing a return type to be a “Article|News” perhaps we want it to be both an article and news type -- we’d specify this with an “&” symbol.
Since it’s illogical to have an intersection with a primitive type, intersections can only be used for classes or interfaces.
DNF Types
DNF types (from PHP 8.2) relate to both of the above concepts, Unions and Intersections. For example:
A&B|C is a combination of an intersection and a Union. We’d read this as “the property can implement A and B, or can be of type C.
DNF stands for “disjunctive normal form”, which is a fancy way to say “boolean format”. Although this is fairly straightforward, there are a few caveats. For example, the following wouldn’t make sense:
(A&B)|A
Since the “A” type union is less restrictive than the intersection, it doesn’t make much sense and will produce an error.
Strongly typed PHP is the future, like it or not! Unions, intersections, and DNF types help make typing in PHP more flexible and usable, which improves life all-around.
Other Neat Stuff!
- Named arguments; this was long overdue!
- Constructor property promotion to reduce boilerplate (see here)
- Nullsafe operators: $now?->we?->can?->party()?->easier;
- The @ operator no longer silences fatal error -- I mention this because it’s destined to break applications in hilarious ways somewhere
Conclusion
As an open source language, PHP’s constant evolution is sometimes a mixed bag. JIT compilation is a fantastic idea, but I’m not sure if it's ready for enterprise-level adoption. Similarly, Fibers are a step forward in bringing more async programming concepts to PHP, but aren’t really intended for application developers to use yet. Also, if you really want non-blocking I/O in PHP, it might be wiser to check out an existing library like ReactPHP, especially since the implementation for Fibers is still considered experimental and subject to change.
If you’re looking for a more detailed rundown of PHP’s latest features or RFCs, check out PhpWatch’s fantastic list here.