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

Так как серверные PHP-скрипты выполняются, бывает, много раз в секунду, скорость загрузки конфигов — достаточно важный параметр. Хотя ему, порой, уделяется не очень много внимания. Давайте сравним различные варианты хранения настроек для PHP-скриптов с точки зрения скорости их работы. Ну и коснемся вкратце их удобства.

Итак, подопытные:
  • INI-файлы
  • PHP-скрипты
  • XML-файлы
  • Текстовые файлы
  • Файлы с сериализованными данными
  • Вне конкурса — PHP-скрипты с define'ами
  • JSON-файлы NEW!
Чтобы никого не обидеть, перечисление в алфавитном порядке. Вариант хранения настроек в базе данных, кстати, не рассматривался. Уж слишком невыгодным он выглядит с точки зрения скорости доступа к настройкам.

Условия:
  • Как можно быстрее загрузить настройки из файла
  • Вернуть массив настроек в виде «ключ» => «значение»
  • Конфигурационный файл содержит 10, 100 или 1000 конфигурационных параметров, представляющих собой короткие строки
  • Конфигурация читается 1000 раз подряд, замеряется время работы в секундах
Понятно, что со вторым из условий тестирования можно поспорить. Мне этот вариант показался оптимальным для хранения настроек в памяти во время работы скрипта, но в некоторых случаях он таковым не является. И, кстати, этому условию не удовлетворяют PHP-скрипты с define'ами, из-за чего они и были помечены «вне конкурса».

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

Правда, необходимо сделать небольшое уточнение по поводу программного обеспечения сервера. Использовался реальный веб-сервер, в момент низкой загрузки. Соответственно, конфигурация сервера «боевая»: Linux Debian Lenny, много памяти и RAID1-массив жестких дисков. PHP серии 5.2.x (не самый последний, врочем) с eAccelerator'ом. На время тестов отключался Zend Optimizer, чтобы тесты были боле�� «чистыми», что минимально повлияло на результаты. Тесты без eAccelerator тоже проводились, но, как ни странно, сильно на распределение сил это не повлияло. Причина, на мой взгляд, кроется в том, что eAccelerator настроен на дисковое кэширование опкодов PHP и на сравнение времени модификации файлов, что «съедает» определенное количество времени — хотя и приносит определенные бонусы.

INI-файлы


Результаты: 0.015, 0.086, 0.784

Пример:
x1 = 1
x2 = 2
x3 = 3

Скрипт:
function config($file) {
    return parse_ini_file($file);
}

Конфигурационный файл с классическим, всем знакомым синтаксисом. Достаточно быстрый и удобный способ.

PHP-скрипты


Результаты: 0.029, 0.111, 0.902

Пример:
<?
return array (
  'x1' => '1',
  'x2' => '2',
  'x3' => '3',
);
?>

Скрипт:
function config($file) {
    return include($file);
}

Сначала маленькая оговорка. Во многих проектах конфигурационный файл не делает return, а просто определяет элементы глобального массива настроек. Это, с одной стороны, не совсем подходило под условия теста, а с другой стороны не совсем идеологически корректно в рамках борьбы против глобальных переменных. Поэтому для сравнения был использован предложенный вариант.

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

XML-файлы


Результаты: 0.062, 0.385, 3.911

Пример:
<root>
  <x1>1</x1>
  <x2>2</x2>
  <x3>3</x3>
</root>

Скрипт:
function config($file) {
    $r = array();
    $dom = new DOMDocument;
    $dom->load($file);
    foreach ($dom->firstChild->childNodes as $node) {
        if ($node->nodeType == XML_ELEMENT_NODE) {
            $r[$node->nodeName] = $node->firstChild->nodeValue;
        }
    }
    return $r;
}

Недостаток очевидный: очень маленькая скорость работы, в несколько раз медленнее, чем другие варианты. Чтобы проверить, не слишком ли медленная PHP-часть этого кода, я попробовал сделать return сразу после загрузки XML-документа (то есть, фактически, конфигурационные параметры не возвращались). Это ускорило процесс всего приблизительно в два раза. Что подтвердило общий вывод.

Результаты: NEW! 0.047, 0.276, 2.791

Скрипт: NEW!
function config($file) {
    $r = array();
    foreach(simplexml_load_file($file) as $k => $v) {
        $r[$key] = strval($v);
    }
    return $r;
}

С помощью SimpleXML получается, коне��но, быстрее. Но не настолько, чтобы претендовать на лидерство.

Текстовые файлы


Результаты: 0.034, 0.250, 2.369

Пример:
x1  1
x2  2
x3  3

Скрипт:
function config($file) {
    $r = array();
    if ($F = fopen($file, "r")) {
        while (($line = fgets($F)) !== false) {
            list($k, $v) = explode("\t", $line, 2);
            $r[trim($k)] = trim($v);
        }
        fclose($F);
    }
    return $r;
}

Не знаю, право, зачем этот способ существует. Возможно, его придумали до того, как изобрели parse_ini_file, или до того, как узнали об этой функции. Не сильно быстрый, не сильно удобный способ.

Результат: NEW! 0.036, 0.250, 2.213

Скрипт: NEW!
function config($file) {
    $r = array();
    foreach (explode("\n", file_get_contents($file)) as $line) {
        list($k, $v) = explode("\t", $line, 2);
        $r[trim($k)] = trim($v);
    }
    return $r;
}

Такой вариант реализации несколько медленнее для небольших файлов и быстрее для больших файлов. Но, в общем, не влияет на расстановку сил.

Файлы с сериализованными данными


Результаты: 0.011, 0.041, 0.309

Пример:
a:3:{s:2:"x1";s:1:"1";s:2:"x2";s:1:"2";s:2:"x3";s:1:"3";}

Скрипт:
function config($file) {
    return unserialize(file_get_contents($file));
}

Наименее удобочитаемый конфигурационный файл — но при этом самый быстрый результат.

PHP-скрипты с define'ами


Результаты: 0.045, 0.252, 2.404

Пример:
<?
define("x1", "1");
define("x2", "2");
define("x3", "3");
?>

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

JSON-файлы NEW!


Результаты: 0.015, 0.057, 0.495

Пример:
{"x1":"1","x2":"2","x3":"3"}

Скрипт:
function config($file) {
    return json_decode(file_get_contents($file), true);
}

JSON ворвался в нашу жизнь. Его реализация в PHP позволила даже обогнать одного из лидеров, INI-файлы, но немного уступает встроенной сериализации PHP. Одно замечание: приведенный код возвращает не массив, а stdClass object.

Выводы


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

Если Вы серьезный человек — то избегайте прямого чтения текстовых файлов, особенно с большими объемами. Вместо этого Вам вполне подойдут JSON-файлы или INI-файлы, тем более, что скрипты станут работать быстрее.

Если нужны гибкие настройки, с возможностью применения условий и переменных — то пишите конфигурационный файл на PHP. Работать будет медленнее предыдущих способов, но гибкость настроек в других способах недостижима.

Настройки в формате XML — самые медленные. Прежде, чем их использовать, подумайте хорошенько.

Искренне надеюсь, что define'ы никто не использует, поэтому оставляем их обсуждение вне выводов.

Итог


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

Результаты: 0.018, 0.046, 0.317

Оптимально с точки зрения гибкости и скорости, на мой взгляд.

А вот и сам скрипт:
function config($file) {
    $file_dat = "$file.dat";
    if (!file_exists($file_dat) || filemtime($file_dat) <= filemtime($file)) {
        $r = include($file);
        if ($F = fopen($file_dat, "w")) {
            fwrite($F, serialize($r));
            fclose($F);
        }
    } else {
        $r = unserialize(file_get_contents($file_dat));
    }
    return $r;
}

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

P.S. PHP-код в статье не самый хороший. Я писал его, преследуя две цели: краткость и скорость работы. Поэтому отсутствуют комментарии, длинные имена переменных и различные проверки. Кроме того, большая часть кода работает под PHP 4 и 5 без проблем (кроме, конечно, XML). Надеюсь, это не вызовет излишнего накала страстей.
P.P.S. В сравнение добавлен код JSON.
P.P.P.S. Добавлены небольшие ремарки по поводу железа и программного обеспечения. Без них, согласен с авторами комментариев, было как-то не так.