14.9. Optimizing Code
This section covers techniques for finding miscellaneous optimizations, including micro-benchmarks, rewriting PHP code in C, and writing procedural versus object-oriented code.
14.9.1. Micro-Benchmarks
Often, you may find yourself wondering which approach is the fastest. For example, which is fasterstr_replace() or preg_replace()for a simple replacement? You can find the answer to many of these questions by writing a little micro-benchmark that measures exactly what you are looking for.
The following example is a library file (ubm.php) to run micro-benchmarks, followed by an example benchmark that tells you which is faster:
<?php
register_shutdown_function('micro_benchmark_summary');
$ubm_timing = array();
function micro_benchmark($label, $impl_func, $iterations = 1) {
global $ubm_timing;
print "benchmarking `$label'...";
flush();
$start = current_usercpu_rusage();
call_user_func($impl_func, $iterations);
$ubm_timing[$label] = current_usercpu_rusage() - $start;
print "<br />\n";
return $ubm_timing[$label];
}
function micro_benchmark_summary() {
global $ubm_timing;
if (empty($ubm_timing)) {
return;
}
arsort($ubm_timing);
reset($ubm_timing);
$slowest = current($ubm_timing);
end($ubm_timing);
print "<h2>And the winner is: ";
print key($ubm_timing) . "</h2>\n";
print "<table border=1>\n <tr>\n <td> </td>\n";
foreach ($ubm_timing as $label => $usercpu) {
print " <th>$label</th>\n";
}
print " </tr>\n";
$ubm_timing_copy = $ubm_timing;
foreach ($ubm_timing_copy as $label => $usercpu) {
print " <tr>\n <td><b>$label</b><br />";
printf("%.3fs</td>\n", $usercpu);
foreach ($ubm_timing as $label2 => $usercpu2) {
$percent = (($usercpu2 / $usercpu) - 1) * 100;
if ($percent > 0) {
printf("<td>%.3fs<br />%.1f%% slower",
$usercpu2, $percent);
} elseif ($percent < 0) {
printf("<td>%.3fs<br />%.1f%% faster",
$usercpu2, -$percent);
} else {
print "<td> ";
}
print "</td>\n";
}
print " </tr>\n";
}
print "</table>\n";
}
function current_usercpu_rusage() {
$ru = getrusage();
return $ru['ru_utime.tv_sec']
+ ($ru['ru_utime.tv_usec'] / 1000000.0);
}
Note
This benchmark library uses the geTRusage() function for measuring consumed CPU cycles. The resolution of the measurements from getrusage() depends on your system setup, but is usually 1/100th of a second (1/1000th of a second on FreeBSD).
This is a potential source of error, so make sure you run your micro-benchmark several times with similar results before accepting the outcome.
Here is the str_replace() versus preg_replace() micro-benchmark:
<?php
require 'ubm.php';
$str = "This string is not modified";
$loops = 1000000;
micro_benchmark('str_replace', 'bm_str_replace', $loops);
micro_benchmark('preg_replace', 'bm_preg_replace', $loops);
function bm_str_replace($loops) {
global $str;
for ($i = 0; $i < $loops; $i++) {
str_replace("is not", "has been", $str);
}
}
function bm_preg_replace($loops) {
global $str;
for ($i = 0; $i < $loops; $i++) {
preg_replace("/is not/", "has been", $str);
}
}
The output from this example appears in Figure 14.15.
According to this micro-benchmark, str_replace() is only 20 percent faster than preg_replace() for simple string substitutions.
Micro-benchmarks are best suited for operations that require little or no I/O activity. After you start performing I/O from benchmarks, your results may be skewed; other processes that involve reading or writing to disk may slow down your test, or a database query that is cached in memory could inflate the speed of your benchmark.
It is a good idea to measure several times and verify that you receive similar results each time. If not, what you are doing is not well-suited for a micro-benchmark, or the machine you are running it on could be running with loads that affects the benchmark.
Tip
Don't throw away your micro-benchmarks! Keep and organize them somewhere, so you can run them all again later to see if a function was optimized (or broken!) in a new PHP release.
14.9.2. Rewrite in C
Sometimes, it is just not possible to optimize a piece of PHP code. The code is as fast as it possibly can be in PHP, but it may still be a bottleneck. This is the time to wield your axe, chop it to bits, and rewrite it in C as a PHP extension. If you have some C skills, it's not that hard. Consult Chapter 15, "An Introduction to Writing PHP Extensions," for examples.
14.9.3. OO Versus Procedural Code
PHP has the advantage of not forcing a particular coding style. You can write 100 percent procedural code, or you can go all object-oriented. Most likely, you are going to end up writing code that is somewhere in between procedural and object-oriented, because most of the functionality provided by PHP's bundled extensions is procedural, while PEAR offers OOP interfaces.
From a performance point of view, procedural code is slightly faster. The following example shows another micro-benchmark that compares the performance difference between regular function calls and method calls:
<?php
require 'ubm.php';
class Adder {
function add2($a, $b) { return $a + $b; }
function add3($a, $b, $c) { return $a + $b; }
}
function adder_add2($a, $b) { return $a + $b; }
function adder_add3($a, $b) { return $a + $b; }
function run_oo_bm2($count) {
$adder = new Adder;
for ($i = 0; $i < $count; $i++) $adder->add2(5, 7);
}
function run_oo_bm3($count) {
$adder = new Adder;
for ($i = 0; $i < $count; $i++) $adder->add2(5, 7, 9);
}
function run_proc_bm2($count) {
for ($i = 0; $i < $count; $i++) adder_add2(5, 7);
}
function run_proc_bm3($count) {
for ($i = 0; $i < $count; $i++) adder_add3(5, 7, 9);
}
$loops = 1000000;
micro_benchmark("proc_2_args", "run_proc_bm2", $loops);
micro_benchmark("proc_3_args", "run_proc_bm3", $loops);
micro_benchmark("oo_2_args", "run_oo_bm2", $loops);
micro_benchmark("oo_3_args", "run_oo_bm3", $loops);
Figure 14.16 shows the result.
Here, function calls are 1112 percent faster than method calls with both two and three arguments.
Keep in mind that this micro-benchmark only measures the overhead caused by the actual function call (looking up the function/method name, passing parameters, returning a value).
This will be a performance factor if your code has many small functions, which makes the call overhead account for a larger portion of the total execution time.
|