php - Почему require_once так плохо использовать?


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

Почему это?

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


Answers



require_once и include_once требуют, чтобы система хранила журнал того, что уже было включено / требуется. Каждый вызов *_once означает проверку этого журнала. Итак, есть определенная некоторая дополнительная работа, которая проводится там, но достаточно, чтобы снизить скорость всего приложения?

... Я действительно сомневаюсь в этом ... Нет, если вы на действительно старом оборудовании или не делаете этого много .

Если вы делаете тысячи *_once , вы можете сделать работу самостоятельно более легким способом. Для простых приложений просто убедитесь, что вы включили только один раз, но если вы все еще получаете ошибки переопределения, вы можете что-то вроде этого:

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

Я лично буду придерживаться утверждений *_once но на глупом миллионном контрольном *_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.



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

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

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

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

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

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

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

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

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

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




Мне стало любопытно и проверил связь Адама Бэкстрома с Tech Your Universe . В этой статье описывается одна из причин, требующих использования вместо 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 и требовать при включении файлов заголовков. Затем я создал 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 с заголовками запросов, которые использовали заголовочный файл, выглядящий так:

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

?>

Я не нашел большой разницы при выполнении этого с требованием vs. require_once. На самом деле мои первоначальные тесты, по-видимому, подразумевали, что require_once был немного быстрее, но я не уверен в этом. Я повторил эксперимент с 10000 входными файлами. Здесь я видел постоянную разницу. Я провел тест несколько раз, результаты близки, но использование require_once использует в среднем 30.8 пользовательских jiffies и 72.6 системных jiffies; использование требует использования в среднем 39,4 пользовательских jiffies и 72.0 системных jiffies. Поэтому оказывается, что нагрузка немного ниже, используя require_once. Однако настенные часы немного увеличены. Для звонков в 10 000 require_once в среднем требуется 10,15 секунды, а в среднем 10000 вызовов - в среднем 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. Ваша Вселенная подразумевает, что 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 ).

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




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




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




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

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

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




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

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

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



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

Люди не должны думать, что require_once - медленная функция. Вы должны включить свой код так или иначе. require_once() vs. 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 не будет использоваться для любого заданного запроса, равно none, тогда require обязательно должно быть в вашем основном классе. Наличие require_once в каждом классе отдельно становится пустой тратой ресурсов.

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




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

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

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




Вы тестируете, используя include, альтернативу oli и __autoload (); и протестируйте его с чем-то вроде APC . Я сомневаюсь, что использование постоянной скорости ускорит процесс.




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

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




Я думаю, что в документации PEAR есть рекомендация require, require_once, include и include_once. Я следую этому руководству. Ваша заявка будет более понятной.




Это не имеет никакого отношения к скорости. Это из-за неудачного изящества.

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




Мое личное мнение заключается в том, что использование require_once (или include_once) является плохой практикой, потому что require_once проверяет вас, если вы уже включили этот файл и подавили ошибки двойных включенных файлов, что привело к фатальным ошибкам (например, дублирующее объявление функций / классов и т. Д.). ,

Вы должны знать, нужно ли включать файл.