Ïðèãëàøàåì ïîñåòèòü
Áèàíêè (bianki.lit-info.ru)

Optimizing PHP Code

Table of Contents
Previous Next

Optimizing PHP Code

Programmers mention preoptimization tasks as "I'll change this vector to a hash table to improve performance" or "I'll change this ereg() function to several str_replace() functions, they will be faster". They are very wrong.

First of all, these kinds of modifications usually improve the script performance by less than 0.01 seconds, so they are not very important and could decrease code readability a lot. Second, preoptimization is the most common way to waste precious programming time.

Optimization is good practice only when we know that optimization is really needed. We need a method to increase performance, and the professional approach is:

Profiling Code

Once a performance problem is detected, the first task should be the construction of a code profile, where we measure the amount of time that the script uses for several tasks and functions. We can then use this information to find where the script is spending most of its time. These troublesome functions are called bottlenecks.

After profiling we can find a lot of surprises, such as a script using 99% of its time in a database query, or other really important bottlenecks such as disk I/O for reading big files or logs. Optimization is not a serious programming technique without a good profiling job.

How to Profile PHP Scripts

To make a profile for a PHP script, we need to put timers around several functions and tasks of the script to measure how long the function takes to execute.

Let's assume we have the following PHP script:

    <?php
    // Some initializations
    require_once("some_class.php");

    $foo = new some_class();

    $res1 = $foo->do_a_method();
    $res2 = $foo->do_a_method2();

    echo($res1);
    echo($res2);
    ?>

If we are having a performance issue with this script there are three suspects – the first suspect is the some_class() constructor called when we create the $foo object.

The second and third suspects are the two methods called. So if we know that the script is taking 2.3 seconds to execute, we want to find how that time is distributed between the three suspects.

A fourth minor suspect can be the time used in the require_once() call, if too many files are included through some_class(). For the purpose of this chapter, we are going to assume that the require_once() call takes very little of the 2.3 seconds the script is using.

PHP has a time() function which returns the number of seconds since 1970. However, this can hardly be useful because a lot of scripts will execute in less than a second and in scripts taking seconds to execute, we need more precision than this to find where the time is used. The function that we have to use is microtime().

The Microtime Function

microtime() returns a string in the form "msec sec" where msec is the microsecond part and sec is the number of seconds since 1970, for example:

    <?php
    echo(microtime());
    ?>

This might return:

0.15672253 987612546

This is not very useful to us when trying to work out differences so we have to manipulate the result as following:


    <?php
    $time_portions = explode(' ', microtime());
    $actual_time = $time_portions[1] . substr($time_portions[0], 1);
    echo($actual_time);
    ?>

With that code we transform:

0.15672253 987612546

into:

987612546.15672253

Now we have to get that number before and after executing a function or code portion, and find the difference. The bc_sub() function is used to get the difference with enough precision as following:

    <?php
    $elapsed_time = bcsub($end_time, $start_time, 6);
    ?>
Important 

To test this script the bcmath extension should be compiled into PHP with --enable-bc-math option.

Here 6 is the number of decimal places that we'd like to keep in the result.

Building a Timer Class

It is not necessary to have a timing class, if we already have a profiler tool for PHP.

The class maintains multiple timers. The basic functions are:

  • timerStart()
    Lets us start a new timer, or leave it as a default timer. To start a named timer we use timer_start(‘foo’).

  • timerStop()
    Stops the given timer or the default timer, if no name is given. It returns the elapsed time for the timer.

    <?php
    class Timer
    {
        var $timers = array();

        function Timer()
        {
            // Nothing
        }
        function timerStart($name = 'default')
        {
            $time_portions = explode(' ', microtime());
            $actual_time = $time_portions[1] . substr($time_portions[0], 1);
            $this->timers['$name'] = $actual_time;
        }

        function timerStop($name = 'default')
        {
            $time_portions = explode(' ', microtime());
            $actual_time = $time_portions[1] . substr($time_portions[0], 1);
            $elapsed_time = bcsub($actual_time, $this->timers['$name'], 6);
            return $elapsed_time;
        }
    }
    ?>

The Timer class uses the bcsub() function to subtract a number from another ensuring extended precision arithmetic.

If you see error_message(undefined function bcsub..) then you don't have the bcmath functions compiled into PHP. Add –enable-bcmath to your configuration string, recompile PHP, and try again.

Important 

Here we checked out the basic working of a timer class. Another timing class is available as a part of PEAR in the pear/Benchmark/Timer.php file of the PHP installation. The PEAR class is more complete and complex.

Write another class Time_Info that displays the time taken in executing the function phpinfo() and the multiply() function that multiplies a large number:

    <?php
    // This test class displays the time taken in executing a function
    // containing phpinfo() and another function multiplying a large number.
    class Time_Info
    {
        // Constructor
        function Time_Info(){}
        // Method 1: containing the builtin function phpinfo()
        function phpinf()
        {
            phpinfo();
        }
        // Method 2: multiplying large numbers
        function multiply()
        {
            $multiplied=10000*10000*10000*10000;
        }
    }
    ?>

Let's now write an advanced script, based on the above sample. Here, we embrace each suspect between timerStart() and timerStop() calls to measure the amount of time each suspect takes:

    <?php
    // Some initializations
    require_once("Timer.php");
    require_once("Time_Info.php");

    $tim = new Timer();
    $tim->timerStart('total');

    $tim->timerStart();
    $foo = new Time_Info();
    print("Constructor: " . $tim->timerStop() . "<br>");

    $tim->timerStart();
    $res1 = $foo->phpinf();
    print("Method1: " . $tim->timerStop() . "<br>");

    $tim->timerStart();
    $res2 = $foo->multiply();
    print("Method2: " . $tim->timerStop() . "<br>");

    echo($res1);
    echo($res2);

    print("Total execution time: " . $tim->timerStop('total') . "<br>");
    ?>

Let's check the time that the constructor, php_inf(), and multiply() took, as well as the total script time.

Constructor: 0.000084

Method1: 0.037100
Method2: 0.000101
Total execution time: 0.037980

By running this test several times we can find all the information we need to determine which of the three functions should be optimized. Thus we have constructed a profile for the problematic script.

Classify Bottlenecks

Profiling is used to find bottlenecks, and once bottlenecks are found they should be classified according to several factors. First, the severity of each bottleneck should be measured. Second, estimate the complexity of optimizing each one.

Taking these factors into consideration decides which functions, portions of code, or methods of the script should be optimized.


Table of Contents
Previous Next