Почему требуется один раз так плохо использовать?
Все, что я читал о лучших методах кодирования PHP, постоянно говорит о том, что не используйте require_once
из-за скорости.
Почему это так?
Каков правильный/лучший способ сделать то же самое, что и require_once
? Если это имеет значение, я использую PHP5.
14 answers
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. Но это было исправлено, и я утверждаю, что дополнительный байт-код, который вам пришлось бы скомпилировать для условного, и дополнительный вызов метода, намного перевесили бы любую внутреннюю проверку хэш-таблицы.
Теперь, признание: этот материал сложно проверить, потому что на него приходится так мало времени выполнения.
Вот вопрос, о котором вам следует подумать: включает в себя, в целом правило, дорого обходится в PHP, потому что каждый раз, когда интерпретатор попадает в один из них, ему приходится переключаться обратно в режим синтаксического анализа, генерировать коды операций, а затем возвращаться назад. Если у вас есть 100+ включений, это определенно повлияет на производительность. Причина, по которой использование или неиспользование require_once является таким важным вопросом, заключается в том, что это усложняет жизнь кэшам кодов операций. Объяснение этого можно найти здесь, но это сводится к следующему:
Если во время разбора, вы точно знаете, какие включенные файлы вам понадобятся на весь срок действия запроса,
require()
те, что в самом начале, и кэш кода операции обработают все остальное за вас.Если вы не используете кэш кода операции, вы находитесь в трудном положении. Вставка всех ваших включений в один файл (не делайте этого во время разработки, только в процессе производства), безусловно, может помочь в анализе времени, но это непросто, а также вам нужно точно знать, что вы будете включать во время запрос.
Автоматическая загрузка очень удобна, но медленна по той причине, что логика автоматической загрузки должна выполняться каждый раз при выполнении включения. На практике я обнаружил, что автоматическая загрузка нескольких специализированных файлов для одного запроса не вызывает особых проблем, но вы не должны автоматически загружать все файлы, которые вам понадобятся.
Если у вас есть, может быть, 10 включений (это очень подсчет конверта), все это дрочение того не стоит: просто оптимизируйте свои запросы к базе данных или что-то в этом роде.
Мне стало любопытно, и я проверил ссылку Адама Бэкстрома на Техническую вашу Вселенную. В этой статье описывается одна из причин, по которой require следует использовать вместо require_once. Однако их утверждения не соответствовали моему анализу. Мне было бы интересно посмотреть, где я, возможно, неправильно проанализировал решение. Я использовал php 5.2.0 для сравнения.
Я начал с создания 100 заголовочных файлов, которые использовали require_once для включения другого заголовочного файла. Каждый из этих файлов выглядел что-то вроде:
<?php
// /home/fbarnes/phpperf/hdr0.php
require_once "../phpperf/common_hdr.php";
?>
Я создал их с помощью быстрого взлома:
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 вызовов require в среднем требуется 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
Технология Вашей Вселенной подразумевает, что 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
Это, по-видимому, подразумевает, что вы могли бы уменьшить количество системных вызовов, используя абсолютные пути, а не относительные пути. Единственное отличие за пределами этого - временные (НУЛЕВЫЕ) вызовы, которые, по-видимому, используются для инструментирования кода для сравните, что быстрее.
Еще одно замечание заключается в том, что пакет оптимизации APC имеет опцию "apc.include_once_override", которая утверждает, что она уменьшает количество системных вызовов, выполняемых вызовами require_once и include_once (см. Документы PHP).
Извините за длинный пост. Мне стало любопытно.
Можете ли вы дать нам какие-либо ссылки на эти методы кодирования, которые говорят, чтобы избежать этого? Насколько я понимаю, это совершенно не проблема. Я сам не смотрел исходный код, но я бы предположил, что единственная разница между include
и include_once
заключается в том, что include_once
добавляет это имя файла в массив и проверяет массив каждый раз. Было бы легко отсортировать этот массив, поэтому поиск по нему должен быть O(log n), и даже в приложении среднего размера будет всего пара десятков включает в себя.
Лучший способ сделать это - использовать объектно-ориентированный подход и использовать __автозагрузку().
Функции *_once()
регистрируют каждый родительский каталог, чтобы убедиться, что файл, который вы включаете, не совпадает с тем, который уже был включен. Это одна из причин замедления темпов роста.
Я рекомендую использовать такой инструмент, как Осада для сравнительного анализа. Вы можете попробовать все предложенные методики и сравнить время отклика.
Подробнее о require_once()
в Создайте свою Вселенную.
Вики PEAR2 (когда она существовала) обычно перечисляла веские причины для отказа от всех директив require/include в пользу автоматической загрузки , по крайней мере, для кода библиотеки. Они привязывают вас к жестким структурам каталогов, когда на горизонте появляются альтернативные модели упаковки, такие как phar.
Обновление: Поскольку веб-архивная версия вики невероятно уродлива, я скопировал наиболее веские причины ниже:
- требуется включить путь для того, чтобы использовать пакет (ГРУША). Это затрудняет объединение пакета PEAR в другое приложение с собственным путем include_path, создание одного файла, содержащего необходимые классы, перемещение пакета PEAR в архив phar без значительных изменений исходного кода.
- когда require_once верхнего уровня смешивается с условным require_once, это может привести к тому, что код не будет доступен для кэширования с помощью кодов операций, таких как APC, который будет поставляться в комплекте с PHP 6.
- относительный require_once требует, чтобы include_path уже был настроен на правильное значение, что делает невозможным использование пакета без надлежащего include_path
Плохо не то, что вы используете функцию. Это неправильное понимание того, как и когда его использовать в общей базе кода. Я просто добавлю немного больше контекста к этому, возможно, неправильно понятому понятию:
Люди не должны думать, что 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");
вместо этого в начальной загрузке вашего приложения или платформы. Но поскольку все относительно, все зависит от того, стоит ли экономить 15-100 строк require_once()
в зависимости от веса и частоты использования класса helpers
. Но если вероятность не использовать файл helpers
по любому заданному запросу равна нулю, то require
определенно должен быть в вашем основном классе вместо этого. Наличие require_once
в каждом классе отдельно становится пустой тратой ресурсы.
Функция require_once
полезна, когда это необходимо, но ее не следует рассматривать как монолитное решение для повсеместного использования для загрузки всех классов.
Даже если require_once
и include_once
являются медленнее, чем require
и include
(или какие бы альтернативы ни существовали), мы говорим здесь о наименьшем уровне микрооптимизации. Ваше время гораздо лучше потратить на оптимизацию этого плохо написанного цикла или запроса к базе данных, чем беспокоиться о чем-то вроде require_once
.
Теперь можно привести аргумент, утверждающий, что require_once
допускает плохие методы кодирования, потому что вам не нужно обращать внимание на то, чтобы ваши включения были чистыми и организованными, но это не имеет никакого отношения к самой функции и особенно к ее скорости.
Очевидно, что автоматическая загрузка лучше для чистоты кода и простоты обслуживания, но я хочу пояснить, что это не имеет ничего общего со скоростью .
Да, это немного дороже, чем обычный старый require(). Я думаю, дело в том, что если вы можете сохранить свой код достаточно организованным, чтобы не дублировать включения, не используйте функции *_once(), так как это сэкономит вам несколько циклов.
Но использование функций _once() не приведет к уничтожению вашего приложения. В принципе, просто не используйте это как предлог, чтобы не организовывать свои включения. В некоторых случаях его использование все еще неизбежно, и это не имеет большого значения.
Вы тестируете, используя include, альтернативу oli и __autoload(); и тестируете его с помощью чего-то вроде APC установленного.
Я сомневаюсь, что использование константы ускорит процесс.
Я думаю, что в документации PEAR есть рекомендация для require, require_once, include и include_once. Я действительно следую этому руководству. Ваше заявление было бы более ясным.
Это не имеет никакого отношения к скорости. Речь идет о том, чтобы изящно потерпеть неудачу.
Если require_once() завершится неудачно, ваш сценарий будет завершен. Больше ничего не обрабатывается. Если вы используете include_once(), остальная часть вашего скрипта попытается продолжить визуализацию, поэтому ваши пользователи потенциально не будут знать о том, что произошло в вашем скрипте.
Мое личное мнение заключается в том, что использование require_once (или include_once) является плохой практикой, потому что require_once проверяет для вас, уже ли вы включили этот файл, и подавляет ошибки двойных включенных файлов, приводящие к фатальным ошибкам (например, дублирование объявления функций/классов/и т.д.).
Вы должны знать, нужно ли вам включать файл.