Вы всё еще используете CJSON? Если нет, то эта короткая заметка не для вас.

Библиотека CJSON известна разработчикам, работающим с фреймворком Yii (версии старее 2.х). И кажется волне логичным использовать в проектах на Yii то, что рекомендуют сами разработчики этого, в целом весьма неплохого, фреймворка.

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

А вот почему «нет»:



Тест стандартного расширение JSON в PHP:

Input size, kb: 866,55
MEM, kb: 13363,2
Time, msec: 0,0292

Тест CJSON:

Input size, kb: 866,55
MEM, kb: 12006,4
Time, msec: 1,9649


Где Input size — это размер строки в формате json, поступающей на вход.

В обоих случаях под замеры попали две последовательные операции: decode и encode получившегося объекта. Для чистоты эксперимента показаны усредненные значения за 10 итераций.

Выводы делайте сами.

И, да, вы всё еще используете CJSON?

UPD: Обновил данные статистики. Для подсчета потребляемой оперативной памяти используется memory_get_usage() с параметром true. Также высчитаны средние значения за 10 итераций.

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


  1. SerafimArts
    05.08.2015 15:09

    Json на C++, CJson на php (не знаю, обёртка ли это или полная реализация). Что удивительного в том, что оно медленнее в ~20 раз? В Ruby 1.9, например, работа с json (декодирование) примерно раз в 60 медленнее, чем в php, но ничего, пользуются, хотя опытные разработчики обычно предпочитают msgpack.

    Я думаю стоит привести примеры тестов. Т.к. в реальной жизни не наберётся и больше пары-тройки кодирований\декодирований на один запрос, где даже подобная разница (~2000%) по скорости будет не существенна, учитывая то, что в тесте цифра в 2 секунды на 100 и более итераций, а не указанные мною выше 2-3.


    1. Fesor
      05.08.2015 15:14

      полная реализация на PHP.

      Я думаю стоит привести примеры тестов.


      То есть вы не верите что код типа:

      namespace my\json;
      
      function decode($json) {
           $data = \json_decode($json);
           if (JSON_ERROR_NONE !== \json_last_error()) {
              throw new \InvalidArgumentException('Invalid JSON');
           }
      
           return $data;
      }
      


      работает на порядки быстрее чем реализация парсера json на PHP?


      1. Fesor
        05.08.2015 15:18

        Я когда-то использовал seld/jsonlint для своего матчера (fesor/json_match) только ради красивых пояснений что не так в структуре json, в итоге отказался от него именно по причине паршивой производительности и того факта что фича давала весьма сомнительный профит.


      1. SerafimArts
        05.08.2015 15:22

        Я не вижу, минимум, количество итераций, как следствие — не могу оценить качество подобного теста. Если там те же 100 итераций на больших данных, то получим 0.01 секунды на кодирование/декодирование, т.е. можно задуматься (по крайней мере иметь ввиду на будущее), даже если подобных вызовов всего пара. Если же там 1000 итераций, то это 0.001 секунды, что вообще можно не брать в расчёт, даже если там будет оверхед не в 2000%, а в 10000%.


        1. Fesor
          05.08.2015 15:24

          разница как раз таки существенна, я правда только с seld/jsonlint гонял, но он где-то раз в 40-50 медленнее.


          1. SerafimArts
            05.08.2015 15:35

            Самое популярное предназначение json — это отдать данные по АПИшке. Что там у нас ещё есть? Ну например вставка инлайн данных для сингл-пейдж (и не только) приложений, например внутрь тега script. Может быть ещё чтение конфигов… Или костыльное превращение инстанса чего-либо в объект StdClass.

            Если подытожить, то цифра в пару кодирований\декодирований думаю вполне нормальна, для среднего ресурса. Оно выполняется один раз за запрос. Разницу в 0.001 (~2000% на 1000 итераций) или 0.002 (~20000% на 1000 итераций) секунды пользователь просто не заметит. Так же не отличит и от нативного json преобразования, которое по времени займёт совсем копейки.

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


            1. Fesor
              05.08.2015 15:50
              +1

              я тестил на объемах данных порядка одного килобайта, то есть это именно те масштабы в которых происходит обмен данными через API. И разница 1ms или 20ms для меня критична (это как-то не правильно когда твоя ORM работает быстрее чем сериализация данных во view layer).


        1. SkiF_TLT
          05.08.2015 15:26
          -2

          Тут нет итераций. Статистика только за один вызов encode и decode. А размер входящих данных там указан.


          1. lair
            05.08.2015 15:27
            +4

            Статистика за один вызов — вообще не смешно.


            1. SkiF_TLT
              05.08.2015 15:40
              -2

              Не в том смысле. Цифры в статистике — за один вызов.

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


          1. SerafimArts
            05.08.2015 15:41
            +3

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


    1. SkiF_TLT
      05.08.2015 15:25
      -2

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

      Код могу показать, но он и так предельно понятен из текста:

      $obj = json_decode($test, true);
      $str = json_encode($obj);
      


      и

      $obj = CJSON::decode($test);
      $str = CJSON::encode($obj);
      


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


  1. Fesor
    05.08.2015 15:11
    +1

    Это настолько капитанское умозаключение… CJson изначально довольно бесполезная штука.


  1. lair
    05.08.2015 15:18
    +2

    А я один вижу 37-кратную разницу в потреблении памяти?


    1. Fesor
      05.08.2015 15:23
      +1

      Меня больше смущает потребление памяти в размере 20% от входящих данных, как-то это не реалистично. Насколько я помню CJson не является стрим парсером.


      1. lair
        05.08.2015 15:28
        +1

        Одно из двух: или ошибка в тесте, или мы имеем memory-cpu-компромис, который каждый решает для себя.


        1. SkiF_TLT
          05.08.2015 15:52
          -3

          Ошибка, спасибо за замечание.
          memory_get_usage() использовалось без параметра true.

          Сейчас обновлю текст.


          1. lair
            05.08.2015 17:54
            +2

            Как следует из документации memory_get_usage отдает текущее потребление (это хорошо видно по уменьшению после unset). Как вы с ее помощью собираетесь померять расходы во время парсинга?


            1. SkiF_TLT
              05.08.2015 20:53
              -2

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


              1. lair
                05.08.2015 21:50
                +3

                Идем, по ссылке, открываем первый (он же единственный) пример:

                echo memory_get_usage() . "\n"; // 36640
                $a = str_repeat("Hello", 4242);
                echo memory_get_usage() . "\n"; // 57960
                unset($a);
                echo memory_get_usage() . "\n"; // 36744
                


                Видим, что после выполнения str_repeat и присвоения — увеличилась, после unset — упала обратно.

                Теперь давайте представим, что вся эта комбинация (выделение — присвоение — очистка) находится в функции:

                function bump()
                {
                $a = str_repeat("Hello", 4242);
                unset($a);
                }
                
                echo memory_get_usage() . "\n";
                bump()
                echo memory_get_usage() . "\n";
                


                Что будет в выводе? Правильно, 36640 и 36744 (порядок, конечно). Скачок память до 57960 мы пропустили. Вот то же самое и в вашем тесте (код которого вы все еще не показываете) — если замерять память до и после кодирования/декодирования, то вы увидите только расходы на результат, но не то, что было потрачено в процессе.

                Впрочем, вообще, замерять память изнутри самого процесса — дело той еще привлекательности.


                1. SkiF_TLT
                  06.08.2015 09:12
                  -1

                  Во-первых, еще раз: в заметке не говорится про пиковое потребление метода. С помощью memory_get_usage я всегда замеряю избытки памяти после выполнение какого-то куска кода. Проще говоря, утечки памяти.

                  Во-вторых, ваш скрипт просто показывает количество используемой памяти на этапах до и после выполнения функции.

                  Например, если убрать unset($a), то данные изменятся. Но при этом разница их останется на прежнем уровне:

                  <?php function bump() { $a = str_repeat("Hello", 4242); unset($a); } echo ($m1 = memory_get_usage()) . PHP_EOL; bump(); echo ($m2 = memory_get_usage()) . PHP_EOL; echo $m2 - $m1 . PHP_EOL; /* RESULT: 226576 226792 216 */

                  и

                  <?php function bump() { $a = str_repeat("Hello", 4242); //unset($a); } echo ($m1 = memory_get_usage()) . PHP_EOL; bump(); echo ($m2 = memory_get_usage()) . PHP_EOL; echo $m2 - $m1 . PHP_EOL; /* RESULT: 226528 226744 216 */

                  И тут, кстати, видна одна истина, про которую вы почему то забыли или просто умолчали: все локальные переменные умирают вместе с завершением функции, в которой они были инициализированы. Соответственно делаете ли вы для них unset в конце или не делаете — разницы нет.

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

                  Теперь то я понятно изложил?

                  P.S. Кстати, тут disem выложил результаты своего независимого теста по сабжу. И это только подтверждает то, о чем, собственно заметка и была. Или вы ему тоже не верите?

                  P.P.S. Увы, форматирование комментария теперь не работает, спасибо вам. Так что ссылку на аналогичный тест дать не могу, полистайте комментарии.


                  1. lair
                    06.08.2015 09:48
                    +3

                    Теперь то я понятно изложил?

                    Вы продолжаете не понимать, в чем проблема. Под реальной нагрузкой вам будут важны не только, как вы выражаетесь, «утечки», но и конкретное потребление скрипта в каждый момент времени: если у вас во время парсинга JSON система отъедает десятки мегабайт памяти, то при параллельной нагрузке шанс, что памяти не хватит, увеличивается (и не важно, что потом вы память отпускаете).

                    Кстати, тут disem выложил результаты своего независимого теста по сабжу. И это только подтверждает то, о чем, собственно заметка и была. Или вы ему тоже не верите?

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

                    Увы, форматирование комментария теперь не работает, спасибо вам.

                    А при чем тут я?


                    1. SkiF_TLT
                      06.08.2015 10:21
                      -1

                      Да я понял это с первого раза, но мне важнее было насколько парсер засирает, простите за выражение, память (потому что оперативки на пиковые нагрузки мне не жалко, а вот мусор, если скрипт работает постоянно, — выливается в большую проблему).

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


                      1. disem
                        06.08.2015 12:33
                        +2

                        Вообще это абстрактные цифры в вакууме, так как данные были взяты не боевые а случайные, сам «измеритель» ничего полезного сказать не может, а оверхед CJSON'а видно и без тестов.
                        То, что CJSON работает медленнее доказано на 100%, с оперативкой остаются вопросы (тут нет xhprof спецов? может там все станет прозрачно?), но с итогом заметки я не согласен.
                        Для рядовых случаев CJSON не оправдан, если разобраться что же там под капотом происходит — есть случаи где CJSON заменит пачку костылей.
                        Посмотрите на CJSONTest… Там есть два тест кейса с красноречивым док блоком " native json_encode can't do it", а кейсы заключаются в сериализации Active Record моделей, разве этого не достаточно чтобы переосмыслить заметку?


                        1. Fesor
                          06.08.2015 12:37

                          глянул — начиная с php 5.4 может он все, есть же интерфейс JsonSerializable, который можно заимплементить в базовом классе модели. Другой вопрос что Yii с его CJson насколько я помню суппортит еще 5.2…


                          1. disem
                            06.08.2015 12:40

                            Yii совместим с php от 5.1.0

                            "require": {
                                "php": ">=5.1.0"
                            },
                            


                      1. lair
                        06.08.2015 12:55
                        +1

                        Вот именно поэтому в результате теста не указывают просто колонку «память», а расшифровывают, что это за память, как мерялась, и почему автор теста считает это измерение показательным.


                        1. SkiF_TLT
                          06.08.2015 13:30

                          Да-да, каюсь, формат подачи информации весьма неудачный.


  1. gwer
    05.08.2015 15:19
    +1

    Я правильно понимаю из ваших результатов, что CJSON при этом в ~40 раз эффективнее по памяти? И тут встает вопрос выбора между более быстрым и более экономным по памяти решением. А вы как-то очень уж однозначно в сторону скорости выбираете, выходит.


    1. SerafimArts
      05.08.2015 15:25
      -7

      Плашка оперативки стоит 2к руб. Новый процессор, боюсь что в разы больше. Так что выбор в сторону производительности вполне очевиден, конечно не стоит возводить в абсолют мои выводы, но в целом…


      1. lair
        05.08.2015 15:30
        +5

        Плашка оперативки стоит 2к руб.

        Особенно в облаке, да, инстансов так на десять.

        Понимаете, вопрос в том, как оно себя реально ведет под нагрузкой в 10-50-100-1000 параллельных запросов, какова зависимость потребления памяти, загрузки процессора и результирующего времени обработки от объема данных и количества параллельных запросов. Потому что когда у вас на 50 пользователях система начинает работать в два раза медленнее — это одно. А когда она на 51-ом падает с нехваткой памяти — это другое.


        1. SerafimArts
          05.08.2015 15:39

          Да, резонно. Соглашусь. Возможно я слишком категорично взглянул на вещи.


  1. disem
    05.08.2015 15:59
    +5

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

    //encode
    ...
    if(function_exists('json_encode'))
        return json_encode($var);
    ...
    
    
    //decode
    ...
    if(function_exists('json_decode'))
    {
        $json = json_decode($str,$useArray);
    ...
    

    А бенчмаркать на одной строке это не серьезно.


    1. Fesor
      05.08.2015 16:08

      да, видать для совместимости с 5.4 где json был выпилен из стандартной поставки из соображений лицензирования.


      1. disem
        05.08.2015 16:31

        Не, CJSON с первого коммита уже был (2008 год).
        Похоже что это тянется еще с Prado (прародителя Yii)
        FirePHP.class.php4 — вот такое нашел. Судя по расширению «php4», делалось это в связи с отсутствием json_encode на тот момент (не уверен, списка функций для php4 быстро найти не удалось).



    1. SkiF_TLT
      05.08.2015 16:42

      Вы явно лукавите, выдёргивая код из контекста.
      В Методе CJSON::encode используется json_encode, но только если на вход передана строка:

      public static function encode($var)
      {
          switch (gettype($var))
      ...
              case 'string':
                  if (($enc=strtoupper(Yii::app()->charset))!=='UTF-8')
                      $var=iconv($enc, 'UTF-8', $var);
      
                  if(function_exists('json_encode'))
                      return json_encode($var);
      ...
      


      В остальных случаях там своя обработка. И да, вопрос: часто ли вы кодируете в json обычные строки? )
      Так что проблема именно в самописном велосипеде внутри encode (который экономит какие-то крохи оперативной памяти, но безбожно жрёт процессорное время).

      Чтобы не быть голословным, вот ссылка на исходный код метода.


      1. disem
        05.08.2015 17:26

        В остальных случаях там своя обработка. И да, вопрос: часто ли вы кодируете в json обычные строки? )

        Ну в массивах например будет опять таки дергать json_encode так как все в конце концов приводится к строкам.
        return '[' . join(',', array_map(array('CJSON', 'encode'), $var)) . ']';
        

        Для объектов добавлена проверка на JsonSerializable который появился в 5.4.0, в простых типах возвращает тоже самое что и json_encode.
        Нельзя сказать что это все хорошо, но новые фичи вводить не ломая старые не так то просто.

        BTW в Yii 2 тоже есть препроцессор энкода.


    1. lair
      05.08.2015 17:52
      +1

      Если это так (простой фолбэк), то (а) откуда почти лишних две мс на вызов (неужели function_exists столько съедает?) и (б) почему потребление памяти на мегабайт меньше?


      1. disem
        05.08.2015 18:29

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


        1. lair
          05.08.2015 18:32

          Значит, это не простой фолбэк, а у него внутре неонка логика.

          (то, что PHP не может оптимизировать множественные вызовы function_exists — это, конечно, восторг)


          1. Fesor
            05.08.2015 19:44
            +1

            это, конечно, восторг


            PHP на данный момент много чего не оптимизирует что стоило бы, но конкретно что бы понимать почему оптимизация function_exists не такая простая штука, следует вспомнить что PHP таки динамический язык:

            assert(false === function_exists('foo'), 'Foo should not exists');
             
            eval('function foo () { return "foo"; }');
             
            assert(true === function_exists('foo'), 'Foo should exists');
            


            А еще можно подключить в любой момент функцию через require/include… словом тут все чуть усложняется. Да и это не узкое место на данный момент.


  1. andrewnester
    05.08.2015 17:00
    +3

    А вы сам CJSON отдельно вне фреймворка использовали или загружали весь Yii?


    1. SkiF_TLT
      05.08.2015 20:46
      -3

      Тест проводился без вызова Yii::createWebApplication($config)->run();, если вы это подразумеваете под «загружали весь Yii».


  1. disem
    05.08.2015 19:06

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

    hhvm speedtest.php 
    string(32) "Test case elements count: 113530"
    string(31) "Execution time: 2.4736559391022"
    string(12) "42.594744 MB" //memory_get_usage(true)
    string(13) "140.213176 MB" //memory_get_peak_usage(true)
    


    /usr/bin/php5 speedtest.php 
    string(32) "Test case elements count: 113530"
    113530string(31) "Execution time: 6.6452739238739"
    string(13) "215.744512 MB" //memory_get_usage(true)
    string(13) "277.348352 MB" //memory_get_peak_usage(true)
    


    json_encode
    hhvm speedtest.php 
    string(32) "Test case elements count: 113530"
    113530string(32) "Execution time: 0.37734508514404"
    string(12) "42.619448 MB" //memory_get_usage(true)
    string(12) "140.24484 MB" //memory_get_peak_usage(true)
    


    /usr/bin/php5 speedtest.php 
    string(32) "Test case elements count: 113530"
    113530string(32) "Execution time: 0.71565389633179"
    string(13) "233.832448 MB" //memory_get_usage(true)
    string(12) "290.97984 MB" //memory_get_peak_usage(true)
    

    Отсюда напрашивается еще один вывод… Юзайте hhvm!


    1. SerafimArts
      05.08.2015 21:51
      -3

      Чем php7 не угодил, что сразу hhvm бежать ставить? Оно вроде по скорости вполне сопоставимо.