Re: clarification on the performance of require_once and APC
| From: | Gregory Beaver | Date: | Sun, 26 Aug 2007 18:05:46 +0000 |
| Subject: | Re: clarification on the performance of require_once and APC | ||
| References: | 1 2 | Groups: | php.pear.dev |
| Request: | Send a blank email to pear-dev+get-47840@lists.php.net to get a copy of this message | ||
Alexey Borzov wrote:
> Hi,
>
> Gregory Beaver wrote:
>> I just tracked down Rasmus on IRC and asked him lots of questions about
>> require_once and APC performance. I think that all of the questions
>> raised in June with reference to require_once were addressed. Here are
>> the facts:
>>
>> * The problem that was brought to light by HTML_QuickForm with APC was
>> that some files were being included both conditionally and
>> unconditionally. The way APC solved this was simply to not cache the
>> files or classes within them, obviously resulting in slower
>> performance. An example of this is when an HTML element extends another
>> element that is used. The user require_once's a driver, and the other
>> driver conditionally includes it.
>
> Don't quite understand here, can you elaborate?
Sure. This code:
file1.php
<?php
require_once 'foo.php';
?>
file1.php
<?php
$driver = 'foo';
require_once $driver . '.php';
?>
is a simplistic example of an unconditional and conditional inclusion of
the same file. The problem was when classes have cross-file inheritance
relationships between unconditional and conditional inclusion, sometimes
the same file was included in different ways on different iterations,
resulting in erroneous caching of the class definition. This led to
segmentation faults or worse, random fatal errors due to mis-defined
classes. If you'd like, I can poke Gopal to find out more detail, but
it is actually unsolvable as long as we have both top-level and
conditional require_once.
>> * autoload is slower than direct unconditional include because it is
>> processed at runtime in the exact way a conditional include is processed
>> * There is no way to improve the performance of require_once other than
>> to remove it and use require
>>
>> With these facts in place, it proves that although the performance
>> difference is small for most users, using require_once has an
>> unavoidable performance hit, and the more calls to require_once, the
>> worse the hit, even with APC in its most optimized state. So yes,
>> performance of *_once *is* an issue, contrary to earlier assertions
>> based on Gopal's blog entries.
>
> Even if there *is* a performance issue, we should weigh both the
> (small?) performance improvement and the huge inconvenience for people
> having to do pear -> pear2 move.
What exactly is the huge inconvenience? Let's say we want to use MDB2
with the mysql driver. Old way:
<?php
require_once 'MDB2.php';
$db = MDB2::connect('mysqli://...');
?>
With a clever design, this could be the difference:
New way (beginners):
<?php
require '/path/to/MDB2/allfiles.php';
$db = MDB2::connect('mysqli://...');
?>
New way (intermediate):
<?php
require 'PEAR2/autoload.php';
function __autoload($class)
{
return PEAR2_autoload($class);
}
$db = MDB2::connect('mysqli://...');
?>
New way (advanced):
<?php
include '/full/path/to/MDB2.php';
include '/full/path/to/MDB2/mysqli.php/;
$db = MDB2::connect('mysqli://...');
?>
When I say clever design, I mean doing this kind of check for the driver
inside MDB2:
<?php
(assuming we're in namespace PEAR2 and have imported
PEAR2::MDB2::Exception as Exception)
if (class_exists('MDB2::mysqli', true)) {
return true;
} elseif (file_exists('PEAR2/MDB2/mysqli.php', true)) {
require 'PEAR2/MDB2/mysqli.php';
return true;
} elseif (file_exists(dirname(__FILE__) . '/MDB2/mysqli.php')) {
require dirname(__FILE__) . '/MDB2/mysqli.php';
return true;
} else {
throw new Exception('Cannot locate mysqli driver - have you
installed PEAR2_MDB2_mysqli package?');
}
?>
It is my mistake (and a large one) that the language of the RFC does not
currently allow this kind of design. require should probably be allowed
as a fallback for loading dependencies if they don't exist, the purpose
of the new design is to provide more flexibility, not to limit it.
I will think about better wording, and if you have any suggestions,
please let us know.
>> The best performance is to be achieved by having a single include
>> statement for each needed file in the entire application.
>
> Better yet performance can be achieved by putting everything into a
> single file. We should also consider stripping comments and whitespace
> from this file so that parser's job is easier if the user does not use
> a bytecode cache.
...which is not possible without munging if a file contains any
unconditional top-level "require_once" statements
>> For users who simply wish to try out a package, performance is not an
>> issue, they just need to have an easy way to include all needed
>> files. This should be done on the application level by including
>> allfiles.php,
>> or by setting up an __autoload().
>
> And here I'd like to re-ask one of my questions. Currently people can
> run usage examples and tests right after they install the package.
> After the proposed changes are implemented will they have to mangle
> usage examples to make them run?
No, and in fact if they do, then we need to change the proposal. I
think one solution might be to relax the replacements restriction for
documentation and tests files. The new language would read something like:
replacements are forbidden in files that are intended to be used in
production environment (role="php", role="data", role="www") but are
encouraged in documentation examples and in tests in order to
demonstrate proper usage out-of-the-box.
In other words, by providing two examples and using an install-as tag,
it becomes possible to provide examples for unzip-and-go and for
pyrus-installed. [Note: these are spur-of-the-moment examples, I am
sure better design could be thought of with more thought and
experimentation by others]
unzip_example1.php:
<?php
// if you have not installed this package using pyrus, this is the
example for you
// we are in docs/PEAR2_MDB2/unzip_example1.php
// load needed files
require dirname(__FILE__) . '/../../PEAR2/MDB2/allfiles.php';
import PEAR2::MDB2;
$db = MDB2::connect('mysqli://...');
?>
pyrus_example1.php
<?php
// if you installed this package using pyrus, this is the example for you
// we are in docs/PEAR2_MDB2/unzip_example1.php
set_include_path('@php_dir@');
require 'PEAR2/autoload.php';
function __autoload($class)
{
return PEAR2_autoload($class);
}
import PEAR2::MDB2;
$db = MDB2::connect('mysqli://...');
?>
Tests can use replacements to determine whether the package is installed
or not in a setup file:
<?php
if ('@data_dir@' == '@'.'data_dir@') {
// unzip-and-go
} else {
// installed by pyrus
}
?>
As a side note, PHPUnit and company are not capable of testing both
unzip-and-go and pyrus-installed methodology because they run all tests
in the same request, but phpt tests can easily do this
>> Unlike PEAR, PEAR2 should support both models equally well. With PEAR,
>> the best performance is physically impossible, and beginners often need
>> to require_once several files just to get started.
>
> The "best performance" is not yet achieved, we should put everything
> into a single file, see above.
>
>> There were several synthetic benchmarks posted over the summer that
>> support the relative performance of slow => fast with autoload =>
>> allfiles.php => custom allfiles.php, there's no need to rehash that
>> argument. It is also true that the relative "performance" of ease of
>> getting started is include '/path/to/PEAR2/Package/Name/allfiles.php';
>> => set up include_path/__autoload() => custom allfiles.php. In
>> addition, creating a customized allfiles.php is relatively easy if
>> autoload is possible. Simply use __autoload() and set up a shutdown
>> function that var_dumps() get_included_files().
>
> See above, being unable to run usage examples without mangling them
> sounds like a major degradation in ease of use.
>
>> In short, after extensive research, I still support the initial proposal
>> of removing require_once and adding allfiles.php and a README for each
>> package.
>>
>> In my opinion, the next step is to figure out the best way to make this
>> new system robust in the case of a missing file/class and to document
>> how it should be used.
>>
>> I would like each package to have a top-level README.PackageName file,
>> so that when it is installed or when it is unzipped, this file is easy
>> to find. For instance:
>>
>> pyrus install PEAR2_PackageName
>> PEAR2_PackageName installed into /full/path/to/pear2/ - read
>> README.PEAR2_PackageName to get started
>
> If your target audience is the people who are unable to set up
> include_path then we have strong empirical evidence that they aren't
> reading the docs that are available now in a *single* place on PEAR's
> website. Do you honestly expect them to read those README files
> scattered throughout the file system?
First of all, these will not be scattered throughout the filesystem,
they will be in the base directory. In other words:
unzip PEAR2_Package.zip
ls README*
README.PEAR2_Package
This will mean that the php_dir of pyrus will contain a README for each
installed or unzipped package.
Users who are unaccustomed to PEAR are not by definition stupid just as
people accustomed to PEAR are not by definition smarter. They are
simply used to an older distribution model that is like so:
1) download .zip or .tar.gz
2) extract
3) look for README or INSTALL file and read them both.
As I remember it, include_path is an innovation that was introduced into
PHP in order to support PEAR. Most users use direct inclusion or assume
that they are simply including files relative to the current directory,
without knowing that include_path often contains "."
Also, again, the inability to set up include_path has nothing to do with
the problem. I marked a bug as bogus just this past week because a user
was confused about PEAR and the use of custom configuration files plus
include_path (http://pear.php.net/bugs/bug.php?id=11733). Although the
user was confused, the behavior of PEAR is equally confusing. I have
run up against problems with include_path several times, and each time
it took me a while to debug because I'm so used to include_path being
correct.
Your contention that this proposal is designed to attract stupid people
is a straw man argument - no one benefits from attracting stupid
people. However, there are plenty of smart PHP developers who are
simply not as familiar with include_path because they've never needed to
know about it.
>> the README would contain instructions on using allfiles, or on using
>> __autoload(). This file could be autogenerated at packaging time for
>> packages that don't have a customized one, as it would be the same for
>> most packages.
>>
>> Within each file, dependencies, both internal and external, need to be
>> clearly documented at the top of the file. Whether this should be done
>> with only /** */ or additionally with class_exists() and exceptions or
>> some other code-based solution is a decision that needs to be made. I
>> would prefer beginning users to see a clear and simple error message if
>> they try to directly include a file without a missing dependency.
>> Making this possible in a way that does not adversely affect performance
>> is important.
>
> Making users manage dependencies manually rather than automatically
> including them when needed doesn't sound like an improvement in "ease
> of use" department, either.
Again, this is not a necessary requirement of the new proposal.
The MDB2 allfiles.php could (for example) try to locate its drivers and
include them:
<?php
// include MDB2 files
...
// try to locate drivers
if (file_exists(dirname(__FILE__) . '/Driver')) {
$a = opendir(dirname(__FILE__) . '/Driver');
while (false !== ($entry = readdir($a))) {
if ($entry{0} == '.') continue;
$p = pathinfo($entry);
if (!isset($p['extension'])) continue;
if ($p['extension'] == 'php') require dirname(__FILE__) .
'/Driver/' . $entry;
}
}
?>
The same could go for dependencies. Attempting to locate dependencies
with a simple class_exists() test prior to inclusion could make a true
no-effort allfiles.php.
<?php
// load dependency
if (!class_exists('PEAR2::DepClass')) {
require dirname(__FILE__) . '/../DepClass/allfiles.php';
}
?>
In fact, the same class_exists() test probably should be used for the
hypothetical MDB2 files, so that if the user does include 2 allfiles.php
of a package that uses MDB2 and MDB2, there won't be a class conflict
error. This would result in:
<?php
if (!class_exists('PEAR2::MDB2')) {
// include MDB2 files
}
?>
All that the new proposal mandates is that we stop using top-level
require_once. The rest is experimental solutions to doing things, and
all possibilities should be explored until we find one that makes coding
with PEAR2 packages just as easy as coding with PEAR packages.
Ease of use will not be an issue when all the smart people like you ( :)
) within PEAR figure out new ways of doing things within the new parameters.
One obvious example of a process that would be far simpler without the
problem of top-level require_once munging is the go-pear script.
Currently, it has to do all kinds of non-obvious weird stuff to get
things downloaded, and additionally is very difficult to read. If we
remove all the require_once, go-pear could literally use the PEAR
installer itself by cramming all of the needed files into a single file,
and putting the installation script at the top of the file. This would
even allow us to consider not using a phar archive for PEAR installation
within PHP, but still supporting a single-file installation model.
Thanks,
Greg