为什么require_once如此糟糕?

时间:2008-10-09 08:03:03

标签: php performance require-once

我读到的关于更好的PHP编码实践的所有内容一直都说不要因为速度而使用require_once

为什么会这样?

require_once做同样事情的正确/更好的方法是什么?如果重要,我正在使用PHP5。

14 个答案:

答案 0 :(得分:146)

这个帖子让我感到畏缩,因为已经发布了“解决方案”,而且,就所有意图和目的而言,这都是错误的。我们列举一下:

  1. 在PHP中定义真的昂贵。您可以look it up或自己测试,但在PHP中定义全局常量的唯一有效方法是通过扩展。 (类常量实际上是相当不错的性能,但这是一个没有实际意义的点,因为2)

  2. 如果您正确使用require_once(),也就是说,为了包含类,您甚至不需要定义;只需检查class_exists('Classname')。如果您所包含的文件包含代码,即您以程序方式使用它,则绝对没有理由认为require_once()对您来说是必要的;每次包含您认为要进行子程序调用的文件时。

  3. 所以有一段时间,很多人确实使用class_exists()方法来包含它们。我不喜欢它,因为它很难,但他们有充分的理由:require_once()在一些最新版本的PHP之前效率很低。但是这已得到修复,我认为你必须为条件和额外方法调用编译的额外字节码远远超过任何内部哈希表检查。

    现在,承认:这个东西很难测试,因为它占执行时间很少。

    这是你应该考虑的问题:作为一般规则,包含在PHP中很昂贵,因为每次解释器命中一个时它必须切换回解析模式,生成操作码,然后跳回。如果您有100多个包含,这肯定会对性能产生影响。使用或不使用require_once的原因是如此重要的问题是因为它使操作码缓存变得困难。可以在这里找到explanation for this,但归结为:

    • 如果在解析时,您确切知道请求的整个生命周期中需要包含哪些文件,require()最开始的那些文件和操作码缓存将为您处理其他所有内容。< / p>

    • 如果您没有运行操作码缓存,那么您将陷入困境。将所有包含内容整合到一个文件中(不要在开发期间执行此操作,仅在生产中)可以帮助解析时间,但这样做很麻烦,而且,您需要确切地知道您将在请求。

    • Autoload非常方便,但速度很慢,因为每次完成包含时都必须运行自动加载逻辑。实际上,我发现为一个请求自动加载几个专用文件不会造成太多问题,但是你不应该自动加载你需要的所有文件。

    • 如果您有10个包含(这是信封计算的非常背面),所有这些都不值得:只需优化您的数据库查询等。

答案 1 :(得分:104)

require_onceinclude_once都要求系统记录已包含/需要的内容。每个*_once调用都意味着检查该日志。所以肯定在那里做了一些额外的工作,但足以损害整个应用程序的速度?

......我真的很怀疑......除非您使用真正的旧硬件或者批次

如果你 做了数以千计的*_once,你可以用更轻松的方式自己完成工作。对于简单的应用程序,只需确保只有

if (!defined('MyIncludeName')) {
    require('MyIncludeName');
    define('MyIncludeName', 1);
}

我个人会坚持使用*_once语句,但是在愚蠢的百万通过基准测试中,您可以看到两者之间存在差异:

                php                  hhvm
if defined      0.18587779998779     0.046600103378296
require_once    1.2219581604004      3.2908599376678

require_once慢了10-100倍,而require_once hhvm看起来似乎更慢,这很奇怪。同样,如果您运行*_once数千次,这仅与您的代码相关。


<?php // test.php

$LIMIT = 1000000;

$start = microtime(true);

for ($i=0; $i<$LIMIT; $i++)
    if (!defined('include.php')) {
        require('include.php');
        define('include.php', 1);
    }

$mid = microtime(true);

for ($i=0; $i<$LIMIT; $i++)
    require_once('include.php');

$end = microtime(true);

printf("if defined\t%s\nrequire_once\t%s\n", $mid-$start, $end-$mid);

<?php // include.php

// do nothing.

答案 2 :(得分:64)

我好奇并检查了Adam Backstrom与Tech Your Universe的链接。本文介绍了应该使用require而不是require_once的原因之一。但是,他们的说法并不符合我的分析。我有兴趣看到我可能错误地分析了解决方案。我使用php 5.2.0进行比较。

我首先创建了100个使用require_once包含另一个头文件的头文件。每个文件看起来都像:

<?php
// /home/fbarnes/phpperf/hdr0.php
require_once "../phpperf/common_hdr.php";

?>

我使用快速bash hack创建了这些:

for i in /home/fbarnes/phpperf/hdr{00..99}.php; do
  echo "<?php
// $i" > $i
  cat helper.php >> $i;
done

这样我可以轻松地在使用require_once和require包含头文件时进行交换。然后我创建了一个app.php来加载一百个文件。这看起来像:

<?php

// Load all of the php hdrs that were created previously
for($i=0; $i < 100; $i++)
{
  require_once "/home/fbarnes/phpperf/hdr$i.php";
}

// Read the /proc file system to get some simple stats
$pid = getmypid();
$fp = fopen("/proc/$pid/stat", "r");
$line = fread($fp, 2048);
$array = split(" ", $line);

// write out the statistics; on RedHat 4.5 w/ kernel 2.6.9
// 14 is user jiffies; 15 is system jiffies
$cntr = 0;
foreach($array as $elem)
{
  $cntr++;
  echo "stat[$cntr]: $elem\n";
}
fclose($fp);

?>

我将require_once标头与使用头文件的require标头进行了对比:

<?php
// /home/fbarnes/phpperf/h/hdr0.php
if(!defined('CommonHdr'))
{
  require "../phpperf/common_hdr.php";
  define('CommonHdr', 1);
}

?>

使用require与require_once运行时,我没有发现太大的区别。事实上,我的初步测试似乎暗示require_once略快,但我不一定相信。我用10000输入文件重复了实验。在这里,我确实看到了一致的差异。我多次运行测试,结果很接近,但使用require_once平均使用30.8个用户jiffies和72.6个系统jiffies;使用require平均使用39.4用户jiffies和72.0系统jiffies。因此,使用require_once看起来负载略低。但是,挂钟略有增加。 10,000个req​​uire_once调用平均使用10.15秒完成,10,000个需要调用平均使用9.84秒。

下一步是研究这些差异。我使用strace来分析正在进行的系统调用。

在从require_once打开文件之前,会进行以下系统调用:

time(NULL)                              = 1223772434
lstat64("/home", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
lstat64("/home/fbarnes", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
lstat64("/home/fbarnes/phpperf", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
lstat64("/home/fbarnes/phpperf/h", {st_mode=S_IFDIR|0755, st_size=270336, ...}) = 0
lstat64("/home/fbarnes/phpperf/h/hdr0.php", {st_mode=S_IFREG|0644, st_size=88, ...}) = 0
time(NULL)                              = 1223772434
open("/home/fbarnes/phpperf/h/hdr0.php", O_RDONLY) = 3

这与要求相反:

time(NULL)                              = 1223772905
lstat64("/home", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
lstat64("/home/fbarnes", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
lstat64("/home/fbarnes/phpperf", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
lstat64("/home/fbarnes/phpperf/h", {st_mode=S_IFDIR|0755, st_size=270336, ...}) = 0
lstat64("/home/fbarnes/phpperf/h/hdr0.php", {st_mode=S_IFREG|0644, st_size=146, ...}) = 0
time(NULL)                              = 1223772905
open("/home/fbarnes/phpperf/h/hdr0.php", O_RDONLY) = 3

Tech Your Universe暗示require_once应该进行更多的lstat64调用。但是,它们都进行了相同数量的lstat64调用。可能的不同之处在于我没有运行APC来优化上面的代码。但是,我接下来要做的是比较整个运行的strace输出:

[fbarnes@myhost phpperf]$ wc -l strace_1000r.out strace_1000ro.out 
  190709 strace_1000r.out
  210707 strace_1000ro.out
  401416 total

使用require_once时,每个头文件大约有两个系统调用。一个区别是require_once还有一个对time()函数的附加调用:

[fbarnes@myhost phpperf]$ grep -c time strace_1000r.out strace_1000ro.out 
strace_1000r.out:20009
strace_1000ro.out:30008

另一个系统调用是getcwd():

[fbarnes@myhost phpperf]$ grep -c getcwd strace_1000r.out strace_1000ro.out 
strace_1000r.out:5
strace_1000ro.out:10004

这是因为我决定在hdrXXX文件中引用的相对路径。如果我将它作为绝对引用,那么唯一的区别是代码中的附加时间(NULL)调用:

[fbarnes@myhost phpperf]$ wc -l strace_1000r.out strace_1000ro.out 
  190705 strace_1000r.out
  200705 strace_1000ro.out
  391410 total
[fbarnes@myhost phpperf]$ grep -c time strace_1000r.out strace_1000ro.out
strace_1000r.out:20008
strace_1000ro.out:30008

这似乎暗示您可以通过使用绝对路径而不是相对路径来减少系统调用的数量。除此之外的唯一区别是时间(NULL)调用,它们似乎用于检测代码以比较更快的代码。

另一个注意事项是APC优化包有一个名为“apc.include_once_override”的选项,它声称它减少了require_once和include_once调用所进行的系统调用次数(参见PHP docs)。 p>

对不起,很长的帖子。我很好奇。

答案 3 :(得分:20)

您能否告诉我们这些编码实践的任何链接,以避免它?就我而言,这是一个完整的非问题。我自己没有查看过源代码,但我认为includeinclude_once之间的唯一区别是include_once将该文件名添加到数组并检查数组每一次。保持该数组排序很容易,因此搜索它应该是O(log n),即使是中等大的应用程序也只有几十个包含。

答案 4 :(得分:6)

更好的方法是使用面向对象的方法并使用__autoload()

答案 5 :(得分:5)

*_once()函数统计每个父目录,以确保您所包含的文件与已包含的文件不同。这是放缓原因的一部分。

我建议使用Siege之类的工具进行基准测试。您可以尝试所有建议的方法并比较响应时间。

require_once() Tech Your Universe的更多内容。

答案 6 :(得分:5)

PEAR2 wiki(当它存在时)用于列出good reasons for abandoning all the require/include directives in favor of autoload,至少对于库代码而言。当phar等替代包装模型即将出现时,这些将您归结为严格的目录结构。

更新:由于wiki的网络存档版本令人眼花缭乱,我复制了以下最引人注目的理由:

  
      
  • include_path是使用(PEAR)包所必需的。这使得很难将PEAR包与另一个应用程序捆绑在一起   拥有include_path,用于创建包含所需类的单个文件,   将PEAR包移动到没有大量源代码的phar存档   修改
  •   
  • 当顶级require_once与条件require_once混合时,这可能导致代码无法通过操作码缓存(例如   APC,将与PHP 6捆绑在一起。
  •   
  • 相对require_once要求include_path已经设置为正确的值,因此无法使用包   适当的include_path
  •   

答案 7 :(得分:5)

它没有使用坏的功能。在整个代码库中,对如何以及何时使用它的理解是错误的。我只是为可能被误解的概念添加更多的上下文:

人们不应该认为require_once是一个缓慢的功能。您必须以某种方式包含您的代码。 require_once()require()的速度不是问题。这是关于阻碍可能导致盲目使用它的警告的性能。如果广泛使用而不考虑上下文,则可能导致大量内存浪费或浪费代码。

我所看到的非常糟糕,当巨大的单片框架以错误的方式使用require_once()时,尤其是在复杂的面向对象环境中。

以每个类顶部使用require_once()为例,如许多库中所示:

require_once("includes/usergroups.php");
require_once("includes/permissions.php");
require_once("includes/revisions.php");
class User{
  //user functions
}

因此User类旨在使用所有其他3个类。很公平! 但是现在如果访问者正在浏览网站甚至没有登录并且框架加载了require_once("includes/user.php");每个请求的话。

它包括1 + 3 不必要的类,它们在该特定请求期间不会使用。这是一个膨胀的框架最终使用每个请求40MB而不是5MB或更少。


其他被滥用的方式,就是当一个类被其他人重复使用时! 假设您有大约50个使用helper函数的类。为确保helpers在加载时可用于这些类,您将获得:

require_once("includes/helpers.php");
class MyClass{
  //Helper::functions();//etc..
}

这里没有任何错误。但是,如果一个页面请求恰好包含15个类似的类。您正在运行require_once 15次,或者视觉效果很好:

require_once("includes/helpers.php");
require_once("includes/helpers.php");
require_once("includes/helpers.php");
require_once("includes/helpers.php");
require_once("includes/helpers.php");
require_once("includes/helpers.php");
require_once("includes/helpers.php");
require_once("includes/helpers.php");
require_once("includes/helpers.php");
require_once("includes/helpers.php");
require_once("includes/helpers.php");
require_once("includes/helpers.php");
require_once("includes/helpers.php");
require_once("includes/helpers.php");
require_once("includes/helpers.php");

除了必须解析那些不必要的行之外,require_once()的使用在技术上影响了运行该函数14次的性能。只有10个其他高度使用的类具有相似的问题,它可能会占到100多行这种相当无意义的重复代码。

有了它,可能值得在应用程序或框架的引导程序中使用require("includes/helpers.php");。但由于一切都是相对的,一切都取决于如果helpers类的权重与使用频率值得保存15-100行require_once()。但是如果在任何给定请求中不使用helpers文件的概率为none,那么require肯定应该在您的主类中。每个班级require_once分别成为浪费资源。


require_once函数在必要时很有用,但它不应被视为在任何地方用于加载所有类的单一解决方案。

答案 8 :(得分:2)

即使require_onceinclude_once 慢于requireinclude(或者可能存在的替代方案),我们也在讨论这里是微观优化的最小水平。与担心require_once等问题相比,优化编写糟糕的循环或数据库查询所花费的时间要好得多。

现在,人们可以提出一个论点,即require_once允许不良的编码实践,因为你不需要注意保持你的包含清洁和有条理,但这与函数本身,尤其不是它的速度。

显然,为了代码清洁和易于维护,自动加载更好,但我想说明这与速度无关。

答案 9 :(得分:0)

使用include,oli的替代和__autoload()进行测试;并安装something like APC进行测试。

我怀疑使用常数会加快速度。

答案 10 :(得分:0)

是的,它比普通的'require()贵一点。我认为重点是,如果你能保持你的代码足够有序,不要重复包含,不要使用* _once()函数,因为它会为你节省一些时间。

但是使用_once()函数不会杀死你的应用程序。基本上,只是不要以此为借口,不必整理您的包含。在某些情况下,使用它仍然是不可避免的,这并不是什么大问题。

答案 11 :(得分:-2)

我认为在PEAR文档中,建议使用require,require_once,include和include_once。我遵循该准则。你的申请会更清楚。

答案 12 :(得分:-3)

它与速度无关。这是关于失败的优雅。

如果require_once()失败,则表示您的脚本已完成。没有其他任何处理。如果您使用include_once(),则脚本的其余部分将尝试继续呈现,因此您的用户可能会对脚本中失败的内容更加明智。

答案 13 :(得分:-4)

我个人认为,使用require_once(或include_once)是不好的做法,因为如果您已经包含该文件并且禁止双重包含文件的错误导致致命错误(例如函数/类的重复声明/),require_once会检查您等)。

您应该知道是否需要包含文件。