145

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

为什么是这样?

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

4

14 回答 14

148

这个线程让我感到畏缩,因为已经有一个“解决方案发布”,而且无论出于何种意图和目的,它都是错误的。我们列举一下:

  1. PHP 中的定义非常昂贵。您可以自己查找或测试它,但在 PHP 中定义全局常量的唯一有效方法是通过扩展。(类常量实际上在性能方面相当不错,但这是一个有争议的问题,因为 2)

  2. 如果您使用require_once()得当,即包含类,您甚至不需要定义;只需检查是否class_exists('Classname')。如果您包含的文件包含代码,即您以程序方式使用它,那么您绝对没有require_once()必要这样做;每次包含您认为正在进行子例程调用的文件时。

所以有一段时间,很多人确实使用这种class_exists()方法来制作他们的夹杂物。我不喜欢它,因为它很丑,但他们有充分的理由:require_once()在一些较新版本的 PHP 之前效率很低。但这已经解决了,我的观点是,你必须为条件编译的额外字节码和额外的方法调用,将远远超过任何内部哈希表检查。

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

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

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

  • 如果您没有运行操作码缓存,那么您将处于困境中。将所有包含内容内联到一个文件中(不要在开发期间这样做,仅在生产中这样做)当然可以帮助解析时间,但这样做很痛苦,而且,您需要确切知道在要求。

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

  • 如果您可能有 10 个包含(这是一个非常落后的计算),那么所有这些手淫都是不值得的:只需优化您的数据库查询或其他东西。

于 2008-10-12T02:05:57.840 回答
115

require_once并且include_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

慢 10-100 倍,require_once奇怪的require_oncehhvm. 同样,这仅在您运行*_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.
于 2008-10-09T08:20:31.480 回答
68

我很好奇并查看了 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 with 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 标头与使用头文件的要求标头进行了对比,如下所示:

<?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 个 require_once 调用平均使用 10.15 秒完成,10,000 个 require_once 调用平均使用 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

这与 require 形成对比:

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 文件中引用的相对路径。如果我将此作为绝对引用,那么唯一的区别是代码中进行了额外的 time(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

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

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

于 2008-10-12T01:43:19.477 回答
21

你能给我们任何链接到这些编码实践,说要避免它吗?就我而言,这完全不是问题include我自己没有查看源代码,但我想和之间的唯一区别include_onceinclude_once将文件名添加到数组并每次检查数组。保持该数组排序很容易,因此搜索它应该是 O(log n),即使是中大型应用程序也只有几十个包含。

于 2008-10-09T08:27:36.257 回答
7

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

于 2008-10-09T08:05:28.483 回答
6

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

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

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

require_once()以在许多库中看到的每个类的顶部使用 using 为例:

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

所以User该类被设计为使用所有其他三个类。很公平!

但是现在,如果访问者正在浏览站点并且甚至没有登录并且框架加载了:require_once("includes/user.php");对于每一个请求。

它包括在该特定请求期间永远不会使用的 1+3 个不必要的类。这就是臃肿的框架最终使用 40 MB 每个请求而不是 5 MB 或更少的原因。


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

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

这里本身没有错。但是,如果一个页面请求恰好包含 15 个类似的类。您正在运行require_once15 次,或者是为了获得漂亮的视觉效果:

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但是,如果在任何给定请求上不使用文件的可能性为零,那么require肯定应该在您的主类中。分别在require_once每个班级中成为资源浪费。


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

于 2015-02-25T05:31:47.467 回答
5

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

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

更多信息请require_once()参见Tech Your Universe

于 2008-10-09T12:35:01.640 回答
5

PEAR2 wiki(当它存在时)曾经列出了放弃所有require/include 指令以支持 autoload 的充分理由,至少对于库代码而言。当诸如phar之类的替代包装模型即将出现时,这些会将您束缚在严格的目录结构中。

更新:由于 wiki 的网络存档版本非常丑陋,我复制了以下最令人信服的理由:

  • 需要 include_path 才能使用 (PEAR) 包。这使得很难将 PEAR 包与它自己的 include_path 捆绑到另一个应用程序中,以创建包含所需类的单个文件,将 PEAR 包移动到 phar 存档而无需大量修改源代码。
  • 当顶级 require_once 与条件 require_once 混合时,这可能会导致代码无法被操作码缓存(例如 APC)缓存,这将与 PHP 6 捆绑在一起。
  • 相对 require_once 要求 include_path 已经设置为正确的值,因此无法使用没有正确 include_path 的包
于 2008-10-11T04:55:35.750 回答
3

即使require_onceandinclude_once requireand慢include(或任何可能存在的替代方案),我们在这里谈论的是最小级别的微优化。与担心类似require_once.

现在,有人可能会说require_once允许糟糕的编码实践,因为您不需要注意保持包含的干净和有条理,但这与函数本身无关,尤其是与它的速度无关。

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

于 2015-01-27T13:14:09.670 回答
0

您使用 include、oli 的替代方法和 __autoload() 进行测试;并使用安装了 APC 之类的东西对其进行测试。

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

于 2008-10-09T10:04:49.353 回答
0

是的,它比普通的 require() 稍微贵一些。我认为关键是如果你能保持你的代码足够有条理以不重复包含,不要使用 *_once() 函数,因为它会为你节省一些周期。

但是使用 _once() 函数不会杀死你的应用程序。基本上,不要以此为借口不必组织您的 include。在某些情况下,使用它仍然是不可避免的,而且没什么大不了的。

于 2008-10-09T12:45:59.853 回答
-3

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

于 2008-10-09T08:42:48.957 回答
-3

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

如果 require_once() 失败,你的脚本就完成了。没有其他任何处理。如果您使用 include_once() 脚本的其余部分将尝试继续渲染,因此您的用户可能对您的脚本中失败的事情一无所知。

于 2008-10-12T02:11:59.637 回答
-4

我个人的看法是,使用 require_once(或 include_once)是不好的做法,因为 require_once 会检查您是否已经包含该文件并抑制双重包含文件的错误,从而导致致命错误(例如函数/类/等的重复声明) .

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

于 2008-10-09T10:18:33.270 回答