Chapter 10: Improving Performance

PHP 8.x introduces a number of new features that have a positive effect on performance. Also, a number of internal improvements, especially in array handling and managing object references, lead to a substantive performance increase over earlier PHP versions. In addition, many of the PHP 8 best practices covered in this chapter lead to greater efficiency and lower memory usage. In this chapter, you'll discover how to optimize your PHP 8 code to achieve maximum performance.

PHP 8 includes a technology referred to as weak references. By mastering this technology, discussed in the last section of this chapter, your applications will use far less memory. By carefully reviewing the material covered in this chapter and by studying the code examples, you will be able to write faster and more efficient code. Such mastery will vastly improve your standing as a PHP developer and result in satisfied customers, as well as improving your career potential.

Topics covered in this chapter include the following:

  • Working with the Just-In-Time (JIT) compiler
  • Speeding up array handling
  • Implementing stable sort
  • Using weak references to improve efficiency

Technical requirements

To examine and run the code examples provided in this chapter, the minimum recommended hardware is the following:

  • x86_64 based desktop PC or laptop
  • 1 gigabyte (GB) free disk space
  • 4 GB of RAM
  • 500 kilobits per second (Kbps) or faster internet connection

In addition, you will need to install the following software:

  • Docker
  • Docker Compose

Please refer to the Technical requirements section of Chapter 1, Introducing New PHP 8 OOP Features, for more information on Docker and Docker Compose installation, as well as how to build the Docker container used to demonstrate the code explained in this book. In this book, we refer to the directory in which you stored the sample code for this book as /repo.

The source code for this chapter is located here: https://github.com/PacktPublishing/PHP-8-Programming-Tips-Tricks-and-Best-Practices. We can now begin our discussion by having a look at the long-awaited JIT compiler.

Working with the JIT compiler

PHP 8 introduces the long-awaited JIT compiler. This is an important step and has important ramifications for the long-term viability of the PHP language. Although PHP already had the ability to produce and cache bytecode, before the introduction of the JIT compiler, PHP did not have the ability to directly cache machine code.

There have actually been several attempts to add JIT compiler capabilities to PHP, dating back to 2011. The performance boost seen in PHP 7 was a direct result of these early efforts. None of the earlier JIT compiler efforts were proposed as RFCs (Requests for Comments) as they didn't significantly improve performance. The core team now feels that any further performance gains can now only be achieved using JIT. As a side benefit, this opens the possibility of PHP being used as a language for non-web environments. Another benefit is that the JIT compiler opens the possibility to develop PHP extensions in languages other than C.

It's extremely important to pay close attention to the details given in this chapter as proper use of the new JIT compiler has the potential to greatly improve the performance of your PHP applications. Before we get into implementation details, it's first necessary to explain how PHP executes bytecode without the JIT compiler. We'll then show you how the JIT compiler works. After this, you will be in a better position to understand the various settings and how they can be fine-tuned to produce the best possible performance for your application code.

Let's now turn our attention to how PHP works without the JIT compiler.

Discovering how PHP works without JIT

When PHP is installed on a server (or in a Docker container), in addition to the core extensions, the main component installed is actually a virtual machine (VM) often referred to as the Zend Engine. This VM operates in a manner quite different from virtualization technologies such as VMware or Docker. The Zend Engine is closer in nature to the Java Virtual Machine (JVM) in that it accepts bytecode and produces machine code.

This begs the question: what is bytecode and what is machine code? Let's have a look at this question now.

Understanding bytecode and machine code

Machine code, or machine language, is a set of hardware instructions understood by the CPU directly. Each piece of machine code is an instruction that causes the CPU to perform a specific operation. These low-level operations include moving information between registers, moving a given number of bytes in or out of memory, adding, subtracting, and so forth.

Machine code is often rendered somewhat human-readable by using assembly language. Here is an example of machine code rendered in assembly language:

JIT$Mandelbrot::iterate: ;

        sub $0x10, %esp

        cmp $0x1, 0x1c(%esi)

        jb .L14

        jmp .L1

.ENTRY1:

        sub $0x10, %esp

.L1:

        cmp $0x2, 0x1c(%esi)

        jb .)L15

        mov $0xec3800f0, %edi

        jmp .L2

.ENTRY2:

        sub $0x10, %esp

.L2:

        cmp $0x5, 0x48(%esi)

        jnz .L16

        vmovsd 0x40(%esi), %xmm1

        vsubsd 0xec380068, %xmm1, %xmm1

Although, for the most part, the commands are not easily understood, you can see from the assembly language representation that the instructions include commands to compare (cmp), move information between registers and/or memory (mov), and jump to another point in the instruction set (jmp).

Bytecode, also called opcode, is a greatly reduced symbolic representation of the original program code. Bytecode is produced by a parsing process (often called the interpreter) that breaks human-readable program code into symbols known as tokens, along with values. Values would be any string, integer, float, and Boolean data used in the program code.

Here is an example of a fragment of the bytecode produced based upon the example code (shown later) used to create a Mandelbrot:

Figure 10.1 – Bytecode fragment produced by the PHP parsing process

Figure 10.1 – Bytecode fragment produced by the PHP parsing process

Let's now have a look at the conventional execution flow of a PHP program.

Understanding conventional PHP program execution

In a conventional PHP program run cycle, the PHP program code is evaluated and broken down into bytecode by an operation known as parsing. The bytecode is then passed to the Zend Engine, which in turn converts the bytecode into machine code.

When PHP is first installed on a server, the installation process kicks in the necessary logic that tailors the Zend Engine to the specific CPU and hardware (or virtual CPU and hardware) for that particular server. Thus, when you write PHP code, you have no awareness of the particulars of the actual CPU that eventually runs your code. It is the Zend Engine that provides hardware-specific awareness.

Figure 10.2, shown next, illustrates conventional PHP execution:

Figure 10.2 – Conventional PHP program execution flow

Figure 10.2 – Conventional PHP program execution flow

Although PHP, especially PHP 7, is quite fast, it's still of interest to gain additional speed. For this purpose, most installations also enable the PHP OPcache extension. Let's have a quick look at OPcache before moving on to the JIT compiler.

Understanding the operation of PHP OPcache

As the name implies, the PHP OPcache extension caches opcode (bytecode) the first time a PHP program is run. On subsequent program runs, the bytecode is drawn from the cache, eliminating the parsing phase. This saves a significant amount of time and is a highly desirable feature to enable on a production site. The PHP OPcache extension is part of the set of core extensions; however, it's not enabled by default.

Before enabling this extension, you must first confirm that your version of PHP has been compiled with the --enable-opcache configure option. You can check this by executing the phpinfo() command from inside PHP code running on your web server. From the command line, enter the php -i command. Here is an example running php -i from the Docker container used for this book:

root@php8_tips_php8 [ /repo/ch10 ]# php -i

phpinfo()

PHP Version => 8.1.0-dev

System => Linux php8_tips_php8 5.8.0-53-generic #60~20.04.1-Ubuntu SMP Thu May 6 09:52:46 UTC 2021 x86_64

Build Date => Dec 24 2020 00:11:29

Build System => Linux 9244ac997bc1 3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt11-1 (2015-05-24) x86_64 GNU/Linux

Configure Command =>  './configure'  '--prefix=/usr' '--sysconfdir=/etc' '--localstatedir=/var' '--datadir=/usr/share/php' '--mandir=/usr/share/man' '--enable-fpm' '--with-fpm-user=apache' '--with-fpm-group=apache'

// not all options shown

'--with-jpeg' '--with-png' '--with-sodium=/usr' '--enable-opcache-jit' '--with-pcre-jit' '--enable-opcache'

As you can see from the output, OPcache was included in the configuration for this PHP installation. To enable OPcache, add or uncomment the following php.ini file settings:

  • zend_extension=opcache
  • opcache.enable=1
  • opcache.enable_cli=1

The last setting is optional. It determines whether or not PHP commands executed from the command line are also processed by OPcache. Once enabled, there are a number of other php.ini file settings that affect performance, however, these are beyond the scope of this discussion.

Tip

For more information on PHP php.ini file settings that affect OPcache, have a look here: https://www.php.net/manual/en/opcache.configuration.php.

Let's now have a look at how the JIT compiler operates, and how it differs from OPcache.

Discovering PHP program execution with the JIT compiler

The problem with the current approach is that whether or not the bytecode is cached, it's still necessary for the Zend Engine to convert the bytecode into machine code each and every time the program request is made. What the JIT compiler offers is the ability to not only compile bytecode into machine code but to cache machine code as well. The process is facilitated by a tracing mechanism that creates traces of requests. The trace allows the JIT compiler to determine which blocks of machine code need to be optimized and cached. The execution flow using the JIT compiler is summarized in Figure 10.3:

Figure 10.3 – PHP execution flow with the JIT compiler

Figure 10.3 – PHP execution flow with the JIT compiler

As you can see from the diagram, the normal execution flow incorporating OPcache is still present. The main difference is that a request might invoke a trace, causing the program flow to shift immediately to the JIT compiler, effectively bypassing not only the parsing process but the Zend Engine as well. Both the JIT compiler and the Zend Engine can produce machine code ready for direct execution.

The JIT compiler did not evolve out of thin air. The PHP core team elected to port the highly performant and well-tested DynASM preprocessing assembler. Although DynASM was primarily developed for the JIT compiler used by the Lua programming language, its design is such that it's perfectly suited to form the basis of a JIT compiler for any C-based language (such as PHP!).

Another favorable aspect of the PHP JIT implementation is that it doesn't produce any Intermediate Representation (IR) code. In contrast, the PyPy VM used to run Python code using JIT compiler technology, has to first produce IR code in a graph structure, used for flow analysis and optimization, before the actual machine code is produced. The DynASM core in the PHP JIT doesn't require this extra step, resulting in greater performance than is possible in other interpreted programming languages.

Tip

For more information on DynASM, have a look at this website: https://luajit.org/dynasm.html. Here's an excellent overview of how the PHP 8 JIT operates: https://www.zend.com/blog/exploring-new-php-jit-compiler. You can also read the official JIT RFC here: https://wiki.php.net/rfc/jit.

Now that you have an idea of how the JIT compiler fits into the general flow of a PHP program execution cycle, it's time to learn how to enable it.

Enabling the JIT compiler

Because the primary function of the JIT compiler is to cache machine code, it operates as an independent part of the OPcache extension. OPcache serves as a gateway to both enable JIT functionality as well as to allocate memory to the JIT compiler from its own allotment. Therefore, in order to enable the JIT compiler, you must first enable OPcache (see the previous section, Understanding the operation of PHP OPcache).

In order to enable the JIT compiler, you must first confirm that PHP has been compiled with the --enable-opcache-jit configuration option. You are then in a position to enable or disable the JIT compiler by simply assigning a non-zero value to the php.ini file's opcache.jit_buffer_size directive.

Values are specified either as an integer – in which case, the value represents the number of bytes; a value of zero (the default), which disables the JIT compiler; or you can assign a number followed by any of the following letters:

  • K: Kilobytes
  • M: Megabytes
  • G: Gigabytes

The value you specify for the JIT compiler buffer size must be less than the memory allocation you assigned to OPcache because the JIT buffer is taken out of the OPcache buffer.

Here is an example that sets the OPcache memory consumption to 256 M and the JIT buffer to 64 M. These values can be placed anywhere in the php.ini file:

opcache.memory_consumption=256

opcache.jit_buffer_size=64M

Now that you have an idea of how the JIT compiler works, and how it can be enabled, it's extremely important that you know how to properly set the tracing mode.

Configuring the tracing mode

The php.ini setting opcache.jit controls the JIT tracer operation. For convenience, one of the following four preset strings can be used:

  • opcache.jit=disable

    Completely disables the JIT compiler (regardless of other settings).

  • opcache.jit=off

    Disables the JIT compiler but (in most cases) you can enable it at runtime using ini_set().

  • opcache.jit=function

    Sets the JIT compiler tracer to function mode. This mode corresponds to the CPU Register Trigger Optimization (CRTO) digits 1205 (explained next).

  • opcache.jit=tracing

    Sets the JIT compiler tracer to tracing mode. This mode corresponds to the CRTO digits 1254 (explained next). In most cases, this setting gives you the best performance.

  • opcache.jit=on

    This is an alias for tracing mode.

    Tip

    Relying upon runtime JIT activation is risky and can produce inconsistent application behavior. The best practice is to use either the tracing or the function setting.

The four convenience strings actually resolve into a four-digit number. Each digit corresponds to a different aspect of the JIT compiler tracer. The four digits are not bitmasks unlike other php.ini file settings and are specified in this order: CRTO. Here is a summary of each of the four digits.

C (CPU opt flags)

The first digit represents CPU optimization settings. If you set this digit to 0, no CPU optimization takes place. A value of 1 enables the generation of Advanced Vector Extensions (AVX) instructions. AVX are extensions to the x86 instruction set architecture for microprocessors from Intel and AMD. AVX has been supported on Intel and AMD processors since 2011. AVX2 is available on most server-type processors such as Intel Xeon.

R (register allocation)

The second digit controls how the JIT compiler deals with registers. Registers are like RAM, except that they reside directly inside the CPU itself. The CPU constantly moves information in and out of registers in order to perform operations (for example, adding, subtracting, performing logical AND, OR, and NOT operations, and so forth). The options associated with this setting allow you to disable register allocation optimization or allow it at either the local or global level.

T (JIT trigger)

The third digit dictates when the JIT compiler should trigger. Options include having the JIT compiler operate the first time a script is loaded or upon first execution. Alternatively, you can instruct the JIT when to compile hot functions. Hot functions are ones that are called the most frequently. There is also a setting that tells JIT to only compile functions marked with the @jit docblock annotation.

O (optimization level)

The fourth digit corresponds to the optimization level. Options include disabling optimization, minimal, and selective. You can also instruct the JIT compiler to optimize based upon individual functions, call trees, or the results of inner procedure analysis.

Tip

For a complete breakdown of the four JIT compiler tracer settings, have a look at this documentation reference page: https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.jit.

Let's now have a look at the JIT compiler in action.

Using the JIT compiler

In this example, we use a classic benchmark program that produces a Mandelbrot. This is an excellent test as it's extremely computation-intensive. The implementation we use here is drawn from the implementation code produced by Dmitry Stogov, one of the PHP core development team members. You can view the original implementation here: https://gist.github.com/dstogov/12323ad13d3240aee8f1:

  1. We first define the Mandelbrot parameters. Especially important is the number of iterations (MAX_LOOPS). A large number spawns more calculations and slows down overall production. We also capture the start time:

    // /repo/ch10/php8_jit_mandelbrot.php

    define('BAILOUT',   16);

    define('MAX_LOOPS', 10000);

    define('EDGE',      40.0);

    $d1  = microtime(1);

  2. In order to facilitate multiple program runs, we add an option to capture a command line param, -n. If this parameter is present, the Mandelbrot output is suppressed:

    $time_only = (bool) ($argv[1] ?? $_GET['time'] ?? FALSE);

  3. We then define a function, iterate(), drawn directly from the Mandelbrot implementation by Dmitry Stogov. The actual code, not shown here, can be viewed at the URL mentioned earlier.
  4. Next, we produce the ASCII image by running through the X/Y coordinates determined by EDGE:

    $out = '';

    $f   = EDGE - 1;

    for ($y = -$f; $y < $f; $y++) {

        for ($x = -$f; $x < $f; $x++) {

            $out .= (iterate($x/EDGE,$y/EDGE) == 0)

                  ? '*' : ' ';

        }

        $out .= " ";

    }

  5. Finally, we produce output. If running through a web request, the output is wrapped in <pre> tags. If the -n flag is present, only the elapsed time is shown:

    if (!empty($_SERVER['REQUEST_URI'])) {

        $out = '<pre>' . $out . '</pre>';

    }

    if (!$time_only) echo $out;

    $d2 = microtime(1);

    $diff = $d2 - $d1;

    printf(" PHP Elapsed %0.3f ", $diff);

  6. We first run the program in the PHP 7 Docker container three times using the -n flag. Here is the result. Please note that the elapsed time was easily over 10 seconds in the demo Docker container used in conjunction with this book:

    root@php8_tips_php7 [ /repo/ch10 ]#

    php php8_jit_mandelbrot.php -n

    PHP Elapsed 10.320

    root@php8_tips_php7 [ /repo/ch10 ]#

    php php8_jit_mandelbrot.php -n

    PHP Elapsed 10.134

    root@php8_tips_php7 [ /repo/ch10 ]#

    php php8_jit_mandelbrot.php -n

    PHP Elapsed 11.806

  7. We now turn to the PHP 8 Docker container. To start, we adjust the php.ini file to disable the JIT compiler. Here are the settings:

    opcache.jit=off

    opcache.jit_buffer_size=0

  8. Here is the result of running the program three times in PHP 8 using the -n flag:

    root@php8_tips_php8 [ /repo/ch10 ]#

    php php8_jit_mandelbrot.php -n

    PHP Elapsed 1.183

    root@php8_tips_php8 [ /repo/ch10 ]#

    php php8_jit_mandelbrot.php -n

    PHP Elapsed 1.192

    root@php8_tips_php8 [ /repo/ch10 ]#

    php php8_jit_mandelbrot.php -n

    PHP Elapsed 1.210

  9. Right away, you can see a great reason to switch to PHP 8! Even without the JIT compiler, PHP 8 was able to perform the same program in a little over 1 second: 1/10 of the amount of time!
  10. Next, we modify the php.ini file settings to use the JIT compiler function tracer mode. Here are the settings used:

    opcache.jit=function

    opcache.jit_buffer_size=64M

  11. We then run the same program again using the -n flag. Here are the results running in PHP 8 using the JIT compiler function tracer mode:

    root@php8_tips_php8 [ /repo/ch10 ]#

    php php8_jit_mandelbrot.php -n

    PHP Elapsed 0.323

    root@php8_tips_php8 [ /repo/ch10 ]#

    php php8_jit_mandelbrot.php -n

    PHP Elapsed 0.322

    root@php8_tips_php8 [ /repo/ch10 ]#

    php php8_jit_mandelbrot.php -n

    PHP Elapsed 0.324

  12. Wow! We managed to speed up processing by a factor of 3. The speed is now less than 1/3 of a second! But what happens if we try the recommended JIT compiler tracing mode? Here are the settings to invoke that mode:

    opcache.jit=tracing

    opcache.jit_buffer_size=64M

  13. Here are the results of our last set of program runs:

    root@php8_tips_php8 [ /repo/ch10 ]#

    php php8_jit_mandelbrot.php -n

    PHP Elapsed 0.132

    root@php8_tips_php8 [ /repo/ch10 ]#

    php php8_jit_mandelbrot.php -n

    PHP Elapsed 0.132

    root@php8_tips_php8 [ /repo/ch10 ]#

    php php8_jit_mandelbrot.php -n

    PHP Elapsed 0.131

The last result, as shown in the output, is truly staggering. Not only can we run the same program 10x faster than PHP 8 without the JIT compiler, but we are running 100x faster than PHP 7!

Important note

It's important to note that times will vary depending on the host computer you are using to run the Docker containers associated with this book. You will not see exactly the same times as shown here.

Let's now have a look at JIT compiler debugging.

Debugging with the JIT compiler

Normal debugging using XDebug or other tools will not work effectively when using the JIT compiler. Accordingly, the PHP core team added an additional php.ini file option, opcache.jit_debug, which produces additional debugging information. In this case, the settings available take the form of bit flags, which means you can combine them using bitwise operators such as AND, OR, XOR, and so forth.

Table 10.1 summarizes values that can be assigned as an opcache.jit_debug setting. Please note that the column labeled Internal Constant does not show PHP predefined constants. These values are internal C code references:

Table 10.1 – opcache.jit_debug settings

Table 10.1 – opcache.jit_debug settings

So, for example, if you wish to enable debugging for ZEND_JIT_DEBUG_ASM, ZEND_JIT_DEBUG_PERF, and ZEND_JIT_DEBUG_EXIT, you could make the assignment in the php.ini file as follows:

  1. First, you need to add up the values you wish to set. In this example, we would add:

    1 + 16 + 32768

  2. You then apply the sum to the php.ini setting:

    opcache.jit_debug=32725

  3. Or, alternatively, represent the values using bitwise OR:

    opcache.jit_debug=1|16|32768

Depending on the debug setting, you are now in a position to debug the JIT compiler using tools such as the Linux perf command, or Intel VTune.

Here is a partial example of debug output when running the Mandelbrot test program discussed in the previous section. For the purposes of illustration, we are using the php.ini file setting opcache.jit_debug=32725:

root@php8_tips_php8 [ /repo/ch10 ]#

php php8_jit_mandelbrot.php -n

---- TRACE 1 start (loop) iterate() /repo/ch10/php8_jit_mandelbrot.php:34

---- TRACE 1 stop (loop)

---- TRACE 1 Live Ranges

#15.CV6($i): 0-0 last_use

#19.CV6($i): 0-20 hint=#15.CV6($i)

... not all output is shown

---- TRACE 1 compiled

---- TRACE 2 start (side trace 1/7) iterate()

/repo/ch10/php8_jit_mandelbrot.php:41

---- TRACE 2 stop (return)

TRACE-2$iterate$41: ; (unknown)

    mov $0x2, EG(jit_trace_num)

    mov 0x10(%r14), %rcx

    test %rcx, %rcx

    jz .L1

    mov 0xb0(%r14), %rdx

    mov %rdx, (%rcx)

    mov $0x4, 0x8(%rcx)

...  not all output is shown

What the output shows you is machine code rendered in assembly language. If you experience problems with your program code when using the JIT compiler, the assembly language dump might assist you in locating the source of the error.

However, please be aware that assembly language is not portable, and is completely oriented toward the CPU being used. Accordingly, you might have to obtain the hardware reference manual for that CPU and look up the assembly language code being used.

Let's now have a look at the other php.ini file settings that affect the operation of the JIT compiler.

Discovering additional JIT compiler settings

Table 10.2 provides a summary of all other opcache.jit* settings in the php.ini file that have not already been covered:

Table 10.2 – Additional opcache.jit* php.ini file settings

Table 10.2 – Additional opcache.jit* php.ini file settings

As you can see from the table, you have a high degree of control over how the JIT compiler operates. Collectively, these settings represent thresholds that control decisions the JIT compiler makes. These settings, if properly configured, allow the JIT compiler to ignore infrequently used loops and function calls. We'll now leave the exciting world of the JIT compiler and have a look at how to improve array performance.

Speeding up array handling

Arrays are a vital part of any PHP program. Indeed, dealing with arrays is unavoidable as much of the real-world data your program handles day to day arrives in the form of an array. One example is data from an HTML form posting. The data ends up in either $_GET or $_POST as an array.

In this section, we'll introduce you to a little-known class included with the SPL: the SplFixedArray class. Migrating your data from a standard array over to a SplFixedArray instance will not only improve performance but requires significantly less memory as well. Learning how to take advantage of the techniques covered in this chapter can have a substantial impact on the speed and efficiency of any program code currently using arrays with a massive amount of data.

Working with SplFixedArray in PHP 8

The SplFixedArray class, introduced in PHP 5.3, is literally an object that acts like an array. Unlike ArrayObject, however, this class requires you to place a hard limit on the array size, and only allows integer indices. The reason why you might want to use SplFixedArray rather than ArrayObject is SplFixedArray takes significantly less memory and is highly performant. In fact, SplFixedArray actually takes less memory than a standard array with the same data!

Comparing SplFixedArray with array and ArrayObject

A simple benchmark program illustrates the differences between a standard array, ArrayObject, and SplFixedArray:

  1. First, we define a couple of constants used later in the code:

    // /repo/ch10/php7_spl_fixed_arr_size.php

    define('MAX_SIZE', 1000000);

    define('PATTERN', "%14s : %8.8f : %12s ");

  2. Next, we define a function that adds 1 million elements comprised of a string 64 bytes long:

    function testArr($list, $label) {

        $alpha = new InfiniteIterator(

            new ArrayIterator(range('A','Z')));

        $start_mem = memory_get_usage();

        $start_time = microtime(TRUE);

        for ($x = 0; $x < MAX_SIZE; $x++) {

            $letter = $alpha->current();

            $alpha->next();

            $list[$x] = str_repeat($letter, 64);

        }

        $mem_diff = memory_get_usage() - $start_mem;

        return [$label, (microtime(TRUE) - $start_time),

            number_format($mem_diff)];

    }

  3. We then call the function three times, supplying array, ArrayObject, and SplFixedArray respectively as arguments:

    printf("%14s : %10s : %12s ", '', 'Time', 'Memory');

    $result = testArr([], 'Array');

    vprintf(PATTERN, $result);

    $result = testArr(new ArrayObject(), 'ArrayObject');

    vprintf(PATTERN, $result);

    $result = testArr(

        new SplFixedArray(MAX_SIZE), 'SplFixedArray');

    vprintf(PATTERN, $result);

  4. Here are the results from our PHP 7.1 Docker container:

    root@php8_tips_php7 [ /repo/ch10 ]#

    php php7_spl_fixed_arr_size.php

                   :       Time :       Memory

             Array : 1.19430900 :  129,558,888

       ArrayObject : 1.20231009 :  129,558,832

    SplFixedArray : 1.19744802 :   96,000,280

  5. In PHP 8, the amount of time taken is significantly less, as shown here:

    root@php8_tips_php8 [ /repo/ch10 ]#

    php php7_spl_fixed_arr_size.php

                   :       Time :       Memory

             Array : 0.13694692 :  129,558,888

       ArrayObject : 0.11058593 :  129,558,832

    SplFixedArray : 0.09748793 :   96,000,280

As you can see from the results, PHP 8 handles arrays 10 times faster than PHP 7.1. The amount of memory used is identical between the two versions. What stands out, using either version of PHP, is that SplFixedArray uses significantly less memory than either a standard array or ArrayObject. Let's now have a look at how SplFixedArray usage has changed in PHP 8.

Working with SplFixedArray changes in PHP 8

You might recall a brief discussion on the Traversable interface in Chapter 7, Avoiding Traps When Using PHP 8 Extensions, in the Traversable to IteratorAggregate migration section. The same considerations brought out in that section also apply to SplFixedArray. Although SplFixedArray does not implement Traversable, it does implement Iterator, which in turn extends Traversable.

In PHP 8, SplFixedArray no longer implements Iterator. Instead, it implements IteratorAggregate. The benefit of this change is that SplFixedArray in PHP 8 is faster, more efficient, and also safe to use in nested loops. The downside, and also a potential code break, is if you are using SplFixedArray along with any of these methods: current(), key(), next(), rewind(), or valid().

If you need access to array navigation methods, you now must use the SplFixedArray::getIterator() method to access the inner iterator, from which all of the navigation methods are available. A simple code example, shown here, illustrates the potential code break:

  1. We start by building an SplFixedArray instance from an array:

    // /repo/ch10/php7_spl_fixed_arr_iter.php

    $arr   = ['Person', 'Woman', 'Man', 'Camera', 'TV'];$fixed = SplFixedArray::fromArray($arr);

  2. We then use array navigation methods to iterate through the array:

    while ($fixed->valid()) {

        echo $fixed->current() . '. ';

        $fixed->next();

    }

In PHP 7, the output is the five words in the array:

root@php8_tips_php7 [ /repo/ch10 ]#

php php7_spl_fixed_arr_iter.php

Person. Woman. Man. Camera. TV.

In PHP 8, however, the result is quite different, as seen here:

root@php8_tips_php8 [ /repo/ch10 ]#

php php7_spl_fixed_arr_iter.php

PHP Fatal error:  Uncaught Error: Call to undefined method SplFixedArray::valid() in /repo/ch10/php7_spl_fixed_arr_iter.php:5

In order to get the example working in PHP 8, all you need to do is to use the SplFixedArray::getIterator() method to access the inner iterator. The remainder of the code does not need to be rewritten. Here is the revised code example rewritten for PHP 8:

// /repo/ch10/php8_spl_fixed_arr_iter.php

$arr   = ['Person', 'Woman', 'Man', 'Camera', 'TV'];

$obj   = SplFixedArray::fromArray($arr);

$fixed = $obj->getIterator();

while ($fixed->valid()) {

    echo $fixed->current() . '. ';

    $fixed->next();

}

The output is now the five words, without any errors:

root@php8_tips_php8 [ /repo/ch10 ]#

php php8_spl_fixed_arr_iter.php

Person. Woman. Man. Camera. TV.

Now that you have an idea of how to improve array handling performance, we'll turn our attention to yet another aspect of array performance: sorting.

Implementing stable sort

When designing the logic for array sorting, the original PHP developers sacrificed stability for speed. At the time, this was considered a reasonable sacrifice. However, if complex objects are involved in the sorting process, a stable sort is needed.

In this section, we discuss what stable sort is, and why it's important. If you can ensure that data is stably sorted, your application code will produce more accurate output, which results in greater customer satisfaction. Before we get into the details of how PHP 8 enables stable sorting, we first need to define what a stable sort is.

Understanding stable sorts

When the values of properties used for the purposes of a sort are equal, in a stable sort the original order of elements is guaranteed. Such a result is closer to user expectations. Let's have a look at a simple dataset and determine what would comprise a stable sort. For the sake of illustration, let's assume our dataset includes entries for access time and username:

2021-06-01 11:11:11    Betty

2021-06-03 03:33:33    Betty

2021-06-01 11:11:11    Barney

2021-06-02 02:22:22    Wilma

2021-06-01 11:11:11    Wilma

2021-06-03 03:33:33    Barney

2021-06-01 11:11:11    Fred

If we wish to sort by time, you will note right away that there are duplications for 2021-06-01 11:11:11. If we were to perform a stable sort on this dataset, the expected outcome would appear as follows:

2021-06-01 11:11:11    Betty

2021-06-01 11:11:11    Barney

2021-06-01 11:11:11    Wilma

2021-06-01 11:11:11    Fred

2021-06-02 02:22:22    Wilma

2021-06-03 03:33:33    Betty

2021-06-03 03:33:33    Barney

You'll notice from the sorted dataset that entries for the duplicate time of 2021-06-01 11:11:11 appear in the order they were originally entered. Thus, we can say that this result represents a stable sort.

In an ideal world, the same principle should also apply to a sort that retains the key/value association. One additional criterion for a stable sort is that it should offer no difference in performance compared to an unregulated sort.

Tip

For more information on PHP 8 stable sorts, have a look at the official RFC here: https://wiki.php.net/rfc/stable_sorting.

In PHP 8, the core *sort*() functions and ArrayObject::*sort*() methods have been rewritten to achieve a stable sort. Let's have a look at a code example that illustrates the issue that may arise in earlier versions of PHP.

Contrasting stable and non-stable sorting

In this example, we wish to sort an array of Access instances by time. Each Access instance has two properties, $name and $time. The sample dataset contains duplicate access times, but with different usernames:

  1. First, we define the Access class:

    // /repo/src/Php8/Sort/Access.php

    namespace Php8Sort;

    class Access {

        public $name, $time;

        public function __construct($name, $time) {

            $this->name = $name;

            $this->time = $time;

        }

    }

  2. Next, we define a sample dataset that consists of a CSV file, /repo/sample_data/access.csv, with 21 rows. Each row represents a different name and access time combination:

    "Fred",  "2021-06-01 11:11:11"

    "Fred",  "2021-06-01 02:22:22"

    "Betty", "2021-06-03 03:33:33"

    "Fred",  "2021-06-11 11:11:11"

    "Barney","2021-06-03 03:33:33"

    "Betty", "2021-06-01 11:11:11"

    "Betty", "2021-06-11 11:11:11"

    "Barney","2021-06-01 11:11:11"

    "Fred",  "2021-06-11 02:22:22"

    "Wilma", "2021-06-01 11:11:11"

    "Betty", "2021-06-13 03:33:33"

    "Fred",  "2021-06-21 11:11:11"

    "Betty", "2021-06-21 11:11:11"

    "Barney","2021-06-13 03:33:33"

    "Betty", "2021-06-23 03:33:33"

    "Barney","2021-06-11 11:11:11"

    "Barney","2021-06-21 11:11:11"

    "Fred",  "2021-06-21 02:22:22"

    "Barney","2021-06-23 03:33:33"

    "Wilma", "2021-06-21 11:11:11"

    "Wilma", "2021-06-11 11:11:11"

    You will note, scanning the sample data, that all of the dates that have 11:11:11 as an entry time are duplicates, however, you will also note that the original order for any given date is always users Fred, Betty, Barney, and Wilma. Additionally, note that for dates with a time of 03:33:33, entries for Betty always precede Barney.

  3. We then define a calling program. The first thing to do in this program is to configure autoloading and use the Access class:

    // /repo/ch010/php8_sort_stable_simple.php

    require __DIR__ .

    '/../src/Server/Autoload/Loader.php';

    $loader = new ServerAutoloadLoader();

    use Php8SortAccess;

  4. Next, we load the sample data into the $access array:

    $access = [];

    $data = new SplFileObject(__DIR__

        . '/../sample_data/access.csv');

    while ($row = $data->fgetcsv())

        if (!empty($row) && count($row) === 2)

            $access[] = new Access($row[0], $row[1]);

  5. We then execute usort(). Note that the user-defined callback function performs a comparison of the time properties of each instance:

    usort($access,

        function($a, $b) { return $a->time <=> $b->time; });

  6. Finally, we loop through the newly sorted array and display the result:

    foreach ($access as $entry)

        echo $entry->time . " " . $entry->name . " ";

In PHP 7, note that although the times are in order, the names do not reflect the expected order Fred, Betty, Barney, and Wilma. Here is the PHP 7 output:

root@php8_tips_php7 [ /repo/ch10 ]#

php php8_sort_stable_simple.php

2021-06-01 02:22:22    Fred

2021-06-01 11:11:11    Fred

2021-06-01 11:11:11    Wilma

2021-06-01 11:11:11    Betty

2021-06-01 11:11:11    Barney

2021-06-03 03:33:33    Betty

2021-06-03 03:33:33    Barney

2021-06-11 02:22:22    Fred

2021-06-11 11:11:11    Barney

2021-06-11 11:11:11    Wilma

2021-06-11 11:11:11    Betty

2021-06-11 11:11:11    Fred

2021-06-13 03:33:33    Barney

2021-06-13 03:33:33    Betty

2021-06-21 02:22:22    Fred

2021-06-21 11:11:11    Fred

2021-06-21 11:11:11    Betty

2021-06-21 11:11:11    Barney

2021-06-21 11:11:11    Wilma

2021-06-23 03:33:33    Betty

2021-06-23 03:33:33    Barney

As you can see from the output, in the first set of 11:11:11 dates, the final order is Fred, Wilma, Betty, and Barney, whereas the original order of entry was Fred, Betty, Barney, and Wilma. You'll also notice that for the date and time 2021-06-13 03:33:33, Barney precedes Betty whereas the original order of entry is the reverse. According to our definition, PHP 7 does not implement a stable sort!

Let's now have a look at the same code example running in PHP 8. Here is the PHP 8 output:

root@php8_tips_php8 [ /repo/ch10 ]#

php php8_sort_stable_simple.php

2021-06-01 02:22:22    Fred

2021-06-01 11:11:11    Fred

2021-06-01 11:11:11    Betty

2021-06-01 11:11:11    Barney

2021-06-01 11:11:11    Wilma

2021-06-03 03:33:33    Betty

2021-06-03 03:33:33    Barney

2021-06-11 02:22:22    Fred

2021-06-11 11:11:11    Fred

2021-06-11 11:11:11    Betty

2021-06-11 11:11:11    Barney

2021-06-11 11:11:11    Wilma

2021-06-13 03:33:33    Betty

2021-06-13 03:33:33    Barney

2021-06-21 02:22:22    Fred

2021-06-21 11:11:11    Fred

2021-06-21 11:11:11    Betty

2021-06-21 11:11:11    Barney

2021-06-21 11:11:11    Wilma

2021-06-23 03:33:33    Betty

2021-06-23 03:33:33    Barney

As you can see from the PHP 8 output, for all of the 11:11:11 entries, the original order of entry Fred, Betty, Barney, and Wilma is respected. You'll also notice that for the date and time 2021-06-13 03:33:33, Betty precedes Barney consistently. Thus, we can conclude that PHP 8 performs a stable sort.

Now that you can see the issue in PHP 7, and are now aware that PHP 8 addresses and resolves this issue, let's have a look at the effect on keys in stable sorting.

Examining the effect of stable sorting on keys

The concept of stable sorting also affects key/value pairs when using asort(), uasort(), or the equivalent ArrayIterator methods. In the example shown next, ArrayIterator is populated with 20 elements, every other element being a duplicate. The key is a hexadecimal number that increments sequentially:

  1. First, we define a function to produce random 3-letter combinations:

    // /repo/ch010/php8_sort_stable_keys.php

    $randVal = function () {

        $alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';

        return $alpha[rand(0,25)] . $alpha[rand(0,25)]

               . $alpha[rand(0,25)];};

  2. Next, we load an ArrayIterator instance with sample data. Every other element is a duplicate. We also capture the starting time:

    $start = microtime(TRUE);

    $max   = 20;

    $iter  = new ArrayIterator;

    for ($x = 256; $x < $max + 256; $x += 2) {

        $key = sprintf('%04X', $x);

        $iter->offsetSet($key, $randVal());

        $key = sprintf('%04X', $x + 1);

        $iter->offsetSet($key, 'AAA'); // <-- duplicate

    }

  3. We then perform ArrayIterator::asort() and display the resulting order along with the elapsed time:

    // not all code is shown

    $iter->asort();

    foreach ($iter as $key => $value) echo "$key $value ";

    echo " Elapsed Time: " . (microtime(TRUE) - $start);

Here is the result of this code example running in PHP 7:

root@php8_tips_php7 [ /repo/ch10 ]#

php php8_sort_stable_keys.php

0113    AAA

010D    AAA

0103    AAA

0105    AAA

0111    AAA

0107    AAA

010F    AAA

0109    AAA

0101    AAA

010B    AAA

0104    CBC

... some output omitted ...

010C    ZJW

Elapsed Time: 0.00017094612121582

As you can see from the output, although the values are in order, in the case of duplicate values, the keys appear in chaotic order. In contrast, have a look at the output from the same program code running in PHP 8:

root@php8_tips_php8 [ /repo/ch10 ]#

php php8_sort_stable_keys.php

0101    AAA

0103    AAA

0105    AAA

0107    AAA

0109    AAA

010B    AAA

010D    AAA

010F    AAA

0111    AAA

0113    AAA

0100    BAU

... some output omitted ...

0104    QEE

Elapsed Time: 0.00010395050048828

The output shows that the keys for any duplicate entries appear in the output in their original order. The output demonstrates that PHP 8 implements stable sorting for not only values but for keys as well. Further, as the elapsed time results show, PHP 8 has managed to retain the same (or better) performance as before. Let's now turn our attention to another difference in PHP 8 that directly affects array sorting: the handling of illegal sort functions.

Handling illegal sort functions

PHP 7 and earlier allows developers to get away with an illegal function when using usort() or uasort() (or the equivalent ArrayIterator methods). It's extremely important for you to be aware of this bad practice. Otherwise, when you migrate your code to PHP 8, a potential backward-compatibility break exists.

In the example shown next, the same array is created as in the example described in the Contrasting stable and non-stable sorting section. The illegal sort function returns a Boolean value, whereas the u*sort() callback needs to return the relative position between the two elements. In literal terms, the user-defined function, or callback, needs to return -1 if the first operand is less than the second, 0 if equal, and 1 if the first operand is greater than the second. If we rewrite the line of code the defines the usort() callback, an illegal function might appear as follows:

usort($access, function($a, $b) {

    return $a->time < $b->time; });

In this code snippet, instead of using the spaceship operator (<=>), which would return -1, 0, or 1, we use a less-than symbol (<). In PHP 7 and below, a callback that returns a boolean return value is acceptable and produces the desired results. But what actually happens is that the PHP interpreter needs to add an additional operation to make up the missing operation. Thus, if the callback only performs this comparison:

op1 > op2

The PHP interpreter adds an additional operation:

op1 <= op2

In PHP 8, illegal sort functions spawn a deprecation notice. Here is the rewritten code running in PHP 8:

root@php8_tips_php8 [ /repo/ch10 ]#

php php8_sort_illegal_func.php

PHP Deprecated:  usort(): Returning bool from comparison function is deprecated, return an integer less than, equal to, or greater than zero in /repo/ch10/php8_sort_illegal_func.php on line 30

2021-06-01 02:22:22    Fred

2021-06-01 11:11:11    Fred

2021-06-01 11:11:11    Betty

2021-06-01 11:11:11    Barney

... not all output is shown

As you can see from the output, PHP 8 allows the operation to continue, and the results are consistent when using the proper callback. However, you can also see that a Deprecation notice is issued.

Tip

You can also use the arrow function in PHP 8. The callback shown previously might be rewritten as follows:

usort($array, fn($a, $b) => $a <=> $b).

You now have a greater understanding of what a stable sort is, and why it's important. You also are able to spot potential problems due to differences in handling between PHP 8 and earlier versions. We'll now have a look at other performance improvements introduced in PHP 8.

Using weak references to improve efficiency

As PHP continues to grow and mature, more and more developers are turning to PHP frameworks to facilitate rapid application development. A necessary by-product of this practice, however, is ever larger and more complex objects occupying memory. Large objects that contain many properties, other objects, or sizeable arrays are often referred to as expensive objects.

Compounding the potential memory issues caused by this trend is the fact that all PHP object assignments are automatically made by reference. Without references, the use of third-party frameworks would become cumbersome in the extreme. When you assign an object by reference, however, the object must remain in memory, in its entirety, until all references are destroyed. Only then, after unsetting or overwriting the object, is it entirely destroyed.

In PHP 7.4, a potential solution to this problem was introduced in the form of weak reference support. PHP 8 expanded upon this new ability by adding a weak map class. In this section, you'll learn how this new technology works, and how it can prove advantageous to development. Let's first have a look at weak references.

Taking advantage of weak references

Weak references were first introduced in PHP 7.4, and have been refined in PHP 8. This class serves as a wrapper for object creation that allows the developer to use references to objects in such a manner whereby out-of-scope (for example, unset()) objects are not protected from garbage collection.

There are a number of PHP extensions currently residing on pecl.php.net that provide support for weak references. Most of the implementations hack into the C language structures of the PHP language core, and either overload object handlers, or manipulate the stack and various C pointers. The net result, in most cases, is a loss of portability and lots of segmentation faults. The PHP 8 implementation avoids these problems.

It's important to master the use of PHP 8 weak references if you are working on program code that involves large objects and where the program code might run for a long time. Before getting into usage details, let's have a look at the class definition.

Reviewing the WeakReference class definition

The formal definition for the WeakReference class is as follows:

WeakReference {

    public __construct() : void

    public static create (object $object) : WeakReference

    public get() : object|null

}

As you can see, the class definition is quite simple. The class can be used to provide a wrapper around any object. The wrapper makes it easier to completely destroy an object without fear there may be a lingering reference causing the object to still reside in memory.

Tip

For more information on the background and nature of weak references, have a look here: https://wiki.php.net/rfc/weakrefs. The documentation reference is here: https://www.php.net/manual/en/class.weakreference.php.

Let's now have a look at a simple example to help your understanding.

Using weak references

This example demonstrates how weak references could be used. You will see in this example that when a normal object assignment by reference is made, even if the original object is unset, it still remains loaded in memory. On the other hand, if you assign the object reference using WeakReference, once the original object is unset, it's completely removed from memory:

  1. First, we define four objects. Note that $obj2 is a normal reference to $obj1, whereas $obj4 is a weak reference to $obj3:

    // /repo/ch010/php8_weak_reference.php

    $obj1 = new class () { public $name = 'Fred'; };

    $obj2 = $obj1;  // normal reference

    $obj3 = new class () { public $name = 'Fred'; };

    $obj4 = WeakReference::create($obj3); // weak ref

  2. We then display the contents of $obj2 before and after $obj1 is unset. Because the connection between $obj1 and $obj2 is a normal PHP reference, $obj1 remains in memory due to the strong reference created:

    var_dump($obj2);

    unset($obj1);

    var_dump($obj2);  // $obj1 still loaded in memory

  3. We then do the same for $obj3 and $obj4. Note that we need to use WeakReference::get() to obtain the associated object. Once $obj3 is unset, all information pertaining to both $obj3 and $obj4 is removed from memory:

    var_dump($obj4->get());

    unset($obj3);

    var_dump($obj4->get()); // both $obj3 and $obj4 are gone

Here is the output from this code example running in PHP 8:

root@php8_tips_php8 [ /repo/ch10 ]#

php php8_weak_reference.php

object(class@anonymous)#1 (1) {

  ["name"]=>  string(4) "Fred"

}

object(class@anonymous)#1 (1) {

  ["name"]=>  string(4) "Fred"

}

object(class@anonymous)#2 (1) {

  ["name"]=>  string(4) "Fred"

}

NULL

The output tells us an interesting story! The second var_dump() operation shows us that even though $obj1 has been unset, it still lives on like a zombie because of the strong reference created with $obj2. If you are dealing with expensive objects and complex application code, in order to free up memory, you'll need to first hunt down and destroy all the references before memory is freed!

On the other hand, if you really need the memory, instead of making a direct object assignment, which in PHP is automatically by reference, create the reference using the WeakReference::create() method. A weak reference has all the power of a normal reference. The only difference is that if the object it refers to is destroyed or goes out of scope, the weak reference is automatically destroyed as well.

As you can see from the output, the result of the last var_dump() operation was NULL. This tells us that the object has truly been destroyed. When the main object is unset, all of its weak references go away automatically. Now that you have an idea of how to use weak references, and the potential problem they solve, it's time to take a look at a new class, WeakMap.

Working with WeakMap

In PHP 8, a new class, WeakMap, has been added that leverages weak reference support. The new class is similar to SplObjectStorage in functionality. Here is the official class definition:

final WeakMap implements Countable,

    ArrayAccess, IteratorAggregate {

    public __construct ( )

    public count ( ) : int

    abstract public getIterator ( ) : Traversable

    public offsetExists ( object $object ) : bool

    public offsetGet ( object $object ) : mixed

    public offsetSet ( object $object , mixed $value ) :     void

    public offsetUnset ( object $object ) : void

}

Just like SplObjectStorage, this new class appears as an array of objects. Because it implements IteratorAggregate, you can use the getIterator() method, to gain access to the inner iterator. Thus, the new class offers not only traditional array access, but OOP iterator access as well, the best of both worlds! Before getting into the details of how to use WeakMap, it's important for you to understand typical usage for SplObjectStorage.

Implementing a container class using SplObjectStorage

A potential use for the SplObjectStorage class is to use it to form the basis of a dependency injection (DI) container (also referred to as a service locator or inversion of control container). DI container classes are designed to create and hold instances of objects for easy retrieval.

In this example, we load a container class with an array of expensive objects drawn from the LaminasFilter* classes. We then use the container to sanitize sample data, after which we unset the array of filters:

  1. First, we define a container class based on SplObjectStorage. (Later, in the next section, we develop another container class that does the same thing and is based upon WeakMap.) Here is the UsesSplObjectStorage class. In the __construct() method, we attach configured filters to the SplObjectStorage instance:

    // /repo/src/Php7/Container/UsesSplObjectStorage.php

    namespace Php7Container;

    use SplObjectStorage;

    class UsesSplObjectStorage {

        public $container;

        public $default;

        public function __construct(array $config = []) {

            $this->container = new SplObjectStorage();

            if ($config) foreach ($config as $obj)

                $this->container->attach(

                    $obj, get_class($obj));

            $this->default = new class () {

                public function filter($value) {

                    return $value; }};

        }

  2. We then define a get() method that iterates through the SplObjectStorage container and returns the filter if found. If not found, a default class that simply passes the data straight through is returned:

        public function get(string $key) {

            foreach ($this->container as $idx => $obj)

                if ($obj instanceof $key) return $obj;

            return $this->default;    

        }

    }

    Note that when using a foreach() loop to iterate a SplObjectStorage instance, we return the value ($obj), not the key. If we're using a WeakMap instance, on the other hand, we need to return the key and not the value!

We then define a calling program that uses our newly created UsesSplObjectStorage class to contain the filter set:

  1. First, we define autoloading and use the appropriate classes:

    // /repo/ch010/php7_weak_map_problem.php

    require __DIR__ . '/../src/Server/Autoload/Loader.php';

    loader = new ServerAutoloadLoader();

    use LaminasFilter {StringTrim, StripNewlines,

        StripTags, ToInt, Whitelist, UriNormalize};

    use Php7ContainerUsesSplObjectStorage;

  2. Next, we define an array of sample data:

    $data = [

        'name'    => '<script>bad JavaScript</script>name',

        'status'  => 'should only contain digits 9999',

        'gender'  => 'FMZ only allowed M, F or X',

        'space'   => "  leading/trailing whitespace or ",

        'url'     => 'unlikelysource.com/about',

    ];

  3. We then assign filters that are required for all fields ($required) and filters specific to certain fields ($added):

    $required = [StringTrim::class,

                 StripNewlines::class, StripTags::class];

    $added = ['status'  => ToInt::class,

              'gender'  => Whitelist::class,

              'url'     => UriNormalize::class ];

  4. After that, we create an array of filter instances, used to populate our service container, UseSplObjectStorage. Please bear in mind that each filter class carries a lot of overhead and can be considered an expensive object:

    $filters = [

        new StringTrim(),

        new StripNewlines(),

        new StripTags(),

        new ToInt(),

        new Whitelist(['list' => ['M','F','X']]),

        new UriNormalize(['enforcedScheme' => 'https']),

    ];

    $container = new UsesSplObjectStorage($filters);

  5. We now cycle through the data files using our container class to retrieve filter instances. The filter() method produces a sanitized value specific to that filter:

    foreach ($data as $key => &$value) {

        foreach ($required as $class) {

            $value = $container->get($class)->filter($value);

        }

        if (isset($added[$key])) {

            $value = $container->get($added[$key])

                                ->filter($value);

        }

    }

    var_dump($data);

  6. Finally, we grab memory statistics to form the basis of comparison between SplObjectStorage and WeakMap usage. We also unset $filters, which should theoretically release a sizeable amount of memory. We run gc_collect_cycles() to force the PHP garbage collection process, releasing freed memory back into the pool:

    $mem = memory_get_usage();

    unset($filters);

    gc_collect_cycles();

    $end = memory_get_usage();

    echo " Memory Before Unset: $mem ";

    echo "Memory After  Unset: $end ";

    echo 'Difference         : ' . ($end - $mem) . " ";

    echo 'Peak Memory Usage : ' . memory_get_peak_usage();

Here is the result, running in PHP 8, of the calling program just shown:

root@php8_tips_php8 [ /repo/ch10 ]#

php php7_weak_map_problem.php

array(5) {

  ["name"]=>  string(18) "bad JavaScriptname"

  ["status"]=>  int(0)

  ["gender"]=>  NULL

  ["space"]=>  string(30) "leading/trailing whitespace or"

  ["url"]=>  &string(32) "https://unlikelysource.com/about"

}

Memory Before Unset: 518936

Memory After  Unset: 518672

Difference          :    264

Peak Memory Usage  : 780168

As you can see from the output, our container class works perfectly, giving us access to any of the stored filter classes. What is also of interest is that the memory released following the unset($filters) command is 264 bytes: not very much!

You now have an idea of the typical usage of the SplObjectStorage class. Let's now have a look at a potential problem with the SplObjectStorage class, and how WeakMap solves it.

Understanding the benefits of WeakMap over SplObjectStorage

The main problem with SplObjectStorage is that when an assigned object gets unset or otherwise goes out of scope, it still remains in memory. The reason for this is when the object is attached to the SplObjectStorage instance, it's done by reference.

If you're only dealing with a small number of objects, you'll probably not experience any serious issues. If you use SplObjectStorage and assign a large number of expensive objects for storage, this could eventually cause memory leaks in long-running programs. If, on the other hand, you use a WeakMap instance for storage, garbage collection is allowed to remove the object, which in turn frees up memory. When you start to integrate WeakMap instances into your regular programming practice, you end up with more efficient code that takes up much less memory.

Tip

For more information about WeakMap, have a look at the original RFC here: https://wiki.php.net/rfc/weak_maps. Also have a look at the documentation: https://www.php.net/weakMap.

Let's now rewrite the example from the previous section (/repo/ch010/php7_weak_map_problem.php), but this time using WeakMap:

  1. As described in the previous code example, we define a container class called UsesWeakMap that holds our expensive filter classes. The main difference between this class and the one shown in the previous section is that UsesWeakMap uses WeakMap instead of SplObjectStorage for storage. Here is the class setup and __construct() method:

    // /repo/src/Php7/Container/UsesWeakMap.php

    namespace Php8Container;

    use WeakMap;

    class UsesWeakMap {

        public $container;

        public $default;

        public function __construct(array $config = []) {

            $this->container = new WeakMap();

            if ($config)

                foreach ($config as $obj)

                    $this->container->offsetSet(

                        $obj, get_class($obj));

            $this->default = new class () {

                public function filter($value) {

                    return $value; }};

        }

  2. Another difference between the two classes is that WeakMap implements IteratorAggregate. However, this still allows us to use a simple foreach() loop in the get() method:

        public function get(string $key) {

            foreach ($this->container as $idx => $obj)

                if ($idx instanceof $key) return $idx;

            return $this->default;

        }

    }

    Note that when using a foreach() loop to iterate a WeakMap instance, we return the key ($idx) and not the value!

  3. We then define a calling program that invokes the autoloader and uses the appropriate filter classes. The biggest difference between this calling program and the one from the previous section is that we use our new container class that's based upon WeakMap:

    // /repo/ch010/php8_weak_map_problem.php

    require __DIR__ . '/../src/Server/Autoload/Loader.php';

    $loader = new ServerAutoloadLoader();

    use LaminasFilter {StringTrim, StripNewlines,

        StripTags, ToInt, Whitelist, UriNormalize};

    use Php8ContainerUsesWeakMap;

  4. As in the previous example, we define an array of sample data and assign filters. This code is not shown as it's identical to steps 2 and 3 of the previous example.
  5. We then create filter instances in an array that serves as an argument to our new container class. We use the array of filters as an argument to create the container class instance:

    $filters = [

        new StringTrim(),

        new StripNewlines(),

        new StripTags(),

        new ToInt(),

        new Whitelist(['list' => ['M','F','X']]),

        new UriNormalize(['enforcedScheme' => 'https']),

    ];

    $container = new UsesWeakMap($filters);

  6. Finally, exactly as shown in step 6 from the previous example, we cycle through the data and apply filters from the container class. We also collect and display memory statistics.

Here is the output, running in PHP 8, for the revised program using WeakMap:

root@php8_tips_php8 [ /repo/ch10 ]#

php php8_weak_map_problem.php

array(5) {

  ["name"]=>  string(18) "bad JavaScriptname"

  ["status"]=>  int(0)

  ["gender"]=>  NULL

  ["space"]=>  string(30) "leading/trailing whitespace or"

  ["url"]=>  &string(32) "https://unlikelysource.com/about"

}

Memory Before Unset: 518712

Memory After  Unset: 517912

Difference          :    800

Peak Memory Usage  : 779944

As you might expect, overall memory usage is slightly lower. The biggest difference, however, is the difference in memory after unsetting $filters. In the previous example, the difference was 264 bytes. In this example, using WeakMap produced a difference of 800 bytes. This means that using WeakMap has the potential to free up more than three times the amount of memory compared with using SplObjectStorage!

This ends our discussion of weak references and weak maps. You are now in a position to write code that is more efficient and uses less memory. The larger the objects being stored, the greater the potential for memory saving.

Summary

In this chapter, you learned not only how the new JIT compiler works, but you gained an understanding of the traditional PHP interpret-compile-execute cycle. Using PHP 8 and enabling the JIT compiler has the potential to speed up your PHP application anywhere from three times faster and up.

In the next section, you learned what a stable sort is, and how PHP 8 implements this vital technology. By mastering the stable sort, your code will produce data in a rational manner, resulting in greater customer satisfaction.

The section following introduced you to a technique that can vastly improve performance and reduce memory consumption by taking advantage of the SplFixedArray class. After that, you learned about PHP 8 support for weak references as well as the new WeakMap class. Using the techniques covered in this chapter will cause your applications to execute much quicker, run more efficiently, and use less memory.

In the next chapter, you'll learn how to perform a successful migration to PHP 8.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
107.21.176.63