为什么 need_once 使用起来这么糟糕?

我读到的关于更好的 PHP 编码实践的所有内容都在说,由于速度的原因,不要使用 require_once

为什么会这样?

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

73800 次浏览

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

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_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.

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

我认为在 PEAR 文档中,有一个关于 request、 need _ once、 include 和 include _ once 的建议。我确实遵守了这个指导方针。你的申请会更明确。

您使用 include 测试 oli 的替代方案和 _ _ autoload () ; 并使用安装的 装甲运兵车之类的测试它。

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

我个人的观点是,使用 request _ once (或 include _ once)是不好的做法,因为如果您已经包含了该文件,那么就需要对该文件进行 request _ once 检查,并抑制双重包含文件的错误,从而导致致命错误(如重复声明函数/类等)。

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

*_once()函数 立刻每个父目录,以确保您包含的文件不同于已经包含的文件。这是经济放缓的部分原因。

我建议使用像 围城这样的工具进行基准测试。您可以尝试所有建议的方法并比较响应时间。

有关 require_once()的详情,请浏览 科技你的宇宙翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳翻译: 奇芳

是的,它比普通的要求稍微贵一点。我认为问题的关键在于,如果您能够保持代码足够有组织,以避免重复 include,那么就不要使用 * _ once ()函数,因为它将为您节省一些周期。

但是使用 _ once ()函数并不会杀死您的应用程序。基本上就是 不要用它作为借口,不用组织你的包括。在某些情况下,使用它仍然是不可避免的,这不是一个大问题。

PEAR2 wiki (当它存在时)用于列出 为了支持自动加载,有充分的理由放弃需求/包含指令ing,至少对于库代码是这样。当像 法尔这样的替代打包模型即将出现时,这些将您绑定到严格的目录结构上。

更新: 由于维基的网络存档版丑得令人瞠目结舌,我复制了以下最有说服力的理由:

  • Include _ path 是使用(PEAR)包所必需的。这使得在另一个应用程序中将 PEAR 包与其 拥有 include _ path,以创建包含所需类的单个文件, 在没有大量源代码的情况下将 PEAR 包移动到 phar 归档 修改。
  • 当顶层的 need _ once 与条件的 need _ once 混合时,这可能导致代码不能被操作码缓存(如 APC,它将与 PHP6捆绑在一起。
  • 相对的 need _ once 要求 include _ path 已经设置为正确的值,这使得不使用 正确的 include _ path

我很好奇,查了亚当 · 巴克斯卓和 科技你的宇宙的联系。本文描述了需求应该被使用而不是 need _ once 的原因之一。然而,他们的说法与我的分析不符。我很想知道我哪里分析错了溶液。我使用 PHP 5.2.0进行比较。

我首先创建了100个头文件,它们使用 need _ once 来包含另一个头文件。每个文件看起来都像这样:

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


?>

我使用一个快速的 Bash 黑客技术创建了这些:

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

通过这种方式,我可以很容易地在包含头文件时在使用 need _ once 和 request 之间进行切换。然后我创建了一个 app.php 来加载100个文件。这看起来像:

<?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);
?>

我对比了 need _ once 头文件和使用类似下面这样的头文件的 request 头文件:

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

我没有发现在运行这个命令时,使用 need 和 need _ once 有什么不同。事实上,我最初的测试似乎暗示了 need _ once 稍微快一点,但我不一定相信这一点。我用10000个输入文件重复了这个实验。在这里我确实看到了一致的差异。我多次运行这个测试,结果很接近,但是使用 need _ once 使用平均30.8个用户 jiffies 和72.6个系统 jiffies; 使用 need 使用平均39.4个用户 jiffies 和72.0个系统 jiffies。因此,看起来使用 need _ once 的负载会稍微低一些。然而,墙上的时钟时间略有增加。10,000个 need _ once 调用平均需要10.15秒才能完成,10,000个 request 调用平均需要9.84秒。

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

一旦进行了以下系统调用,在从 request _ 打开文件之前:

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 意味着 need _ 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

在使用 need _ once 时,每个头文件大约有两个以上的系统调用。一个不同之处是 need _ 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 _ overage”的选项,该选项声称它减少了 need _ once 和 include _ once 调用的系统调用次数(参见 PHP 文档)。

这个帖子让我感到害怕,因为已经有一个“解决方案发布”了,而且从各种意图和目的来看,这是错误的。让我们列举一下:

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

  2. 如果正确地使用 require_once(),也就是说,为了包含类,甚至不需要定义; 只需检查 class_exists('Classname')。如果您包含的文件包含代码,也就是说,您正在以过程的方式使用它,那么完全没有理由需要 require_once(); 每次您包含您假定要进行子例程调用的文件时。

因此,有一段时间,很多人确实使用 class_exists()方法进行包含。我不喜欢它,因为它太丑了,但他们有充分的理由这样做: require_once()在最近的一些 PHP 版本之前效率非常低。但是这个问题已经解决了,我的观点是,为条件调用编译的额外字节码和额外的方法调用,将远远超过任何内部散列表检查。

现在,我们承认: 这种东西很难测试,因为它只占执行时间的很少一部分。

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

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

  • 如果您没有运行操作码缓存,那么您将陷入困境。将所有的包含内联到一个文件中(不要在开发期间这样做,只在生产期间)当然有助于解析时间,但是这是一个痛苦的过程,而且,您需要确切地知道在请求期间将包含什么。

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

  • 如果您有大约10个包含(这是一个 非常背面的信封计算) ,所有这一切都是不值得的: 只是优化您的数据库查询或东西。

这和速度无关,而是要优雅地失败。

如果 request _ once ()失败,脚本就完成了。其他的都不会被处理。如果您使用 include _ once () ,脚本的其余部分将尝试继续呈现,因此您的用户可能对脚本中失败的内容一无所知。

即使 require_onceinclude_once requireinclude慢(或者任何可能存在的替代方案) ,我们在这里讨论的是最小级别的微优化。与其担心像 require_once这样的问题,不如把时间花在优化编写糟糕的循环或数据库查询上。

现在,有人可能会说,require_once允许糟糕的编码实践,因为你不需要注意保持包含的清洁和组织,但这与函数 本身没有任何关系,特别是它的速度。

显然,为了代码的整洁和易于维护,自动加载更好,但是我想说明的是,这与 速度没有任何关系。

它没有使用糟糕的函数。在整个代码库中,对如何以及何时使用它的理解是不正确的。我只是要给这个可能被误解的概念加上一点上下文:

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

我所看到的真正糟糕的是,巨大的单体框架以错误的方式使用 require_once(),特别是在复杂的面向对象(OO)环境中。

以在每个类的顶部使用 require_once()为例,正如在许多库中看到的那样:

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个 没必要类,这些类在特定的请求期间永远不会使用。这就是为什么臃肿的框架最终使用每个请求40MB,而不是5MB 或更少。


另一种可能被滥用的方式是,当一个类被许多其他类重用时! 假设您有大约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");

从技术上讲,除了必须解析那些不必要的行之外,使用 need _ once ()还会影响运行该函数14次的性能。只有10个其他高度使用的类有类似的问题,它可以解释100多行这样毫无意义的重复代码。

因此,在应用程序或框架的引导程序中使用 require("includes/helpers.php");可能是值得的。但是因为一切都是相对的,如果 helpers类的权重和使用频率值得节省15-100行 require_once(),那么 看情况了。但是如果在任何给定的请求中不使用 helpers文件的可能性为零,那么 require肯定应该在您的主类中。将 require_once分别放在每个类中会造成资源的浪费。


require_once函数在必要时是有用的,但是它不应该被看作是一个单一的解决方案,可以在任何地方加载所有类。