Bytecode Optimization

When PHP bytecode is cached and does not have to be generated on each and every request then it makes sense to perform more expensive optimizations that improve the performance even further. This is why OpCache can optionally optimize bytecode.

The bytecode optimizations that OpCache performs are controlled by its opcache.optimization_level configuration directive. Its integer value is a bitmask that controls which optimization passes are executed.

As is usual for bitmasks, setting opcache.optimization_level to -1 will always enable all available optimization passes while setting it to 0 will disable optimization entirely.

OpCache’s optimizer first transforms the bytecode into Static Single Assignment (SSA) form. This representation “requires that each variable is assigned exactly once, and every variable is defined before it is used”. Once in this form, Type Inference can be used to automatically detect the type of expressions and variables. Now techniques from control-flow and data-flow analysis can be applied to optimize the bytecode.

The concepts mentioned in the previous paragraph are advanced compiler construction topics. Properly explaining and discussing them is outside the scope of this book. You do not need to understand how OpCache works to benefit from its bytecode optimizations. We’re not going to let you off the hook, though, without showing you at least a couple of examples how OpCache optimizes bytecode.

When a variable is written to and read from only once, respectively, then the bytecode operations to write and read that variable can be eliminated. Consider this example:

function f()
{
    $result = 'result';

    return $result;
}
compiled vars:  !0 = $result
line     #* E I O op          fetch          ext  return  operands
--------------------------------------------------------------------
   4     0  E >   ASSIGN                                  !0, 'result'
   6     1      > RETURN                                  !0
   7     2*     > RETURN                                  null

The original three instructions (see above) can be reduced to a single instruction (see below).

compiled vars:  none
line     #* E I O op          fetch          ext  return  operands
--------------------------------------------------------------------
   6     0  E > > RETURN                                  'result'

Identifying and eliminating a “dead” variable such as $result in the example above is rather simple. OpCache’s bytecode optimizer is able to eliminate more complicated code, too, as the following example will show.

function f(int $a, int $b, int $c)
{
    $d = $a * $b;
    $e = $b * $c;

    return $a + $c;
}

In the example shown above, $d = $a * $b and $e = $b * $c are not used and evaluating these expressions is superfluous. Without so-called escape analysis, however, PHP does not know this and bytecode is generated that performs the respective calculations:

compiled vars:  !0 = $a, !1 = $b, !2 = $c, !3 = $d, !4 = $e
line     #* E I O op          fetch          ext  return  operands
--------------------------------------------------------------------
   2     0  E >   RECV                            !0
         1        RECV                            !1
         2        RECV                            !2
   4     3        MUL                             ~5      !0, !1
         4        ASSIGN                                  !3, ~5
   5     5        MUL                             ~7      !1, !2
         6        ASSIGN                                  !4, ~7
   7     7        ADD                             ~9      !0, !2
         8      > RETURN                                  ~9
   8     9*     > RETURN                                  null

When it performs escape analysis to identify operations that have no effect, OpCache’s bytecode optimizer reduces the original ten instructions (see above) to five instructions (see below).

compiled vars:  !0 = $a, !1 = $b, !2 = $c
line     #* E I O op          fetch          ext  return  operands
--------------------------------------------------------------------
   2     0  E >   RECV                            !0
         1        RECV                            !1
         2        RECV                            !2
   7     3        ADD                             ~3      !0, !2
         4      > RETURN                                  ~3

It goes without saying that you should not have code that can be optimized away like that in the first place. With regard to optimization, the main lesson to be learned is that developers should write readable and maintainable code, and let the compiler take care of optimizations.