Почему require_once так плохо в использовании? - PullRequest
139 голосов
/ 09 октября 2008

Все, что я читал о лучших практиках PHP-кодирования, постоянно говорит: не используйте require_once из-за скорости.

Почему это?

Как правильно / лучше сделать то же самое, что и require_once? Если это имеет значение, я использую PHP5.

Ответы [ 14 ]

146 голосов
/ 12 октября 2008

Эта тема заставляет меня съеживаться, потому что уже было опубликовано "решение", и оно, по сути, неверно. Давайте перечислим:

  1. Определяет действительно дорого в PHP. Вы можете найти его или протестировать его самостоятельно, но единственный эффективный способ определения глобальной константы в PHP - через расширение. (Константы класса на самом деле довольно приличны с точки зрения производительности, но это спорный вопрос из-за 2)

  2. Если вы используете require_once() надлежащим образом, то есть для включения классов вам даже не нужно определение; просто проверьте, если class_exists('Classname'). Если файл, который вы включаете, содержит код, то есть вы используете его в процедурном порядке, то нет абсолютно никаких причин, по которым вам может понадобиться require_once(); каждый раз, когда вы включаете файл, который вы предполагаете сделать подпрограммой.

Итак, какое-то время многие люди использовали метод class_exists() для своих включений. Мне это не нравится, потому что это бесполезно, но у них были веские причины: require_once() был довольно неэффективен перед некоторыми из более поздних версий PHP. Но это было исправлено, и я утверждаю, что дополнительный байт-код, который вам нужно будет скомпилировать для условного, и дополнительный вызов метода, намного перевесят любую внутреннюю проверку хеш-таблицы.

Теперь, допуск: этот материал сложно проверить, потому что он занимает так мало времени выполнения.

Вот вопрос, над которым вы должны задуматься: как правило, include в PHP дорогой, потому что каждый раз, когда интерпретатор нажимает на него, он должен переключаться обратно в режим анализа, генерировать коды операций и затем возвращаться назад. Если у вас есть 100+ включений, это определенно повлияет на производительность. Причина, по которой использование или не использование require_once является таким важным вопросом, заключается в том, что это усложняет жизнь для кэшей кода операции. объяснение этому можно найти здесь, но к чему это сводится:

  • Если во время синтаксического анализа вы точно знаете, какие включаемые файлы вам понадобятся на весь срок действия запроса, require() - те, что находятся в самом начале, и кэш кода операции будет обрабатывать все остальное для вас.

  • Если вы не используете кэш кода операции, вы находитесь в затруднительном положении. Включение всех ваших включений в один файл (не делайте этого во время разработки, только в производстве), безусловно, может помочь разобрать время, но это трудная задача, а также вам необходимо точно знать, что вы будете включать в течение запрос.

  • Автозагрузка очень удобна, но медленна, потому что логика автозагрузки должна запускаться каждый раз, когда выполняется включение. На практике я обнаружил, что автозагрузка нескольких специализированных файлов для одного запроса не вызывает особых проблем, но вам не следует загружать все необходимые вам файлы.

  • Если у вас есть, может быть, 10 включений (это очень обратная сторона вычисления конверта), все это подонки не стоят: просто оптимизируйте запросы к вашей базе данных или что-то в этом роде.

104 голосов
/ 09 октября 2008

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_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.
64 голосов
/ 12 октября 2008

Мне стало любопытно и проверил ссылку Адама Бэкстрома на 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:

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_once был немного быстрее, но я не обязательно в это верю. Я повторил эксперимент с 10000 входными файлами. Здесь я увидел постоянную разницу. Я запускал тест несколько раз, результаты близки, но при использовании require_once в среднем используется 30,8 пользовательских и 72,6 системных запросов; использование require использует в среднем 39,4 пользовательских и 72,0 системных. Таким образом, кажется, что загрузка немного ниже с использованием require_once. Однако настенные часы немного увеличены. Для выполнения 10 000 вызовов require_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

Это отличается от 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 Ваша вселенная подразумевает, что 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-документы ). .

Извините за длинный пост. Мне стало любопытно.

20 голосов
/ 09 октября 2008

Можете ли вы дать нам какие-либо ссылки на эти методы кодирования, которые говорят, чтобы избежать этого? Насколько я понимаю, это совершенно не проблема . Я сам не смотрел на исходный код, но думаю, что единственная разница между include и include_once заключается в том, что include_once добавляет это имя файла в массив и каждый раз проверяет его. Было бы легко сохранить этот массив отсортированным, поэтому поиск по нему должен быть O (log n), и даже приложение среднего размера будет иметь только пару дюжин включений.

6 голосов
/ 09 октября 2008

Лучший способ сделать это - использовать объектно-ориентированный подход и использовать __ autoload () .

5 голосов
/ 25 февраля 2015

Это не использует функцию, которая плоха. Это неверное понимание того, как и когда его использовать, в общей базе кода. Я просто добавлю немного больше контекста к этому, возможно, неправильно понятому понятию:

Люди не должны думать, что 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 предназначен для использования всех трех других классов. Справедливо! Но что теперь, если посетитель просматривает сайт и даже не вошел в систему, а фреймворк загружается: require_once("includes/user.php"); для каждого отдельного запроса.

Он включает в себя 1 + 3 ненужных классов, которые он никогда не будет использовать во время этого конкретного запроса. Вот как раздутые фреймворки в итоге используют 40 МБ на запрос, а не 5 МБ или меньше.


Другие способы, которыми он может быть использован не по назначению, - это когда класс повторно используется многими другими! Допустим, у вас есть около 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 в любом заданном запросе равна нулю, тогда require определенно должен быть в вашем основном классе. Наличие require_once в каждом классе отдельно становится пустой тратой ресурсов.


Функция require_once полезна при необходимости, но ее не следует рассматривать как монолитное решение, которое можно использовать везде для загрузки всех классов.

5 голосов
/ 11 октября 2008

Вики PEAR2 (когда она существовала) использовалась для перечисления веских причин для отказа от всех директив require / include в пользу автозагрузки , по крайней мере для библиотечного кода. Они связывают вас с жесткой структурой каталогов, когда на горизонте появляются альтернативные модели упаковки, такие как phar .

Обновление: поскольку веб-архивная версия вики выглядит ужасно уродливо, я скопировал наиболее убедительные причины ниже:

  • include_path требуется для использования пакета (PEAR). Это затрудняет связывание пакета PEAR в другом приложении с его собственный include_path, чтобы создать отдельный файл, содержащий необходимые классы, переместить пакет PEAR в архив phar без обширного исходного кода модификация.
  • когда верхний уровень require_once смешан с условным require_once, это может привести к тому, что код не будет кэшироваться кэшем кода операции, таким как APC, который будет в комплекте с PHP 6.
  • Относительный require_once требует, чтобы include_path уже был установлен на правильное значение, что делает невозможным использование пакета без правильный include_path
5 голосов
/ 09 октября 2008

Функции *_once() регистрируют каждый родительский каталог, чтобы гарантировать, что включаемый вами файл отличается от того, который уже был включен. Это одна из причин замедления.

Я рекомендую использовать такой инструмент, как Siege для сравнительного анализа. Вы можете попробовать все предложенные методики и сравнить время ответа.

Подробнее о require_once() на Tech Your Universe .

2 голосов
/ 27 января 2015

Даже если require_once и include_once медленнее , чем require и include (или любые другие альтернативы), мы говорим о наименьшем уровне микрооптимизации. Ваше время гораздо лучше потрачено на оптимизацию плохо написанного цикла или запроса к базе данных, чем на беспокойство о чем-то вроде require_once.

Теперь можно привести аргумент, гласящий, что require_once допускает плохую практику кодирования, потому что вам не нужно обращать внимание на чистоту и организацию ваших включений, но это не имеет ничего общего с самой функцией . и особенно не его скорость.

Очевидно, что автозагрузка лучше для чистоты кода и простоты обслуживания, но я хочу прояснить, что это не имеет ничего общего с speed .

0 голосов
/ 12 октября 2008

Это никак не связано со скоростью. Речь идет о изящной неудаче.

Если require_once () не работает, ваш скрипт готов. Больше ничего не обрабатывается. Если вы используете include_once (), остальная часть вашего скрипта будет пытаться продолжить рендеринг, так что ваши пользователи потенциально не будут ничего удивляться тому, что не удалось в вашем скрипте.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...