Saturday, April 9, 2011

PHP Efficiency and Debugging in Software Engineering Model

Chapter 28. Efficiency and Debugging

Topics in This Chapter
·         Optimization
·         Measuring Performance
·         Optimize the Slowest Parts
·         When to Store Content in a Database
·         Debugging Strategies
·         Simulating HTTP Connections
·         Output Buffering
·         Output Compression
·         Avoiding eval
·         Don't Load Extensions Dynamically
·         Improving Performance of MySQL Queries
·         Optimizing Disk-Based Sessions
·         Don't Pass by Reference
·         Avoid Concatenation of Large Strings
·         Avoid Serving Large Files with PHP-Enabled Apache
·         Understanding Persistent Database Connections
·         Avoid Using exec, Backticks, and system If Possible
·         Use php.ini-recommended
·         Don't Use Regular Expressions Unless You Must
·         Optimizing Loops
·         IIS Configuration
This chapter touches upon some issues of efficiency and debugging, which are more art than science. Efficiency should not be your first concern when writing code. You must first write code that works, and hopefully your second concern is keeping the code maintainable.
You will pick up some tactical design issues as you gain more experience in programming. These begin to gel as idioms—repeated structures applied to similar problems. Individuals and organizations tend to develop their own idioms, and you will notice them in code found in magazine articles and code repositories. Once you accept an idiom as your own, you can consider it a solved problem. This consistency saves time when writing code and when reading it later.
In most projects, a tiny minority of code is responsible for most of the execution time. Consequently, it pays to measure first and optimize the slowest section. If performance increases to acceptable levels, stop optimizing.
When a bug appears in your script, the time you spent writing meaningful comments and indenting will pay off. Sometimes just reading over troublesome code reveals its flaws. Most of the time you must print incremental values of variables to understand the problem.
Among the many books on the subject, I can recommend two. The first is Writing Solid Code by Steve Maguire. It's oriented toward writing applications in C, but many of the concepts apply to writing PHP scripts. The other is The Practice of Programming by Brian Kernighan and Rob Pike; Chapter 7 will be of particular interest.

 

28.1 Optimization

One reason I like PHP is that it allows the freedom to quickly create Web applications without worrying about following all the rules of proper design. When it comes to rapid prototyping, PHP shines. With this power comes the responsibility to write clean code when it's time to write longer-lasting code. Sticking to a style guide helps you write understandable programs, but eventually you will write code that doesn't execute fast enough.
Optimization is the process of fine-tuning a program to increase speed or reduce memory usage. Memory usage is not as important as it once was, because memory is relatively inexpensive. However, shorter execution times are always desirable.
Before you write a program, commit yourself to writing clearly at the expense of performance. Follow coding conventions, such as using mysql_fetch_row instead of mysql_result. But keep in mind that programming time is expensive, especially when programmers must struggle to understand code. The simplest solution is usually best.
When you finish a program, consider whether its performance is adequate. If your project benefits from a formal requirements specification, refer to any performance constraints. It's not unusual to include maximum page load times for Web applications. Many factors affect the time between clicking a link and viewing a complete page. Be sure to eliminate factors you cannot control, such as the speed of the network.
If you determine that your program needs optimization, consider upgrading the hardware first. This may be the least expensive alternative. In 1965 Gordon Moore observed that computing power doubled every 18 months. It's called Moore's law. Despite the steep increase in power, the cost of computing power drops with time. For example, despite CPU clock speeds doubling, their cost remains relatively stable. Upgrading your server is likely less expensive than hiring programmers to optimize the code.
After upgrading hardware, consider upgrading the software supporting your program. Start with the operating system. Linux and BSD UNIX have the reputation of squeezing more performance out of older hardware, and they may outperform commercial operating systems, especially if you factor in server crashes.
If your program uses a database, consider the differences between relational databases. If you can do without the few advanced features not yet part of MySQL, it may offer a significant performance enhancement over other database servers. Check out the benchmarks provided on their Web site. Also, consider giving your database server more memory.
Two Zend products can help speed execution times of PHP programs. The first is the Zend Optimizer. This optimizes PHP code as it passes through the Zend Engine. It can run PHP programs 40 percent to 100 percent faster than without it. Like PHP, the Zend Optimizer is free. The next product to consider is the Zend Cache. It provides even more performance over the optimizer by keeping compiled code in memory. Some users have experienced 300 percent improvements. Visit the Zend Web site <http://www.zend.com/> to purchase the Zend Cache.

 

 

28.2 Measuring Performance

Before you can begin optimizing, you must be able to measure performance. There are two easy methods: inserting HTML comments and using Apache's ApacheBench utility. PHP applications run on a Web server, but the overhead added by serving HTML documents over a network should be factored out of your measurements.
You need to isolate the server from other activity, perhaps by barring other users or even disconnecting it from the network. Running tests on a server that's providing a public site may give varying results, as traffic changes during the day. Run your tests on a dedicated server even if the hardware doesn't match the production server. Optimizations made on slower hardware should translate into relative gains when put into production.
The easiest method you can use is insertion of HTML comments into your script's output. This method adds to the overall weight of the page, but it doesn't disturb the display. I usually print the output of the microtime function. Insert lines like print("<!--" . microtime() . "-->\n")at the beginning, end, and at key points inside your script. To measure performance, request the page in a Web browser and view the source. This produces lines like those in Figure 28.1.
Figure 28.1 Measuring performance with microtime.
<!-- 0.57843700 1046300374 -->
<!-- 0.71726700 1046300374 -->
<!-- 0.10676900 1046300375 -->
The microtime function returns the number of seconds on the clock. The first figure is a fraction of seconds, and the other is the number of seconds since January 1, 1970. You can add the two numbers and put them in an array, but I prefer to minimize the effect on performance by doing the calculation outside of the script. In the example above, the first part of the script takes approximately 0.14 seconds, and the second part takes 0.39.
If you decide to calculate time differences, consider the method used in Listing 28.1. Entries to the clock array contain a one-word description followed by the output of microtime. The explode function breaks up the three values so the script can display a table of timing values. The first column of the table holds the number of seconds elapsed since the last entry.
Listing 28.1 Calculating microtime differences
<?php
    //start clock
    $clock[] = 'Start ' . microtime();
 
    //fake some long calculation
    $value = 0;
    for($index = 0; $index < 10000; $index++)
    {
        $value += (cos(time()%pi()));
    }
 
    //end clock
    $clock[] = 'cos ' . microtime();
 
 
    //write to file
    $fp = fopen("/tmp/data.txt", "w");
    for($index = 0; $index < 10000; $index++)
    {
        fputs($fp, "Testing performance\n");
    }
    fclose($fp);
 
    //end clock
    $clock[] = 'fputs ' . microtime();
 
    //print clock
    $entry = explode(' ', $clock[0]);
    $lastVal = $entry[1] + $entry[2];
    print('<table border="1">');
    foreach($clock as $c)
    {
        $entry = explode(' ', $c);
 
        print('<tr>');
 
        print('<td>' . ($entry[1] + $entry[2] - $lastVal) .
            '</td>');
        print('<td>' . $entry[0] . '</td>');
        print('<td>' . ($entry[1] + $entry[2]) . '</td>');
        print('</tr>');
 
        $lastVal = $entry[1] + $entry[2];
    }
    print('</table>');
?>
Inserting HTML comments is my favorite method, because it takes no preparation. But its big weakness is a small sample size. I always try three or four page loads to eliminate any variances due to caching or periodic server tasks.
The Apache Web server includes a program that addresses this problem by measuring the number of requests your server can handle. It's called ApacheBench, but the executable is ab. ApacheBench makes a number of requests to a given URL and reports on how long it took. Figure 28.2 shows the results of running 1,000 requests for a simple HTML script. The line in bold is the part I typed into my shell.
I requested an HTML document to get an idea of the baseline performance of my server. Any PHP script ought to be slower than an HTML document. Comparing the figures gives me an idea of the room for improvement. If I found my server could serve a PHP script at 10 requests per second, I'd have a lot of room for improvement.
Keep in mind that I'm running ApacheBench on the server. This eliminates the effects of moving data over the network, but ApacheBench uses some CPU time. I could test from another machine to let the Web server use all the system resources.
By default, ApacheBench makes one connection at a time. If you use 100 for the -n option, it connects to the server 100 times sequentially. In reality, Web servers handle many requests at once. Use the -c option to set the concurrency level. For example, -n 1000 -c 10 makes one thousand connections with 10 requests active at all times. This usually reduces the number of requests the server can handle, but at low levels the server is waiting for hardware, such as the hard disk.
The ApacheBench program is a good way to measure overall change without inconsistencies, but it can't tell you which parts of a script are slower than others. It also includes the overhead involved with connecting to the server and negotiating for the document using HTTP. You can get around this limitation by altering your script. If you comment out parts and compare performance, you can gain an understanding of which parts are slowest. Alternatively, you may use ApacheBench together with microtime comments.
Figure 28.2 ApacheBench output.
# /usr/local/apache/bin/ab -n 10000 http://localhost/50k.html
This is ApacheBench, Version 1.3d <$Revision: 1.65 $> apache-1.3
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/
 
Benchmarking localhost (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Finished 10000 requests
Server Software:        Apache/1.3.26
Server Hostname:        localhost
Server Port:            80
 
Document Path:          /50k.html
Document Length:        51205 bytes
 
Concurrency Level:      1
Time taken for tests:   20.161 seconds
Complete requests:      10000
Failed requests:        0
Broken pipe errors:     0
Total transferred:      514950000 bytes
HTML transferred:       512050000 bytes
Requests per second:    496.01 [#/sec] (mean)
Time per request:       2.02 [ms] (mean)
Time per request:       2.02 [ms] (mean, across all concurrent requests)
Transfer rate:          25541.89 [Kbytes/sec] received
 
Connnection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0     0    0.0      0     2
Processing:     1     1    0.0      1     4
Waiting:        0     0    0.0      0     2
Total:          1     1    0.1      1     4
 
Percentage of the requests served within a certain time (ms)
  50%      1
  66%      1
  75%      1
  80%      1
  90%      1
  95%      1
  98%      1
  99%      1
 100%      4 (last request)
Whichever method you use, be sure to test with a range of values. If your program uses input from the user, try both the easy cases and the difficult ones, but concentrate on the common cases. For example, when testing a program that analyzes text from a textarea tag, don't limit yourself to typing a few words into the form. Enter realistic data, including large values, but don't bother with values so large they fall out of normal usage. People rarely type a megabyte of text into a text area, so if performance drops off sharply when you do, it's probably not worth worrying about.
Remember to measure again after each change to your program, and stop when you achieve your goal. If a change reduces performance, return to an earlier version. Let your measurements justify your changes.



28.3 Optimize the Slowest Parts
Although there are other motivations, such as personal satisfaction, most people optimize a program to save money. Don't lose sight of this as you spend time increasing the performance of your programs. There's no sense in spending more time optimizing than the optimization itself saves. Optimizing an application used by many people is usually worth the time, especially if you benefit from licensing fees. It's hard to judge the value of an open-source application you optimize, but I find work on open-source projects satisfying as recreation.
To make the most of your time, try to optimize the slowest parts of your program where you stand to gain the most. Generally, you should try to improve algorithms by finding faster alternatives. Computer scientists use a special notation called big-O notation to describe the relative efficiency of an algorithm. An algorithm that must examine each input datum once is O(n). An algorithm that must examine each element twice is still called O(n), as linear factors are not interesting. A really slow algorithm might be O(n^2), or O of n-squared. A really fast algorithm might be O(n log n), or n times the logarithm of n. This subject is far too complex to cover here. You can find detailed discussions of this topic on the Internet and in university courses. Understanding it may help you choose faster algorithms.
Permanent storage, such as a hard disk, is much slower than volatile storage, such as RAM. Operating systems compensate somewhat by caching disk blocks to system memory, but you can't keep your entire system in RAM. Parts of your program that use permanent storage are good candidates for optimization.
If you are using data stored in files, consider using a relational database instead. Database servers can do a better job of caching data than the operating system because they view the data with a finer granularity. Database servers may also cache open files, saving you the overhead in opening and closing files.
Alternatively, you can try caching data within your own program, but consider the lifecycle of a PHP script execution. At the end of the request, PHP frees all memory. If during your program you need to refer to the same file many times, you may increase performance by reading the file into a variable.
Consider optimizing your database queries too. MySQL includes the EXPLAIN statement, which returns information about how the join engine uses indexes. MySQL's online manual includes information about the process of optimizing queries.
Here are two tips for loops. If the number of iterations in a loop is low, you might get some performance gain from replacing the loop with a number of statements. For example, consider a for loop that sets 10 values in an array. You can replace the loop with 10 statements, which is a duplication of code but may execute slightly faster.
Also, don't recompute values inside a loop. If you use the size of an array in the loop, use a variable to store the size before you enter the loop instead of calling count each time through the loop. Likewise, look for parts of mathematical expressions that factor into constant values.
Function calls carry a high overhead. You can get a bump in performance if you eliminate a function. Compiled languages, such as C and Java, have the luxury of replacing function calls with inline code. You should avoid functions that you call only once. One technique for readable code is to use functions to hide details. This technique is expensive in PHP.
If all else fails, you have the option of moving part of your code into C, wrapping it in a PHP function. This technique is not for the novice, but many of PHP's functions began as optimizations. Consider the in_array function. You can test for the presence of the value in an array by looping through it, but the function written in C is much faster.

28.4 When to Store Content in a Database
When I speak of content, I mean static text, perhaps containing HTML. There is no rule saying that content should never be placed in a database or that it should always be put in a database. In the case of a bulletin board, it makes sense to put each message in a database. Messages are likely to be added continually. It is convenient to treat them as units, manipulating them by their creation dates or authors. At the other extreme, a copyright message that appears at the bottom of every page of a site is more suited to a text file that is retrieved with the require function.
Somewhere between these two extremes is a break-even point. The reason is that databases provide a tradeoff. They allow you to handle data in complex ways. They allow you to associate several pieces of information around a common identifier. However, you trade away some performance, as retrieving data is slower than if you opened a file and read the contents.
Many Web sites are nothing more than a handful of pages dressed up in a common graphic theme. A hundred files in a directory are easy to manage. You can name each one to describe its contents and refer to it in a URL, such as http://www.example.com/index.php?screen=about_us, and still get the benefit of systematically generating the layout and navigation. Your PHP script can use the value of the screen variable as the name of a local file, perhaps in a directory named screens. Developers can work on the page contents as they are accustomed, because they know the code is stored in a file named about_us in a directory named screens.
When the content grows to a thousand pages, keeping each in a separate file starts to become unmanageable. A relational database will help you better organize the content. With a site so large, it's likely that there will be many versions of the navigation. In a database it is easy to build a table that associates page content with navigation. You can also automate hyperlinks by creating one-way associations between pages. This would cause a link to automatically appear on a page.
The biggest problem with this approach is the lack of good tools for editing the site. Developers are used to fetching files into an editor via FTP. Asking these same people to start using a database shell is most likely out of the question. The cost of teaching SQL to anyone who might work on the site may eliminate any benefit gained when the content is put into the database. So, you are faced with creating your own tools to edit the content. The logical path is to create Web-based tools, since coding a desktop application is a major project in itself, especially if both Windows and Macintosh users are to be accommodated. As you might guess, Web-based site editors are less than ideal. However, with very large sites they become bearable, because the alternative of managing such a large static site is a greater evil, so to speak.


28.5 Debugging Strategies
There are times when code produces unexpected results. Examining the code reveals nothing. In this case the best thing to do is some in-line debugging. PHP scripts generate HTML to be interpreted by a browser, and HTML has a comment tag. Therefore, it is a simple matter to write PHP code that reports diagnostic information inside HTML comments. This allows you to put diagnostic information into an application without affecting its operation.
Often I create database queries dynamically, based on user input. A stray character or invalid user input can cause the query to return an error. Sometimes I print the query itself. I also print the results of the error functions, such as mysql_error. The same applies to code unrelated to databases. Printing diagnostic information, even if it is as simple as saying "got here," can help.
Chapter 9 describes many debugging-related functions. The print_r function can be particularly helpful.
You can go a long way toward finding bugs in your applications by turning on all errors, warnings, and notices. Warnings and notices may not halt your scripts, but they can indicate potential problems. Consider how PHP allows the use of a variable before initializing it. If you mistype the name of a variable, PHP creates a new variable with an empty value. PHP generates a notice if you use the value of a variable before initializing it.
It may be easiest to turn on notices inside php.ini, assuming the Web server is dedicated to development. A production server should not display error messages as a security precaution. You can always turn on full error reporting from within your script with the error_reporting function.
If you don't wish to disturb the HTML output of your scripts, you can write messages to a log file. The error_log and syslog functions are two solutions built into PHP. Of course, you can always open a text file in your code and write diagnostic information. If you use Apache, you can also use the apache_note function to pass debugging information up to the Apache process where it may be included in Apache's logs. Refer to the Apache documentation to learn how to create custom logs.
Finally, there are several tools available for debugging PHP scripts. Zend Studio, for example, includes a remote debugger that allows you to step over each line of your script.


28.6 Simulating HTTP Connections
When writing PHP scripts, it is not necessary to understand every detail of the HTTP protocol. I would be straying to include a treatise here, but you ought to have enough understanding so that you could simulate a connect by using telnet. You may know that Web servers listen on port 80 by default. HTTP is a text-based protocol, so it's not hard to telnet directly to a Web server and type a simple request. HTTP has several commands that should be familiar; GET and POST are used most often. HEAD is a command that returns just the headers for a request. Browsers use this command to test whether they really want to get an entire document.
It is especially helpful to simulate an HTTP connection when your script sends custom headers. Figure 28.3 is an example showing a request I made to an Apache server. The text in bold is what I typed. The remote server returned everything else.
Figure 28.3 Simulating an HTTP connection.
[View full width]
# telnet www.example.com 80
Trying 192.168.178.111...
Connected to www.example.com.
Escape character is '^]'.
HEAD / HTTP/1.0

HTTP/1.1 200 OK
Date: Wed, 26 Feb 2003 23:19:07 GMT
Server: Apache/1.3.26 (Unix) AuthMySQL/2.20 PHP/4.1.2 mod_gzip/1.3.19.1a mod_ssl/2.8.9
graphics/ccc.gif OpenSSL/0.9.6g
X-Powered-By: PHP/4.1.2
Connection: close
Content-Type: text/html

Connection closed by foreign host.
[root@www tmp]#



28.7 Output Buffering
Output buffering is an advanced feature added in PHP 4. Enabling output buffering makes PHP direct the output of applications to a memory buffer instead of sending it directly to the client browser. Once in the buffer, applications can manipulate the output. This manipulation may be compression, conversion from XML to HTML, or even changing embedded URLs. Afterwards, the application emits the processed results to the browser.
Even if you have no need to perform postprocessing on the output your applications emit, output buffering can improve the performance of PHP-based Web sites by decreasing the number of I/O calls to the Web server's infrastructure. Calling the I/O layer of the Web server is typically an expensive operation. Gathering the output into one big block and performing just one I/O operation can be much faster than performing an I/O call every time PHP emits a piece of output—that is, every time you call print or echo.
If your PHP scripts emit HTML pages larger than 10K, allocating and reallocating the buffer can consume more time than is saved from the reduced number of I/O calls. As in many other cases in computer science, you achieve the best performance by finding a good balance between no buffering at all and complete buffering.
Thankfully, PHP's output buffering layer allows users to strike this balance. Instead of telling PHP to buffer all output, you can enable chunked output buffering. Chunked output buffering limits the amount of buffered data to a designated value and flushes the buffer every time the buffer fills up. A good balanced value for chunked output buffering is 4K. It significantly reduces the number of I/O calls your script triggers without consuming significant amounts of memory or imposing noticeable allocation overhead. For instance, if the average size of a PHP-generated HTML page on your site is 50K, PHP will typically perform between 500 and 10,000 I/O calls. With a 4K buffer, it would perform between 12 and 13 I/O calls, resulting in a noticeable gain.
To enable a 4KB output buffer for your entire site, set the output_buffering directive to 4096. If you wish to enable output buffering on a per-script basis, use ob_start. For example, you might write ob_start(null, 4096) to use a 4K buffer.


28.8 Output Compression
Even considering the growing availability of personal broadband Internet access, many sites still address the market of dialup users. If you happen to be running one of them, you probably know that the size of your pages has direct influence on the amount of time your users have to wait before they can see your Web site. Regardless of your Web server's performance, delivery to the client remains at the mercy of the network. Reducing the size of your content reduces the impact of network performance on the overall request-to-response time.
Typically, giving up on certain elements in your Web site just to improve performance is not an option. That is, graphics designers go through their own process of optimizing the design with respect to application requirements. One practical solution is compression of your content. As you would hope, PHP comes to your aid if you need to make use of compression.
PHP's output compression support takes advantage of the fact that most of the popular browsers (including Internet Explorer, Netscape Navigator, and Mozilla) are capable of seamlessly decompressing compressed pages. Such browsers send a special entry in their HTTP request (Accept-Encoding: deflate, gzip), which hints to the server that they know how to handle compressed content. Most servers don't do anything with this information, but with PHP you can easily turn this into smaller pages and faster download times. Document sizes typically reduce by 2 to 10 times!
If enabled, output compression will detect the special entry in the browser's request and will seamlessly compress any output that is emitted by your application. To enable output compression (only for browsers that support it; the behavior for browsers that don't will not be affected), simply turn on the zlib.output_compression directive in php.ini.
If you wish to enable output compression for a specific page only, you can do so with ob_start. For example, ob_start("ob_gzhandler", 4096) activates compression and buffers the output. Note that PHP implements output compression on top of the output buffering mechanism. Unlike regular chunked output buffering, which simply sends out the contents of the output buffer each time it fills up, when output compression is enabled the contents of the buffer go through a special compression filter each time it has to be flushed. The size of the buffer directly affects the efficiency of the compression. If you use a smaller buffer size, compression ratios will be worse. Using larger buffers will usually result in better compression ratios, but typically comes with a price of higher allocation overhead. As with regular output buffering, 4096 bytes is a good, balanced chunk size. Unless you have a good reason to change it, you should stick to the defaults.
Because compressing information is a CPU-intensive task, it only makes sense if
  • your pages are large,
  • a large percentage of your users accesses your Web site over slow connections, and
  • your Web server has CPU cycles to spare.
If some of these factors are not true in your case, enabling output compression may actually decrease the overall performance. In case you're interested in output compression without having to pay the CPU overhead price, consider the Zend Performance Suite. Zend Performance Suite combines output compression with content caching, which means you get all the benefits of output compression without having to wait for the data to compress each time.


28.9 Avoiding eval
Before we get into the gory details, the best way to remember this tip is to remember the catchy phrase eval() is evil. You should do your best to avoid it: eval suffers from slow performance because in order to execute the code, it must invoke the runtime compiler component in the Zend Engine, which is an expensive operation. In many situations, you can replace a call to eval with equivalent code that does not make use of eval.
The most common case where eval can be replaced with faster code is when you use it for accessing variables or functions dynamically. Consider Listing 28.2.
Listing 28.2 Unnecessary use of eval
<?php
    function assign($varname, $value)
    {
        eval("global \$$varname; \$$varname = \$value;");
    }

    for($i=0; $i<100; $i++)
    {
        assign("foo", 5);
        print($foo);
    }
?>
In this example, assign can be used to assign values into variables when you have the variable name handy and not the variable itself. In our case, the eval string will expand to global $foo; $foo = $value; which assigns 5 to the global foo variable, and when we print it, we get 5, as expected. You can achieve the same functionality without using eval by using an indirect reference. See Listing 28.3.
Listing 28.3 Removing unnecessary use of eval
<?php
    function assign($varname, $value)
    {
        global $$varname;
        $$varname = $value;
    }

    for($i=0; $i<100; $i++)
    {
        assign("foo", 5);
        print($foo);
    }
?>
Prefixing variable varname with an extra $ tells PHP to fetch the variable whose name is the value of var. This feature is called an indirect reference. In our case, the value of variable is foo, so PHP globalizes foo and assigns the value to it. Since it doesn't have to invoke the runtime compiler, this new version will yield approximately twice as many requests per second as the eval version!
Another way to eliminate repeated calls to eval involves creating a function dynamically. Let's assume that we have a few lines of code in a variable named code, possibly fetched from a database, passed from a different part of the program or constructed locally. Listing 28.4 shows a naïve implementation.
Listing 28.4 Call to dynamic code with eval
<?php
    //create some example code
    $code = "sqrt(pow(543, 12));";

    for($i=0; $i<100; $i++)
    {
        eval($code);
    }
?>
As mentioned before, this is exceptionally slow. PHP invokes the Zend Engine runtime compiler for each iteration. The technique in Listing 28.5 offers a better solution.
Listing 28.5 Using a dynamic function to eliminate eval
<?php
    //create some example code
    $code = "sqrt(pow(543, 12));";

    //create a function to wrap
    //the loaded code
    $func = create_function('', $code);

    for($i=0; $i<100; $i++)
    {
        $func();
    }
?>

The create_function function creates a new function from the code passed to it, as discussed in Chapter 11. While the results of Listing 28.4 and Listing 28.5 are identical, Listing 28.5 is several times faster. The reason is simple: Listing 28.4 invokes the runtime compiler 100 times, each time we eval the code. Using create_function, the script invokes the runtime compiler only once and declares an anonymous function, which it calls 100 times. This saves 99 invocations of the runtime compiler, which results in a huge performance boost.

 

 

28.10 Don't Load Extensions Dynamically

The dl function allows applications to dynamically load extensions into PHP, thereby adding functionality to the PHP engine. It is the runtime equivalent of extension=/path/to/extension.so in php.ini. However, using dl has many drawbacks over using php.ini. We strongly encourage you not use it.
Dynamically loading a library for each script execution is much slower than doing it once on server startup. You're actually getting hurt twice, because if you load it using the extension directive in php.ini, it gets loaded once for all of the Web server processes instead of being loaded for each process separately.
Due to the nature of memory management under UNIX, loading an extension once on server startup is much more efficient than loading it later, separately for each server process. An extension loaded on server startup, by the Apache parent process, is shared among all the child processes. However, when we load an extension in runtime into specific Web server processes, each copy we load ends up consuming its own chunk of memory, which is not shared with any other process, thereby consuming much more memory.
What if I'm a Windows user and don't care too much about Apache or UNIX?, you may ask. In that case, the motivation for not using dl is even simpler—dl is not supported by the thread-safe version of PHP. Because virtually all of the PHP builds for Windows are built in thread-safe mode, dl is typically not an option if you're a Windows user.

 

 

 

28.11 Improving Performance of MySQL Queries

The mysql_query function is perhaps the most popular function in PHP. If you're a MySQL user, you use it routinely to issue queries to the MySQL server and receive result sets. What you may not know is that when a query returns large result sets or queries large databases, mysql_query can be very inefficient.
In order to understand the reason for the inefficiency, you must understand how mysql_query works. When you issue a SELECT statement using mysql_query, PHP sends it to the MySQL server. The MySQL server parses it, creates an execution plan, and starts to iterate over the table rows, looking for valid results. Every time it finds a valid result, the server sends it back over the network to the client. On the client side, PHP appends each row to a buffer, until the server sends a message that acknowledges that no rows remain. When this happens, mysql_query returns control to the PHP application and allows it to iterate over the result buffer.
The performance problem arises when we deal with large result sets or when we're querying very big databases. In such cases, the time that passes from receiving the first result row and receiving the last one can be quite long. Even though our client is idle and is virtually doing nothing, we cannot use this time to begin processing the results. We have to wait until the server sends the very last row, and only after we get control back can we process the results. If we could start processing the result rows as soon as they start arriving instead of having to wait for the last row, performance would improve significantly. As usual, PHP doesn't disappoint us.
In addition to mysql_query, PHP offers an additional version of the function, named mysql_unbuffered_query. The API for the two functions is identical, but mysql_unbuffered_query does not buffer the result rows before returning control to the PHP application. Instead, it returns control to PHP as soon as it issues the query successfully. Each time we fetch a row, the MySQL module attempts to read the next row from the server and returns control to the application as soon as it fetches the row. That way, we can process the rows as they arrive instead of having to wait for the entire result set to become available.
If unbuffered queries are so good, why does PHP even let you use regular, buffered queries? Unfortunately, there's a good reason for that—unbuffered queries are not always a good idea. If the server sends the rows faster than the client reads them, the server will keep the relevant tables locked for more time than necessary. SQL statements needing to write to the table must wait until the read operation finishes. Since this may result in a huge performance degradation for pages that make changes to the database, using unbuffered queries is recommended only if the amount of processing your pages perform on each row is sufficiently small or if updates are infrequent.



28.12 Optimizing Disk-Based Sessions
Many Web applications use HTTP sessions to retain information about specific users for the duration of their visit. The default and most common storage for the session information is disk files, located in /tmp. With heavily loaded Web sites that serve large number of users, accessing the session store on the disk may become extremely inefficient, since most file systems (including Linux's ext2 and Windows' NTFS) don't handle a large number of files in the same directory very efficiently. As the number of files in the /tmp directory grows due to a large number of active sessions, the time it takes to open each session file becomes longer.
A good first step would be moving the session storage directory from /tmp into a dedicated directory in the file system. You can do that by setting the session.save_path directive in php.ini. Using a different directory removes the overhead of non-PHP sessions-related files if any reside in /tmp. However, this is indeed just a first step and not necessarily a very big one. Given enough active sessions, the number of other files in /tmp may be negligible.
As if out of habit, PHP comes to the rescue and allows you to easily distribute the session files to multiple directories without any hassle. PHP has built-in support to treat the first n letters in the session key as hashing directories. For those of you not familiar with this methodology, let's illustrate. Consider we have a session with the key 3fdb6cd5748e5ef2ecc415530a3f167e. Assuming we've set session.save_path to /tmp/php_sessions, PHP stores this session in a file named /tmp/php_sessions/ sess_3fdb6cd5748e5ef2ecc415530a3f167e. However, if we change php.ini to session.save_path = 2;/tmp/php_sessions, PHP stores the session information in /tmp/php_sessions/3/f/sess_3fdb6cd5748e5ef2ecc415530a3f167e. Note the extra directories separating php_sessions and the session file itself. Similarly, if we set the session.save_path to 4;/tmp/php_sessions, PHP stores the session file in /tmp/php_sessions/3/f/d/b/sess_3fdb6cd5748e5ef2ecc415530a3f167e. The optional semicolon-separated number in session.save_path is named the session save path depth.
Thanks to the exponential nature of this algorithm, the number of files per directory is reduced by a power of 36 that equals the session save path depth, 36 being the number of characters used for session identifiers. This means that there usually isn't a need to go beyond a depth of 2 or 3.
Garbage collection may be improved too. Garbage collection is the process of removing old session files after a certain expiration timeout. By default, PHP takes care of garbage collection automatically. However, due to architectural constraints, PHP's built-in garbage collection takes place inside the context of a request. This means that at least one request will end up being blocked for the duration of the cleanup, which can sometimes take more than a few seconds. Moreover, PHP's automated cleanup supports only the default depth setting of 0. As soon as we move to use a different depth, automated garbage collection will no longer work, and session files will begin to pile up.
The best solution for the garbage collection issue is to move it out of PHP and into a cron job. For instance, if you would like to remove sessions after 24 hours and perform collection every hour, you could add the following line to the system's crontab:
0 * * * * nobody find /tmp/php_sessions -name sess_\* -ctime +1 | xargs rm –f
Using this mechanism works regardless of any session.save_path depth you may be using and prevents any requests from getting stuck for long periods of time due to garbage collection. Of course, you may want to tune the frequency of garbage collection by using different cron settings or change the expiration limit for session file by using different find settings.

 

 

28.13 Don't Pass by Reference (or, Don't Trust Your Instincts)

Telling people not to trust their instincts may be startling, but in the context of PHP it can be good advice. One of the most common examples of a popular bad hunch is the urge to pass variables by reference for performance reasons. Admittedly, it sounds very convincing. Instead of passing a copy of the variable, a script passes the variable itself. That's bound to be faster, isn't it? Well, no. In order to understand why, we need to understand a bit more about how the Zend Engine handles values.
The Zend Engine implements a reference-counted, copy-on-write value system. This means that multiple variables may point to the same value without consuming multiple blocks of memory. Consider Listing 28.6.
Listing 28.6 Zend Engine reference counting
<?php
    //create an array
    $apple = array(1=>'a', 2=>'b', 3=>'c');
 
    //make a copy, ZE keeps one version only
    $ball = $apple;
?>
In this example we assign apple to ball, but PHP copies no data. Instead, it updates ball to point to the same location in memory apple does, a location that contains the array that we originally assigned to apple. For bookkeeping purposes, PHP notifies the array and updates it with a reference count of 2. The Zend Engine takes responsibility to ensure that the reference count of each value in the system reflects the number of symbols referencing it. So much for the reference-counted part. Let's enhance our example with the code in Listing 28.7.
Listing 28.7 Zend Engine splitting references on write
<?php
    //create an array
    $apple = array(1=>'a', 2=>'b', 3=>'c');
 
    //make a copy, ZE keeps one version only
    $ball = $apple;
 
    //apple changes, ZE makes separate versions
    //for apple and ball
    $apple[1] = 'd';
 
    //element 1 of ball remains a
    print($ball[1]);
?>
Of course, we don't expect that modifying apple[1] will change ball[1] and hope that the contents of ball[1] will remain a. If you try running it, you'll find out that indeed it does not get affected by the assignment to apple[1]. But how could this be if we just said that a and ball point to the very same location in memory?
This is where the copy-on-write part kicks in. As soon as the Zend Engine detects a write operation to a value that is referenced by more than one symbol, it replicates the value, creating an identical value that sits in a different place in memory, disconnected from any other symbols. Only then does it allow the write operation to continue. This just-in-time duplication greatly improves performance without any functional side effects thanks to avoiding unnecessary data copies.
How does all of this relate to passing-by-reference being a bad idea? A good start would be understanding why it doesn't help, and the reason is that thanks to the engine's reference-counting mechanism, there's no need to explicitly pass any variables by reference. The engine will automatically avoid unnecessary duplication if at all possible.
Okay, so it's not a good idea. It still doesn't mean it will do any harm—or does it? In reality, it turns out that it does. Let's go back to our apple and ball arrays and add a function that displays their contents. See Listing 28.8.
Listing 28.8 Unnecessary pass by reference
<?php
    //function to print the count of an array
    //passed by reference
    function printArray(&$arr)
    {
        print(count($arr));
    }
 
    //create an array
    $apple = array(1=>'a', 2=>'b', 3=>'c');
 
    //make a copy, ZE keeps one version only
    $ball = $apple;
 
    //print array
    printArray($apple);
?>
Seemingly, there's nothing wrong with this code. It produces the expected results. However, this implementation is roughly 30 percent slower than it would have been if you declared printArray to receive its argument by value instead of by reference. When the engine comes to pass apple to printArray, it detects that it needs to pass it by reference. It then detects that the value in question has a reference count of 2. Since we're passing apple by reference, and any changes that printArray might make must not be reflected in ball, the Zend Engine must make separate copies for apple and ball. If you pass a variable into a function by value, the Zend Engine simply can increment the reference count.
Never use pass-by-reference for performance reasons. Use it only when it makes sense from a functional point of view—let the engine take care of passing arguments!

 

 

 

28.14 Avoid Concatenation of Large Strings

A very common practice in PHP is to needlessly concatenate large chunks of data before printing them. Compare Listing 28.9 to Listing 28.10. The concatenation of subject and contents has to happen before the script calls print, and if the size of contents is very big, it can consume a lot of time. Listing 28.10 calls print multiple times; PHP never needs to concatenate subject and contents in memory, which saves valuable time. Note that since calling print itself has some overhead, it may not always be advisable to separate concatenations into multiple print statements. In certain cases it may also make the code less readable. For that reason, it's best if you follow this practice only when displaying large strings.
Listing 28.9 Concatenation of large strings
<?php
    $subject = "some subject";
    $contents = "...a very large block of text...";
    print("Subject:  $subject\n\n$contents");
?>
Listing 28.10 Avoiding concatenation of large strings
<?php
    $subject = "some subject";
    $contents = "...a very large block of text...";
    print("Subject:  $subject\n\n");
    print($contents);
?>


28.15 Avoid Serving Large Files with PHP-Enabled Apache
This isn't a coding tip, but rather a server setup tip. If your Web site serves large files for downloading, it may be a good idea to set up a special Web server for serving them instead of serving them through the PHP-enabled Apache Web server. There are several reasons for doing so.
Large downloads can take a significant amount of time. The number of concurrent processes that Apache uses is typically limited by a relatively small number. Every Apache process that serves a download file remains unavailable for the duration of the download. This reduces the number of concurrent users that your Web site can handle.
Apache processes consume relatively large amounts of memory for each process, especially if Apache is PHP-enabled. Even if increasing the maximum number of concurrent Apache processes is an option for you, you will be wasting a large amount of memory needlessly.
To set up a download server, consider using the throttling Web server thttpd <http://www.acme.com/software/thttpd/>. It is extremely lightweight and imposes almost no overhead on the server, which makes it one of the most suitable Web servers for serving large amount of


28.16 Understanding Persistent Database Connections
Persistent database connections are one of the least understood features in PHP. Many people don't understand the meaning of persistent links, misconfigure their setup, get beaten by connection problems, and dump persistent connections altogether. Since in many situations, using persistent connections yields significant performance gains, it is important to understand how to properly set them up so that they get a fair trial.
The first important thing to understand about persistent connections is what they are not. Persistent connections are not the same as connection pooling, functionality offered by ODBC, JDBC, and certain database drivers. Connection pooling, the process of juggling a pool of connections to server threads, is not suitable for PHP because the typical PHP environment is not multithreaded. In Apache 1.x (and also in version 2.0, when using the prefork MPM), concurrency is implemented by having several processes. Since database connections cannot be shared among different processes, there's no way to implement connection pooling. That is, if we open a connection in process, it cannot be used by any other process.
As opposed to pooled connections, persistent connections are connections that are simply not closed at the end of the request, as are ordinary connections. Future requests that are handled by the same process can then reuse the opened connection, thus avoiding the overhead of establishing a new database connection in each request. The fundamental difference between pooled and persistent connections is therefore that persistent connections keep one connection open per Web server instance, whereas with pooled connections, a relatively small number of opened connections is shared between all server instances.
Given this knowledge, we can now make more informed decisions about whether it makes sense for us to use persistent connections, and if so, how to set them up. Here are a couple of guidelines to follow.
When using persistent connections, given a long enough uptime, there will be one connection open for each running Apache process. This means that your database server must be able to handle at least as many active connections as your Apache server's MaxClients setting. Having a few extra free connections in excess of MaxClients is best so that you will still be able to connect to the database server for administration purposes.
Persistent connections make sense only if your database server handles a large number of open connections efficiently. Certain database servers suffer from a significant performance hit when working with a large number of open connections, even if they are mostly idle. Other servers may have licensing restrictions on the number of simultaneous connections that can be made to them in any given time. With such servers, persistent connections are not recommended. One example of a server that does handle a large number of simultaneous connections very efficiently is MySQL, so using persistent connections in conjunction with it is highly recommended.

28.17 Avoid Using exec, Backticks, and system If Possible

A common mistake that many PHP programmers make is overusing external processes for tasks that can be performed using PHP's built-in native functions. For instance, exec("/bin/ls –a $dirname", $files), which uses the external /bin/ls program, can be replaced by code in Listing 28.11.
Listing 28.11 Avoiding executing an external process
<?php
    $dir = opendir($dirname);
    while($entry = readdir($dir))
    {
        $files[] = $entry;
    }
?>
Even though it's a few more lines of code, Listing 28.11 is much faster and is also much less prone to security hazards. The exec version requires you to make sure that dirname contains no malicious switches or code that may end up doing something other than you expect.
Whenever you find yourself using exec, system, or backticks, check whether there's a way to implement the same functionality using native PHP code. If it can be done with reasonable effort, always prefer the native PHP approach to external program invocation.


28.18 Use php.ini-recommended
The PHP distribution includes a file named php.ini-recommended alongside the standard php.ini-dist file. Unlike php.ini-dist, which comes preconfigured for PHP's default settings, php.ini-recommended has a list of nonstandard settings, which improve PHP's security and performance. Each nonstandard setting is thoroughly documented in the body of php.ini-recommended, which describes the consequences of enabling it as well as the category of improvement to which this setting is related, such as performance or security. When installing PHP for the first time, or when you want to tune your PHP server for performance, try to use php.ini-recommended.

28.19 Don't Use Regular Expressions Unless You Must
PHP features a very large library of string functions, some of which are extremely powerful. However, in many situations two or more functions can be used to perform the same task, but with great differences in performance.
Perhaps the most commonly overused functions are ereg_replace and preg_replace. These regular-expression-based pattern-replacing functions are often used even when the replacement pattern is completely static and there's no need for compiling a complex regular expression. For instance,
$str = ereg_replace("sheep", "lamb", "Mary had a little sheep");
can be up to 10 times slower than the equivalent
$str = str_replace("Mary had a little sheep", "sheep", "lamb");
Use regular expressions only when you absolutely have to!
If you do have to use a regular expression, try to use the Perl-compatible functions, such as preg_match and preg_replace instead of the older regular expression functions, such as ereg and ereg_replace. Besides being more powerful, the Perl-compatible functions are typically quicker than the old, POSIX regular expressions.


28.20 Optimizing Loops
A very common performance mistake in PHP is creating loops that iterate over an array without caching the number of elements in the array. For example, consider Listing 28.12. The first loop can be optimized to perform about 50 percent faster by caching the value of count($arr) in a variable instead of calling count over and over again. You can even get the count inside the for loop's initialization step. Wherever possible, see if you can take static code, which is invariant of the loop's iterator, out of the loop.
Listing 28.12 Count array elements once
<?php
    //setup sample array
    $arr = array("Cosmo" , "Elaine", "George", "Jerry");

    //loop over elements, recounting each time
    for ($i=0; $i < count($arr); $i++)
    {
        print $arr[$i];
    }

    //loop over elements, make count first
    $n = count($arr);
    for ($i=0; $i < $n; $i++)
    {
        print $arr[$i];
    }

    //put count into init step
    for ($i=0, $n = count($arr); $i < $n; $i++)
    {
        print $arr[$i];
    }
?>


28.21 IIS Configuration
If you have a performance-sensitive PHP server deployed on Microsoft IIS under Windows, you should be aware of the different setup options that IIS allows. The different settings allow you to trade reliability and security for performance.
Inside the your PHP application's properties window in Internet Services Manager, select the Home Directory tab. In that tab, you will see an Application Protection pull-down menu, which determines the isolation level of the application. By default, IIS sets it to Medium, which means that PHP pages will be running in a separate process of IIS. In practice, it means that if PHP experiences a crash, perhaps due to memory corruption or stack overflow, the only the PHP-dedicated IIS process is affected. Other applications are served by other processes and are not effected.
While this setting helps make your server more robust, it comes at the price of a big performance hit. The other applicable Application Protection setting, Low, would make IIS run PHP in the main inetinfo.exe process. Requests will not have to be relayed to external processes, and performance will be dramatically increased. However, the price may come in the form of reduced stability—any PHP crash will bring the entire Web server down with it. Unfortunately, because not all of PHP's modules and the third-party libraries they use are entirely thread-safe, such crashes cannot be avoided.
For a performance-sensitive Web site, we recommend that you first try using PHP with the Low Application Protection setting. Only if you experience trouble should you switch to the Medium setting.

By PHP with No comments

0 comments:

Post a Comment

    • Popular
    • Categories
    • Archives