13 сентября 2010 в 09:53

Производительность функции unserialize

PHP*
В PHP есть две замечательные функции serialize и unserialize. Первая преобразует в строку практически любой набор данных, вторая производит обратное преобразование. Эти функции удобно использовать при организации кеширования или хранения сессий в базе данных. Я обнаружил, что время работы функции unserialize может оказаться неожиданно большим.

Не буду описывать как и почему мне потребовалось сериализовать и десериализовать большой массив данных, гораздо интереснее посмотреть на то, что я обнаружил.
  1. <?php
  2. ini_set('memory_limit', '512M');
  3. $file = '/tmp/1';
  4. $data = range(1,2000000);
  5.  
  6. echo "Test serialize\n";
  7. $time0 = microtime(1);
  8. file_put_contents($file, serialize($data));
  9. $time1 =  microtime(1);
  10. unserialize(file_get_contents($file));
  11. $time2 =  microtime(1);
  12.  
  13. $timeset = $time1-$time0;
  14. $timeget = $time2-$time1;
  15.  
  16. echo "Serialize set time $timeset get time $timeget\n";

Test serialize
Serialize set time 1.35619807243 get time 31.1126699448


30 секунд на десериализацию! Этот результат меня просто шокировал. Для начала я проверил, что file_get_contents не влияет на результат выполнения. Затем посмотрел на производительность json_encode и json_decode (JSON set time 0.270335912704 get time 1.30652809143). «Всё страньше и страньше», подумал я и решил построить график зависимости времени работы функции unserialize от длины десериализуемого массива.



На графике чётко видна квадратичная (!) зависимость времени выполнения функции от длины массива. Вот такая неожиданно медленная встроенная функция unserialize.

Ситуация, конечно, не критичная. Можно использовать другие способы сериализации и десериализации. Основная цель статьи — показать как зависит производительность функции unserialize от размера данных.

Исходные коды для проверки результатов можно взять по адресу http://alexxz.ru/habr/unserialize_benchmark.tar.gz

Используемый софт и железо PHP 5.3.2
Linux ubuntu 2.6.32–24-generic (10.4)
Intel® Core2 CPU 6600 @ 2.40GHz

______________________
Текст подготовлен в Редакторе Блогов от © SoftCoder.ru
Алексей Еремихин @alexxz
карма
67,4
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (74)

  • 0
    Больная тема. Протестируйте на 5.3.3 — будете удивлены результатом.
    • +1
      На 5.3.3 на i3 получил «Test serialize Serialize set time 1.4620440006256 get time 36.045368909836». То есть где-то так же. Правда, пришлось memory_limit до гига задрать.
  • +8
    Длина массива 2*10^6? Неслабо
  • +3
    В комментариях к статье «Производительность кодирования и декодирования serialize и json» (http://habrahabr.ru/blogs/php/30210/) рекомендуют для кодирования больших объемов данных использовать реализацию на PHP формата Bencode.
    • 0
      Это случаем не то, чем в header'е torrent-файлов информацию пакуют?
      • 0
        Прошу прощения за невнимательность, с недосыпа не заметил ссылку.
        Да, это именно этот формат.
  • 0
    Для больших массивов попробуйте использовать var_dump($var, true); и пишите в файл. По идее в данном случае файловая операция будет быстрее чем 30 секунд.
    • +6
      Вы же имели ввиду var_export?
      • 0
        Да, конечно :)
    • 0
      Это serialize. А unserialize в таком случае как сделать?
      • 0
        eval (для var_export)
        • 0
          Интерестно а что будет если подменить файлик c массивом ( или запись в базе ) на произвольный PHP-код?
          • +3
            Если есть доступ к сессии на запись, то и заменять не нужно ничего, по сути у вас и так полный доступ.
          • +1
            Догадайтесь.
        • 0
          А вы время засекали? Сериализация ведь занимает мало времени, основная проблема десериализация.
          • 0
            Я — нет. Вариант предложил kvf77.
          • +2
            Собственно:

            ini_set('memory_limit', '512M');
            $file = '/tmp/1';
            $data = range(1,2000000);

            echo «Test serialize\n»;
            $time0 = microtime(1);
            file_put_contents($file, var_export($data, true));
            unset($data);
            $time1 = microtime(1);
            eval( '$data='.file_get_contents($file).';');
            $time2 = microtime(1);

            $timeset = $time1-$time0;
            $timeget = $time2-$time1;

            echo «Serialize set time $timeset get time $timeget\n»;

            Результат:
            Serialize set time 3.1096308231354 get time 2.9017460346222
            • 0
              Хороший результат =) в таком случае он имеет право на жизнь, мне такая вариация больше понравилась: habrahabr.ru/blogs/php/104069/#comment_3243873
              • +1
                Возможно дело в машине, но у меня json_* показали такие результаты: set time 0.533811092377 get time 0.83913397789
                php 5.3.3, core 2 duo, ubuntu
                • 0
                  сори 5.3.2 core 2 duo e7400 2.8 =)
            • 0
              еще можно eval( '$data='.file_get_contents($file).';'); заменить на include $file;
              • 0
                для инклуда тогда нужно в начало файла писать "
                • 0
                  эх, какая-то сволочь опять в карму накакала и теперь даже код вставить не могу :(
                  В общем, eval универсальнее и позволяет читать не только из файла, но и например из БД
        • +6
          Можно сделать лучше:
          file_put_contents($file, '<?php return'.var_export($data,1).'; ?>');
          $data = include $file;
        • 0
          Можно при экспорте запихнуть в теги <?php ?> и потом просто делать include или require, так по крайней мере закешируется опкод. Это если писать в файл. Если в сессию, то такое не пойдет :(
          • +1
            Извините, тормознул.
  • +5
    Интересно другое. Зачем сериализовать массивы, содержащие 2млн элементов?
    • 0
      Например, есть исследовательский скрипт, который производит анализ определённых данных. Для его работы первоначально нужно составить справочник на 2млн элементов. Справочник составляется долго. Хочется ускорить скрипт. Чтобы не составлять справочник лишний раз, если исходные данные не изменились, можно этот справочник закешировать.
      • +1
        почему бы не использовать любое key:value хранилище?
        • +1
          Да, я понимаю, что можно сделать быстрее, лучше и прочнее. Дело в том, что цели — исследовательские (причем исследование не возможностей PHP, а исследование данных), потому очень часто приходится менять и формат хранения и способы использования.

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

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

          Осталась проблема с квадратичным временем выполнения unserialize. Об этом и написана статья.
          • +1
            Редис хорошо и быстро работает с операциями над множествами, попробуйте попробовать =)
      • 0
        А вы не пробовали эксперементировать с ленивой загрузкой или подобными методами?
      • 0
        Небольшой оффтоп: Я что-то не понимаю, но почему здесь нельзя использовать БД?
  • +3
    Попробуйте IG Binary
  • –1
    Попробуйте Data::Dumper и eval.
    Или Storable.
  • +2
    хоть и ужасно несекурно, но если нужен результат «на скорую руку»:
    function cache_store($data, $key) {
    file_put_contents(«cache/».$key.".php", "
    • +1
      парсер — кака
      pastebin.com/xgbL3NUs
      • –2
        Сомнительный метод, как мне кажется. Делать include() для каждого ключа неразумно.
        • 0
          Опомнитесь, key это имя кешируемого элемента
          • 0
            Точно, упустил из виду, что там делается var_export($data, true) для каждого ключа.
            Изначально подумал, что это была попытка сделать key=value на файлах.
    • 0
      в обычной жизни евал (или инклюд, как в примере) гораздо тормознее сериалайза
      • 0
        … и значительно шустрее unserialize'а. проверено на собственной шкуре. даже массивчика в 2К элементов достаточно, чтобы заставить тупить сайт, использующий unserialize. при тех же условиях include работает сильно быстрее.
        • 0
          2к элементов (исходник):

          unserialize: 4.7870788574219
          unserialize + file_get_contents: 6.4405863285065
          eval: 14.268187999725
          include: 21.123723506927
          • 0
            ну может сферический unserialize в вакууме и обгоняет сферический include… в боевых условиях, когда данные — это не просто range(1, 2000), а многомерный массив и обращение к этим данным происходит очень часто include выигрывает (наверное, за счёт кеша фс). по крайней мере, когда в DLE был дефолтный кеш на serialize+unserialize страница под нагрузкой генерилась 20 секунд. когда я сделал var_export+include она стала генериться 400 мсек.
            • 0
              В моем примере не использовался range(1, 2000), а условия генерации очень близки к реальной работе. Давайте подумаем вместе: чтобы загрузить serialize-данные необходимо просто разместить их в памяти, а для инклюд-данных нужно сначала интерпретировать код, что уже само собой отнимает время. Поэтому врядли кеш фс играет тут какую-то роль. Может быть в ваших экспериментах не учтен ускоритель пхп (xcache, eAccelerator)?
              • 0
                Давайте подумаем :)

                Чтобы десериализировать данные нужно:
                1. вычитать их из хранилища (файл, база, етс.)
                2. разместить их в памяти
                3. распарсить их в пхп-структуру, проверив корректность

                Чтобы сделать инклюд нужно:
                1. вычитать код из хранилища
                2. разместить его в памяти
                3. выполнить его, проверив на корректность

                По-вашему выходит так, что распарсить строку в пхп-структуру быстрее, чем сделать eval. Вполне возможно, что в идеальных условиях так и есть, но в реальности, я повторюсь, мой метод выиграл у unserialize'а по производительности с огромным опережением. Речь идёт о проекте с ~200k хитов в сутки. Без нагрузки unserialize справлялся отлично, но как только нагрузку дали страница стала генериться по полминуты. Заменил unserialize на include — вышло 200-400 мсек без каких-либо акселераторов.
                • 0
                  Может быть дело в том, что размер массива, записанного в сериализованном виде был 8 мегабайт? :-D
                • 0
                  1. Таки что же вы подразумеваете под «выполнить его» в 3-ем пункте инклюда?
                  2. Сравнима ли интерпретация пхп-кода и простая проверка на длину данных + размещение данных в памяти в serialize?
                  3. Что значит «нагрузку дали»? Закончилась RAM?
                  4. Используется ли кешатор пхп-кода в RAM (xcache, eAccelerator и т.п.)?
                  • 0
                    1 — Ну таки код РНР нужно перегнать в Zend-структуру? Или нет? :)
                    2 — Вы считаете, что unserialize — это только проверить длину строки и разместить в памяти? А то, что из строки нужно получить массив с правильными типами каждого элемента? Скорее всего внутри РНР идёт посимвольная обработка строки.
                    3 — Значит, запустили ab :)
                    4 — Не используется до сих пор. Всё, что изменили — переехали с винды на генту и спрятали апач за джинкс, но всё это вряд ли критично для конкретно обсуждаемого куска кеша.
                    • 0
                      > Ну таки код РНР нужно перегнать в Zend-структуру? Или нет? :)

                      то есть распарсить, проверить на синтаксис и интерпретировать, в процессе чего будет перегонка данных в структуру?

                      > Вы считаете, что unserialize — это только проверить длину строки и разместить в памяти

                      в том-то и дело, был даже такой баг когда при неверных значениях длины данных, пхп выдавал куски таблицы памяти

                      > Значит, запустили ab :)

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

                      • 0
                        1 — в процессе интерпретации ;-)
                        2 — ещё раз, я не говорю про длину и всё такое. кроме проверки длины нужно РАЗОБРАТЬ строку и СФОРМИРОВАТЬ структуру.
                        3 — это был список категорий :-) он практически не менялся. память не закончилась, памяти было несколько гигов свободно. сильно загружался процессор.
                        • 0
                          > ещё раз, я не говорю про длину и всё такое. кроме проверки длины нужно РАЗОБРАТЬ строку и СФОРМИРОВАТЬ структуру.

                          я правильно понимаю, что затраты на разбор строки сериализованных данных несравнимы с затратами на распарсивание кода?

                          > это был список категорий :-) он практически не менялся. память не закончилась, памяти было несколько гигов свободно. сильно загружался процессор.

                          похоже, тут дело в кешировании исполняемого кода системой и получается прирост в производительности если данные одни и те же
                          • 0
                            > я правильно понимаю, что затраты на разбор строки сериализованных данных несравнимы с затратами на распарсивание кода?

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

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

                            стоял (и стоит) голый интерпретатор, без оптимайзеров, без кешеров. не могу утверждать на 100%, код не читал, но мне кажется, что сам по себе интерпретатор не кеширует код между запросами.

                            зы: php 5.2.13 win32, apache 2.2, windows 2k3 32bit, 8 процессоров по 2 ядра, 16 гиг оперативки.
                            • 0
                              Думаю, из моих тестов можно сделать выводы по скорости разбора (4-5 раз). Наивно предполагать, что десеаризация, скажем, объекта будет занимать дольше его же распарсивания в коде.

                              С некоторой погрешностью можно представить мою мысль наглядно:
                              // unserialize
                              $data = unserialize("...");

                              // упрощенный процесс include
                              eval('$data = unserialize("...")');
                              • 0
                                упрощённый процесс инклюда другой :)
                                eval('$data = "..."');
                                откуда там десериализация внутри евала?
                                • 0
                                  Все правильно, может быть $data = "...", может $data = array("...") или $data = new Dir;
                                  считаю, что ресурсы на распарсивание данных после "=" сопоставимы с разбором unserialize.
                                  • 0
                                    Простите, не уследил за вашей мыслью. Можно медленно и подробно, что с чем и почему :)
                                    • 0
                                      чтобы подробно, можно посмотреть этот код, чтобы медленно увеличьте кол-во итераций.

                                      unserialize: 13.223651647568
                                      php: 11.908849477768
                        • 0
                          кстати, на графике в посте не видно кривой инклюда
                          • 0
                            автор в посте инклюды не тестировал :)
                            • 0
                              цель статьи рассказать про проблему unserialize, а не найти лучший способ
  • –2
    >В PHP есть две замечательные функции serialize и unserialize.

    никакие они не замечательные, встроенные объекты абсолютно не переваривают. Причем без всяких предупреждений, просто на выходе пустой объект. И разработчики даже не считают это багом :(
    • 0
      Отключи magic quotes или перед unserialize убери слеши ( stripslashes ).
      • 0
        при чем тут magic quotes, если прямо после serialize получается пустой объект? Если думаешь, что я делаю что-то не так, можешь не стараться: в документации есть примечание про встроенные объекты
  • 0
    То есть на наборе, на котором serialize отрабативает за 30 сек., json_encode — за 0.27..? Я правильно понял?
  • 0
    на P4 3.0Ghz, ubuntu выдало: «Test serialize Serialize set time 9.42804217339 get time 65.6582648754» о_О
  • +1
    Test serialize
    Serialize set time 1.63469910622 get time 2.52785992622

    Intel® Core(TM)2 Quad CPU Q9300 @ 2.50GHz
    PHP 5.3.2-1ubuntu4.2
  • 0
    Есть такая засада. В общем, они там при десериализации с целью поддержки ссылок каждую переменную кладут еще и в отдельный список. Список этот — обычный односвязный, по 1024 переменные на элемент списка. Правда, чтобы положить очередную переменную, они проходятся по списку, начиная с головы, так что сложность алгоритма получается порядка N2*log(N)/1024.

    • 0
      bugs.php.net/bug.php?id=52832

      Вину за собой признают, правда патчить 5.3 отказываются, видимо, пойдет в 6.0 (5.4?).
  • 0
    Я обнаружил, что время работы функции unserialize может оказаться неожиданно большим.
    к бабке гадалке далеко ходить не надо, ясен пень, что большим. Все что связано с деревьями — это тяжелые операции. Все почему-то не любят по этому XML, но забывают про другие структуры данных. Но если использовать простые структуры данных (типа одномерного массивчика), то это на фоне общей производительность будет не заметно.
    • 0
      Я сначала тоже подумал, что здесь слишком большое время построения двоичного дерева по ключам, и особенность принципиальная. Потому я и проверил эту гипотезу на JSON. Как видим, можно сделать и быстрее, но почему-то не сделано.
      • 0
        Все из-за кривой реализации поддержки ссылок, в транке уже поправили, см. мою ссылку выше.

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