25 Easy PHP7 Micro-Optimizations

Searching Google for "Is micro-optimization worth it?" turns up a lot of hostile responses that all boil down to a big, fat, No, which is interesting because without more context, such a question cannot have such a simple answer. The Gatekeepers of Knowledge are choosing the frame to arrive at that answer, and they're all choosing the same frame: "Is it worth my time to make small improvements to this run-of-the-mill project?"

This suggests that most programmers — at least the vociferous ones — approach their craft with something like a gig mentality, missing the broader point. Mastering a programming language means understanding the nuance, however small, and that is incredibly valuable.

Framed thusly, the following micrommandments are as good a place as any to start!

Statement Order Matters

PHP evaluates code top down, left right. Whenever you have some sort of chained logic like a multi-part condition or a series of operations inside a loop that might result in a continue or break depending on how things work out, you should prioritize statements based on their ease of execution or likelihood of breaking the chain.

Consider the following:

// Say we need a string with a length, unless $force is set.
if ($force || ($str && is_string($str)) { ... }

We check $force first, because its existence moots the need to run the two chained tests on the string. $str is second because it is a simple yay/nay, and is_string() falls last because it is the heaviest of the lot.

Avoid Dynamic Variables, Functions, Etc.

In PHP, variables, functions, classes, etc., can be stored in a variable, or called by a variable variable. While this is sometimes convenient, it does not receive any opcode love from the PHP7 interpreter. It is much better to code such things directly. For example, avoid code like the following:

// A variable variable.
$my_var = 'hello';
$$my_var = 'world';
echo $$my_var; // world

// A variable function.
$my_func = function() { echo 'Hello World'; }
$my_func(); // Hello World

// An unnamed function.
usort($arr, function($a, $b) { ... });

// A variable class method.
$my_func = '\\my\\stuff::hello';
$my_func();

Quotes vs Apostrophes

When PHP sees double-quotes ", it will check the string for the presence of variables, which wastes micro-resources. If your string has no variables or apostrophes in it, wrap it in apostrophes ' instead.

On a related note, mind your use of the . concatenation operator, particularly when parts of the equation are quoted and parts are not. If you can handle everything in one way, go with that one way. For example:

// Make a string.
$foo = "{$bar}{$none}" . 'hello';
$foo = "{$bar}{$none}hello"; // This is better.

Case Sensitivity

PHP provides a handful of case-insensitive functions like stripos() or the use of the i flag in a regular expression. These are very helpful when matches should not consider case, but come with additional overhead. When case does matter, you should use case-sensitive functions instead.

Multi-Byte

Similarly, multi-byte helpers like those provided via the mbstring extension are necessary for correctly handling strings like "Björk", but come with their own overhead. If you are evaluating a string that does not need anything fancy, use the standard functions instead.

Strictness and Looseness

As with most programming languages, PHP can evaluate conditions strictly — matching both the value and the data type — or loosely — matching only the value. For both performance and security reasons, strict comparisons should be used. This means using === instead of ==.

A notable exception to this, however, is when checking for somethingness or nothingness. Unless you're dealing with a function like strpos() which might return false or 0 (the latter being a positive result), it is better to run a loose, naked test like if ($str).

foreach()

There are a lot of ways to iterate over an array, but foreach() is the most efficient.

$k=>$v

A foreach() loop can be written as $k=>$v or simply $v. The latter is more efficient and should be preferred except in cases where you need to access the current index.

&$v

When iterating over byte-heavy arrays, you can avoid expensive copy operations by assigning $v by reference, e.g. foreach ($foo as &$v). Interestingly, this decreases performance under normal circumstances, so should only be used for extreme data.

Avoid One-Liners

PHP provides many convenient wrapper functions like array_map() and array_filter(), which perform their own internal loops so you don't have to. These functions do not perform any better than a manual foreach() loop, and because they can only perform one task, the chances of expensive re-iteration are increased.

Group Loop Operations

On a similar note, it is better to group any operations you need to perform against an array into a single loop, rather than iterating over the same data multiple times.

Avoid Dynamic Loop Conditions

Unless the condition needs to dynamically change, avoid using a function in a loop's condition as it will be called at every iteration. Consider the following:

// Don't do this.
for ($x = 0; $x < get_the_limit(); $x++) { ... }

// This is better.
$limit = get_the_limit();
for ($x = 0; $x < $limit; $x++) { ... }

Avoid Aliases

Aliases are useful to maintain backward compatibility, but when writing new code, you should take care to reference the main function. Otherwise PHP will have to spend time following links.

Pass Heavy Arguments by Reference

By default, function arguments are copies of the original. This is helpful in isolating scope, but can lead to increased memory use and processing time. For heavy data, like a large array, it is better to set up the function to accept the argument by reference. Refer to the PHP manual for more information.

isset() is Fucking Magic

PHP's isset() function is designed to check whether or not something is something (versus being null). Along these lines, it is most frequently used to check whether or not a variable exists before performing some operation. But it can also stand-in for all sorts of more specific (and heavier) functions. Consider the following use cases:

// String length (not multi-byte safe!).
if (strlen($str) >= 4) { ... }
if (isset($str[3])) { ... }

// Array keys (provided the value is not null).
if (array_key_exists($key, $arr)) { ... }
if (isset($arr[$key])) { ... }

// In PHP 7+, isset() can test constant array keys too.
if (isset(my_class::ARR[$key])) { ... }

// isset() can go deep too.
if (array_key_exists($key, $arr) && is_array($arr[$key]) && array_key_exists($key2, $arr[$key])) { ... }
if (isset($arr[$key][$key2])) { ... }

// And it can check multiple things at once.
if (isset($var1, $var2, $var3)) { ... }

// Honorable mention: in_array()
// If checking values in a large array, or running a lot of
// in_array() tests in a loop, perform a single array_flip()
// before the block, then use isset().
$arr = array_flip($arr);
if (isset($arr[$key])){ ... }

Know a Function's Intent

A lot of different PHP functions can be used to provide an answer to a given question. Understanding what a function's intended purpose is, and thus its internal operations, you can choose a function that gets there more quickly. Consider the following, ordered worst to best.

// Does the string contain a comma?
if (preg_match('/,/', $str)) { ... }
if (substr_count($str, ',')) { ... }
if (false !== strpos($str, ',')) { ... }

While preg_match() is a powerhouse, its power isn't needed here.

substr_count() is simpler, but is tasked with counting every occurrence of the substring. If $str is a book, that could mean checking tens of thousands of characters.

strpos(), on the other hand, is only tasked with finding the first occurrence of the substring. If the first sentence of the book contains a comma, it's job is done.

Avoid @Warning Suppression

In PHP, a function call can be prefixed with an @ symbol to suppress warning output. This can be useful, but when not needed increases overhead for the call, so should be avoided.

Avoid Require/Include (once)

The purpose of require_once() and include_once() is to avoid accidentally inserting the same document twice within a given program run. In places where a file may or may not have already be included, the _once() variant is appropriate. However in other places, like a bootstrap file that you know is only being called once, it is more performant to simply use require() or include().

echo() Beats print()

Both functions print data to the screen, but echo() does it lighter.

Switch vs If/ElseIf

The condition in a switch() statement is only evaluated once. If you have a large conditional block checking the same value, a switch() will be more efficient than a number of if/elseif pairings.

As previously mentioned, it is best to organize the statements by either their ease of execution or likelihood of ending the chain.

Avoid Globals

Global variables come with some special overhead, both in their declaration, and also in their use. If you can avoid a global variable altogether, please do. If you can't avoid it, place any in-function global $foo statements after you know the function is going to run. For example:

function get_post($id=0) {
	$id = (int) $id;
	if ($id <= 0) {
		return false;
	}

	// Placed here to avoid unnecessary inclusion
	// with invalid IDs.
	global $posts;

	...
}

Operators Beat Functions

The closer you can get to the base of a language, the better performance will be. Typecasting is a common example of this. If, for example, you wanted to make sure a variable is an integer, you could do any of the following:

// Make it an int.
$foo = filter_var($foo, FILTER_SANITIZE_NUMBER_INT);
$foo = intval($foo);
$foo = (int) $foo;

Using (int) is about a magnitude faster than calling a function. But that said, intval() accepts an optional second argument for the base, and filter_var() strips non-numeric pieces from the value before returning it. Complex use cases might require heavier solutions, but for the simple ones, avoid function calls.

Autoloaders

Just-in-Time class loading is a little complicated, and will probably require specific benchmarking to see which method works best for a given project. As a general rule, if a project contains only a few dozen classes, the autoloader will be most efficient with a static map (such as an array of classnames to file paths). If there are hundreds, a file-based approach (checking at runtime to see whether a thusly-named file exists) will be better, particularly on a Linux server.

Use Static Methods and Properties

Any method or property in a class that is not instance-specific and does not need $this should be declared static to receive the maximum performance gains from PHP.

Unset and Forget

PHP will free up memory on its own, but if you have a large variable in the middle of a chunk of code that is no longer needed, the application should benefit from a quick call to unset().

Josh Stoik
22 September 2017
Previous The Art of Database Partitioning
Next Meltdown/Spectre Vulnerability Checker for Linux