Почему require_once так плохо в использовании?
Все, что я читал о лучших практиках кодирования PHP, говорит, что не используй require_once
из-за скорости.
Почему это?
Как правильно / лучше сделать то же самое, что и require_once
? Если это имеет значение, я использую PHP5.
14 ответов
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.
Эта тема заставляет меня съеживаться, потому что уже было опубликовано "решение", и это, по сути, неправильно. Давайте перечислим:
Определения действительно дорогие в PHP. Вы можете посмотреть его или протестировать самостоятельно, но единственный эффективный способ определения глобальной константы в PHP - через расширение. (Константы класса на самом деле довольно приличная производительность, но это спорный вопрос, из-за 2)
Если вы используете
require_once()
соответственно, для включения классов вам даже не нужно определение; просто проверьте, еслиclass_exists('Classname')
, Если файл, который вы включаете, содержит код, т.е. вы используете его в процедурном порядке, то нет абсолютно никаких причинrequire_once()
должен быть необходим вам; каждый раз, когда вы включаете файл, вы предполагаете, что выполняете вызов подпрограммы.
Так что какое-то время многие люди использовали class_exists()
метод их включения. Мне это не нравится, потому что это плохо, но у них были веские причины: require_once()
был довольно неэффективным перед некоторыми из более поздних версий PHP. Но это было исправлено, и я утверждаю, что дополнительный байт-код, который вам нужно будет скомпилировать для условного, и дополнительный вызов метода, намного перевесят любую внутреннюю проверку хеш-таблицы.
Теперь, признание: этот материал сложно проверить, потому что он занимает так мало времени выполнения.
Вот вопрос, о котором вы должны подумать: как правило, include в PHP дорогой, потому что каждый раз, когда интерпретатор нажимает на него, он должен переключаться обратно в режим синтаксического анализа, генерировать коды операций и затем возвращаться назад. Если у вас есть 100+ включений, это определенно повлияет на производительность. Причина, по которой использование или не использование require_once является таким важным вопросом, заключается в том, что это усложняет жизнь для кэшей кода операции. Объяснение этому можно найти здесь, но это сводится к следующему:
Если во время разбора вы точно знаете, какие включаемые файлы вам понадобятся на весь срок действия запроса,
require()
те в самом начале, и кэш кода операции будет обрабатывать все остальное для вас.Если вы не используете кэш кода операции, вы находитесь в трудном положении. Включение всех ваших включений в один файл (не делайте этого во время разработки, только в производстве), безусловно, может помочь разобрать время, но это трудная задача, а также вам нужно точно знать, что вы будете включать во время запрос.
Автозагрузка очень удобна, но медленна, потому что логика автозагрузки должна выполняться каждый раз, когда выполняется включение. На практике я обнаружил, что автозагрузка нескольких специализированных файлов для одного запроса не вызывает особых проблем, но вам не следует загружать все нужные вам файлы.
Если у вас есть, может быть, 10 включений (это очень обратная сторона вычисления конверта), все это подделка не стоит: просто оптимизируйте запросы к базе данных или что-то в этом роде.
Мне стало любопытно и проверил ссылку Адама Бэкстрома на 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 против require_once. На самом деле мои первоначальные тесты предполагали, что 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
Это контрастирует с требованием:
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).
Простите за длинный пост. Мне стало любопытно.
Можете ли вы дать нам какие-либо ссылки на эти методы кодирования, которые говорят, чтобы избежать этого? Насколько я понимаю, это совершенно не проблема. Я не смотрел на исходный код сам, но я думаю, что единственная разница между include
а также include_once
в том, что include_once
добавляет это имя файла в массив и проверяет массив каждый раз. Было бы легко сохранить этот массив отсортированным, поэтому поиск по нему должен быть O(log n), и даже приложение среднего размера будет иметь только пару десятков включений.
Лучший способ сделать это - использовать объектно-ориентированный подход и использовать __autoload ().
Вики 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
*_once()
Функция stat каждого родительского каталога, чтобы убедиться, что файл, который вы включаете, не совпадает с тем, который уже был включен. Это одна из причин замедления.
Я рекомендую использовать такой инструмент, как Siege для бенчмаркинга. Вы можете попробовать все предложенные методики и сравнить время отклика.
Еще require_once()
на Tech Your Universe.
Это не использование функции, которая плоха. Это неверное понимание того, как и когда его использовать, в общей базе кода. Я просто добавлю немного больше контекста к этому, возможно, неправильно понятому понятию:
Люди не должны думать, что 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 ненужных класса, которые он никогда не будет использовать во время этого конкретного запроса. Вот как раздутые фреймворки используют 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
Функция полезна, когда это необходимо, но ее не следует рассматривать как монолитное решение, которое можно использовать везде для загрузки всех классов.
Даже если require_once
а также include_once
медленнее, чем require
а также include
(или какие-либо альтернативы могут существовать), мы говорим о наименьшем уровне микрооптимизации здесь. На оптимизацию этого плохо написанного цикла или запроса к базе данных тратится гораздо больше времени, чем на беспокойство require_once
,
Теперь можно привести аргумент о том, что require_once
допускает плохую практику кодирования, потому что вам не нужно обращать внимание на то, чтобы ваши включения были чистыми и организованными, но это не имеет ничего общего с самой функцией , особенно с ее скоростью.
Очевидно, что автозагрузка лучше для чистоты кода и простоты обслуживания, но я хочу прояснить, что это не имеет никакого отношения к скорости.
Да, это немного дороже, чем обычные старые (). Я думаю, суть в том, что если вы можете сохранить свой код достаточно организованным, чтобы не дублировать включения, не используйте функции *_once(), так как это сэкономит вам несколько циклов.
Но использование функций _once () не убьет ваше приложение. В основном, просто не используйте это как оправдание, чтобы не организовывать ваши включения. В некоторых случаях его использование все еще неизбежно, и это не имеет большого значения.
Вы проверяете, используя include, альтернативу oli и __autoload(); и протестируйте его с установленным APC.
Я сомневаюсь, что использование константы ускорит процесс.
Я думаю, что в документации PEAR есть рекомендации для require, require_once, include и include_once. Я следую этому руководству. Ваша заявка будет более понятной.
Это не имеет ничего общего со скоростью. Речь идет о изящной неудаче.
Если require_once() не работает, ваш скрипт готов. Больше ничего не обрабатывается. Если вы используете include_once(), остальная часть вашего скрипта будет пытаться продолжить рендеринг, поэтому ваши пользователи потенциально не будут ничего удивляться тому, что не удалось в вашем скрипте.
Мое личное мнение таково, что использование require_once (или include_once) является плохой практикой, потому что require_once проверяет вас, если вы уже включили этот файл, и подавляете ошибки двойных включенных файлов, приводящие к фатальным ошибкам (таким как двойное объявление функций / классов / и т. Д.),
Вы должны знать, если вам нужно включить файл.