
Advanced PHP Programming
.pdf
468 Chapter 19 Synthetic Benchmarks: Evaluating Code Blocks and Functions
Although this routine is complex looking, the idea behind this code is simple: For everything that looks like a tag (that is, a word contained in braces), you perform an evaluated replacement. (The e at the end of your regular expression means that the substitution is evaluated.That is, instead of substituting the text of the replacement block, we execute it with the eval() function, and the result is used for the replacement.) The evaluated expression checks to see whether the suspected tag is a member of the macro set, and if it is, it performs the substitution.This prevents code that looks like a tag but is not (for example, a JavaScript function) from being replaced with whitespace.
The benchmark yields the following:
expand_macros_v3 0.000958 seconds/execution
This seems strange.The code “improvement” (which does fewer regular expression matches) is slower than the original code! What could the problem be?
Unlike Perl, PHP does not have the option to have evaluated substitution expressions be compiled once and executed repeatedly. In Perl this is done with s/$pattern/$sub/eo; the o modifier tells the regular expression to compile $sub only once. PHP allows for similar “compiled” regex capability with the preg_replace_ callback() function, but it is a bit awkward to use in many contexts.
When you use eval on a code block in PHP, it is parsed, compiled, and then executed.The simpler the code that you are using eval on, the less time must be spent in eval.To minimize the cost of using eval on replacement text on every execution, you can attempt to reduce the code to a single function call. Because the function is compiled as part of the main include compilation, you largely avoid the per-call compile overhead. Here is the evaluated substitution that uses a single helper function:
function find_macro($sub, &$macros){
return array_key_exists($sub, $macros)?$macros[$sub]:”{$sub}”;
}
function expand_macros_v4(&$text, &$macroset) { if($text) {
$text = preg_replace(“/\{([^}]+)\}/e”,
“find_macro(‘\\1’, \$macroset)”,
$text);
}
}
You might remember the function tr_replace, which, as its name implies, replaces all occurrences of a given string with a replacement string. Because your token names are fixed, str_replace seems like an ideal tool for your task.You can add it to your benchmark as well:
function expand_macros_v5(&$text, &$macroset) {
if($text) {
$text = str_replace(array_keys($macroset),

Benchmarking Examples |
469 |
array_values($macroset),
$text);
}
}
By benchmarking these with your same macro set (20 macros defined, 5 used in the text), you get the results shown in Figure 19.2 on different message body sizes.
Figure 19.2 A comparison of the linear growth of the token-matching method with the nonlinear growth of the straight preg_replace method.
So, although str_replace() beats preg_replace() when used in the same way, the PHP 4 token-centric method still comes out ahead by a good margin.This is because the token matcher makes only one match, whereas both the str_replace() and preg_replace() methods perform count(array_keys($macroset)) matches.
It is an interesting exercise to find the combination of $macroset size and text size below which it becomes preferable to use the pure str_replace() (PHP5) method. On my system, with documents 4KB in size and smaller, this breaking point was 10 macros. Because you have maintained identical APIs for your expand_macro() implementations, you could even dynamically switch to an optimal implementation based on the size of the macro set, although this would likely be overkill.
The reason that you get much more scalable performance out of the later macro substitution methods is that the pure preg_replace() and str_replace() methods both require O(M*N) work, where M is the number of macros and N is the size of the document.This is because both of these methods must scan the entire document, looking for each of the macros. In contrast, the tokenization methods (version 3 and version 4) only

470 Chapter 19 Synthetic Benchmarks: Evaluating Code Blocks and Functions
need to do O(N) matches (because they only match a single pattern on the document) and then do a series (N at most) of O(1) hash-lookups to determine the substitutions. As the size of the macro set grows smaller, the preg_replace() and str_replace() methods become closer to O(N) in speed, and the cost of calling eval() in the tokenization method becomes more visible.
Interpolation Versus Concatenation
Interpolation of variables is a fancy name for expanding their values in a string.When you use this:
$name = ‘George’;
$string = “Hello $name!\n”;
you cause the current value of $name (‘George’) to be interpolated into the string $string, resulting in it being assigned the value “Hello George!\n”.
At the beginning of this chapter I made a statement that the cost of interpolating variables has dropped in PHP 4.3 and PHP 5.Taking that statement at face value would be contrary to the basic message of this book, so let’s write a quick test to divine the truth. Both string concatenation and variable interpolation in strings are language primitives in PHP. Neither requires calling a function, and both can be expressed in a short sequence of operations in the PHP virtual machine.They are extremely fast. For this reason, using a wrapper function to package them up for calling from your benchmarking harness will skew your results heavily. Even using your MyBench class will introduce significant bias because you still have to wrap them in a userspace function.To address this in the best way possible, you can write a wrapper that does all the iterations itself (in a tight loop, with no function calls at all), and then benchmark that:
require ‘RusageBench.inc’;
function interpolated($name, $iter) { for($i=0;$iter; $i++) {
$string = “Hello $name and have a very nice day!\n”;
}
}
function concatenated($name, $iter) { for($i=0;$iter; $i++) {
$string = “Hello “.$name.” and have a very nice day!\n”;
}
}
$iterations = 100000;
foreach(array(‘interpolated’, ‘concatenated’) as $func) {
$bm = new RusageBench;
$bm-run(1, $func, ‘george’, $iterations);

Benchmarking Examples |
471 |
$result = $bm->get();
printf(“$func\tUser Time + System Time: %0.6f\n”,
($result[mean][ru_utime] + $result[mean][ru_stime])/$iterations);
}
When you run this under PHP 4.2.3, you get the following:
PHP 4.2.3 |
|
|
|
|
|
interpolated |
User |
Time |
+ |
System Time: |
0.000016 |
concatenated |
User |
Time |
+ |
System Time: |
0.000006 |
When you run it under PHP 4.3, you get this:
PHP 4.3 |
|
|
|
|
|
interpolated |
User |
Time |
+ |
System Time: |
0.000007 |
concatenated |
User |
Time |
+ |
System Time: |
0.000004 |
So although you see a significant improvement in the performance of interpolation, it is still faster to use concatenation to build dynamic strings. Chapter 20,“PHP and Zend Engine Internals” which looks at the internals of the Zend Engine (the scripting engine at the core of PHP), also investigates the internal implementation difference between internal and user-defined functions.
A Word of Warning on Focused Tuning
Ahmdahl’s Law is a warning for prospective tuners. Gene Amdahl was a computer scientist at IBM and one of the principal architects on IBM’s S/360 mainframe line. He is perhaps most famous for his discovery of Amdahl’s Law regarding the limit of potential speedup of a program executing in parallel. Amdahl’s Law asserts that if two parts of a program run at different speeds, the slower portion will dominate the runtime. For our use, this translates into the following: The largest gain can be had by optimizing the slowest portions of the code. Or alternatively: There is less to be gained from optimizing code that already accounts for a small portion of the total runtime.


V
Extensibility
20PHP and Zend Engine Internals
21Extending PHP: Part I
22Extending PHP: Part II
23Writing SAPIs and Extending the Zend Engine



476 Chapter 20 PHP and Zend Engine Internals
chapters—but the next chapters will assume a working knowledge of the material covered here. Knowledge of C is not necessary to understand this chapter, although it would certainly help; a large amount of internal engine code in C is excerpted.
How the Zend Engine Works: Opcodes and Op
Arrays
The Zend Engine executes a script by walking it through the following steps:
1.The script is run through a lexical analyzer (often called a lexer) to convert the human-readable code into machine-digestible tokens.These tokens are then passed to the parser.
2.The parser parses the stream of tokens passed to it from the lexer and generates an instruction set (or intermediate code) that runs on the Zend Engine.The Zend Engine is a virtual machine that takes assembly-style, three-address instruction code and executes it. Many parsers generate an abstract syntax tree or parse tree that can then be manipulated or optimized before being passed to the code generator.The Zend Engine parser combines these steps into one and generates intermediate code directly from the tokens passed to it from the lexer.
What Is a Virtual Machine?
The Zend Engine is a virtual machine (VM), which means it is a software program that simulates a physical computer. In a language such as Java, the VM architecture provides portability, allowing you to move compiled bytecode from one machine to another. The Zend Engine has no native support for precompiled programs. A VM provides flexibility to PHP.
In contrast to the 75 base operations on an x86 series processor (what most likely drives your computer), the Zend Engine implements approximately 150 base instructions (called opcodes in Zend language). This instruction set includes not only typical VM instructions such as logical and mathematical operations, but also complex instructions, such as calling include() (a single Zend Engine instruction) and printing a string (also a single instruction).
A VM is always slower than the physical machine it runs on, so extra speed is gained by performing complex instructions as a single VM operation. This is in general called a Complex Instruction Set Computer (CISC) architecture, in contrast to a Reduced Instruction Set Computer (RISC), which uses a small set of simple instructions and relies on being able to execute them extremely quickly.
From the point of view of someone authoring PHP extensions or embedding PHP into applications, this functionality is wrapped into a single phase: compilation. Compilation takes the location of a script and returns intermediate code for it.This intermediate code is (more or less) machine-independent code that one can think of as “assembler code” for the Zend virtual machine.

How the Zend Engine Works: Opcodes and Op Arrays |
477 |
This intermediate code is an ordered array (an op array—short for operations array) of instructions (known as opcodes—short for operation code) that are basically threeaddress code: two operands for the inputs, a third operand for the result, plus the handler that will process the operands.The operands are either constants (representing static values) or an offset to a temporary variable, which is effectively a register in the Zend virtual machine. In the simplest case, an opcode performs a basic operation on its two input operands and stores the result in a register pointed at by the result operand. In a more complex case, opcodes can also implement flow control, resetting the position in the op array for looping and conditionals.
3.After the intermediate code is generated, it is passed to the executor.The executor steps through the op array, executing each quad in turn.
These compilation and execution phases are handled by two separate functions in the Zend Engine: zend_compile and zend_execute.These are both implemented internally as function pointers, which means that you can write an extension that overloads either of these steps with custom code at runtime. (We will explore the why and how of this later in this chapter.)
Here is a representation of the intermediate code for the following simple script:
<?php
$hi = ‘hello’; echo $hi;
?>
opnum |
line |
opcode |
op1 |
op2 |
result |
0 |
2 |
ZEND_FETCH_W |
“hi” |
|
‘0 |
1 |
2 |
ZEND_ASSIGN |
‘0 |
“hello” |
‘0 |
2 |
3 |
ZEND_FETCH_R |
“hi” |
|
‘2 |
3 |
3 |
ZEND_ECHO |
‘2 |
|
|
4 |
5 |
ZEND_RETURN |
1 |
|
|
Note
The intermediate code dumps in this chapter were all generated with a tool call op_dumper. op_dumper is fully developed as an example in Chapter 23, “Writing SAPIs and Extending the Zend Engine.” VLD, developed by Derick Rethans and available at http://www.derickrethans.nl/ vld.php, provides similar functionality.
Here’s what is going on in this script:
nopcode 0—First, you assign Register 0 to be a pointer to the variable named $hi. Then you use ZEND_FETCH_W op because you need to assign to the variable (W is for “write”).