PHP 7 получит в два раза более эффективный Hashtable


Начатый процесс переписывания ядра PHP идет семимильными шагами. Эта статья вольный пересказ поста одного из авторов кода ядра PHP о достигнутых значительных успехах в оптимизации такой структуры данных, как hashtable. Больше технических подробностей под катом.
Простой скрипт, который создает массив из 100000 целых чисел, демонстрирует следующие результаты:
32 bit 64 bit
PHP 5.6 7.37 MiB 13.97 MiB
PHP 7.0 3.00 MiB 4.00 MiB

Код теста
$startMemory = memory_get_usage();
$array = range(1, 100000);
echo memory_get_usage() - $startMemory, " bytes\n";


PHP 7, как можно видеть, потребляет в 2.5 меньше памяти в 32-х разрядной версии и в 3.5 раза в 64-х разрядной, что согласитесь впечатляет.

Лирическое отступление


В сущности в PHP все массивы это упорядоченные словари (т.е они представляют собой упорядоченный список пар вида ключ-значение), где ассоциирование ключа значению реализовано на основе hashtable. Сделано это для того, чтобы ключами массива могли выступать не только целочисленные типы, а и сложные типы вроде строк. Сам процесс происходит следующим образом — от ключа массива берется хэш, который представлен целым числом. Это целое число используется как индекс в массиве.

Проблема возникает, когда хэширующая функция возвращает одинаковые хэши для разных ключей, то есть возникает коллизия (то, что это действительно происходит легко убедится – количество возможных ключей бесконечно велико, в то время как количество хэшей ограничено размером типа integer).

Существуют два способа борьбы с коллизиями. Первый – открытая адресация, когда элемент сохраняется с другим индексом, если текущий уже занят, второй – хранить элементы с одинаковым индексом в связном списке. PHP использует второй способ.

Обычно элементы hashtable никак не упорядочены. Но в PHP делая итерацию по массиву, мы получаем элементы именно в том порядке, в котором мы их туда записывали. Это означает, что hashtable должна поддерживать механизм хранения порядка элементов.

Старый механизм работы hashtable


Эта схема наглядно демонстрирует принцип работы hashtable в PHP 5:

Элементы разрешения коллизий на схеме обозначены как buckets (корзина). Для каждой такой «корзины» выделяется память. На картинке не показаны значения, которые хранятся в «корзинах», так как они помещаются в отдельные zval-структуры размером 16 или 24 байта. Также на картинке опущен связный список, который хранит порядок элементов массива. Для массива, который содержит ключи «a», «b», «c» он будет выглядеть так:

Итак, почему старая структура неэффективна в плане производительности и потребляемой памяти?
  • «Корзины» требуют выделения места. Этот процесс довольно медленный и дополнительно требует 8 или 16 байт оверхеда в адресном заголовке. Это не позволяет эффективно использовать память и кэш.
  • Структуры zval для данных также требует выделения места. Проблемы с памятью и оверхом заголовка все те же, что и у «корзины», плюс использование zval обязывает нас хранить указатель на него в каждой «корзине»
  • Два связных списка требуют в сумме 4 указателя на корзину (т.к. списки двунаправленные). Это отнимает у нас еще от 16 до 32 байт. Кроме того, перемещение по связному списку это операция, которая плохо поддается кешированию.

Новая реализация hashtable призвана решить эти недостатки. Структура zval была переписана таким образом, чтобы ее можно было напрямую включать в сложные объекты (например, в вышеупомянутую «корзину»), а сама «корзина» стала выглядеть так:
typedef struct _Bucket {
	zend_ulong        h;
	zend_string      *key;
	zval              val;
} Bucket;

То есть «корзина» стала включать в себя хеш h, ключ key и значение val. Целочисленные ключи сохраняются в переменной h (в таком случае хеш и ключ идентичны).
Давайте взглянем на всю структуру целиком:
typedef struct _HashTable {
	uint32_t          nTableSize;
	uint32_t          nTableMask;
	uint32_t          nNumUsed;
	uint32_t          nNumOfElements;
	zend_long         nNextFreeElement;
	Bucket           *arData;
	uint32_t         *arHash;
	dtor_func_t       pDestructor;
	uint32_t          nInternalPointer;
	union {
		struct {
			ZEND_ENDIAN_LOHI_3(
				zend_uchar    flags,
				zend_uchar    nApplyCount,
				uint16_t      reserve)
		} v;
		uint32_t flags;
	} u;
} HashTable;

«Корзины» хранятся в массиве arData. Этот массив кратен степени двойки и его текущий размер хранится в переменной nTableSize (минимальный размер 8). Реальный размер массива хранится в nNumOfElements. Заметьте, что массив теперь включает в себя все «корзины», вместо того чтобы хранить указатели на них.

Порядок элементов


Массив arData теперь хранит элементы в порядке их вставки. При удалении из массива элемент помечается меткой IS_UNDEF и не учитывается в дальнейшем. То есть при удалении элемент физически остается в массиве пока счетчик занятых ячеек не достигнет размера nTableSize. При достижении этого лимита PHP попытается перестроить массив.
Такой подход позволяет сэкономить 8/16 байт на указателях, по сравнению с PHP 5. Приятным бонусом также будет то, что теперь итерация по массиву будет означать линейное сканирование памяти, что гораздо более эффективно для кеширования, чем обход связного списка.
Единственным недостатком остается неуменьшающийся размер arData, что потенциально может привести к значительному потреблению памяти на массивах в миллионы элементов.

Обход Hashtable


Хэшем заведует функция DJBX33A, которая возвращает 32-х или 64-х битное беззнаковое целое, что слишком много для использования в качестве индекса. Для этого выполняем операцию «И» с хэшем и размером таблицы уменьшенным на единицу и результат записываем в переменную ht->nTableMask. Индекс получаем в результате операции
idx = ht->arHash[hash & ht->nTableMask]

Полученный индекс будет соответствовать первому элементу в массиве коллизий. То есть ht->arData[idx] это первый элемент, который нам нужно проверить. Если хранимый там ключ соответствует требуемому, то заканчиваем поиск. В противном случае следуем к следующему элементу до тех пор пока не найдем искомый или не получим INVALID_IDX, что будет означать что данный ключ не существует.
Прелесть такого подхода заключается в том, что в отличии от PHP 5, нам больше не требуется двусвязный список.

Сжатые и пустые hashtable


PHP использует hashtable для всех массивов. Но в определенных случаях, например, когда ключи массива целые, это не совсем рационально. В PHP 7 для этого применяется сжатие hashtable. Массив arHash в таком случае равен NULL и поиск идет по индексам arData. К сожалению, такая оптимизация применима, только если ключи идут по возрастанию, т.е. для массива [1 => 2, 0 => 1] сжатие применено не будет.

Пустые hashtable это особый случай в PHP 5 и PHP 7. Практика показывает, что пустой массив имеет неплохие шансы таковым и остаться. В этом случае массивы arData/arHash будет проинициализированы только когда первый элмент будет вставлен в hashtable. Для того чтобы избежать костылей и проверок используется следующий прием: пока меньше или равно своему значению по-умолчанию, nTableMask устанавливается в ноль. Это означает, что выражение hash & ht->nTableMask всегда будет равно нулю. В этом случае массив arHash содержит всего один элемент с нулевым индексом, который содержит значение INVALID_IDX. При итерации по массиву мы всегда ищем до первого встреченного значения INVALID_IDX, что в нашем случае будет означать массив нулевого размера, что и требуется от пустого hashtable.

Итог


Все вышеперечисленные оптимизации позволили сократить занимаемый элементом размер с 144 байт в PHP 5 до 36 (32 в случае сжатия) байт в PHP 7. Небольшой недостаток новой реализации – слегка избыточное выделение памяти и отсутствие ускорения, если все значения массива одинаковы. Напомню, что в PHP 5 в этом случае используется всего один zval, так что уменьшение потребление памяти значительное:

Код теста
$startMemory = memory_get_usage();
$array = array_fill(0, 100000, 42);
echo memory_get_usage() - $startMemory, " bytes\n";


32 bit 64 bit
PHP 5.6 4.70 MiB 9.39 MiB
PHP 7.0 3.00 MiB 4.00 MiB

Несмотря на это, PHP 7 все равно показывает лучшую эффективность.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 58

    –2
    Поле uint32_t nTableSize избыточно. Оно хранит 29 различных значений (5 бит), а рядом есть uint16_t reserved. Можно ещё на 4 байта сократить. :)
      +1
      Выравнивание структуры в памяти, ни когда не занимались? Дает ощутимый прирост производительности. Да, компилятор может сам выравнять, Про компилятор писать не нужно, у каждого он свой.
      Ну а если часть в какой нибудь set засунуть :) (По моему что то такое битовое было в с/с++)
        0
        Я не говорил, что надо заменить на поле в конкретно 5 бит. Машинный байт вполне подойдёт под это редко адресуемое поле.
          0
          Можно долго спорить, но я бы оставил 4 байта.
          Многозадачность, обращение в поле тормозит всю память системы и т.д. Пусть сидит в «ровном месте»
        +1
        Что? reserved же часть объединения, и это поле используется исключительно для того, что бы размер структуры был всегда константен… Собственно по этой же причине размер поля flags 4 байта а не бит. И не понятно что вы собирались сократить…
          –4
          reserved это неиспользуемое поле, просто overhead.
            0
            Купить книжку по C++ (можно и по C) и читать, читать, читать, читать. Про различия struct и union. До наступления полного просветления.
              –2
              Вот именно, пойдите и почитайте. Поле reserve это не поле union-а, а поле структуры. Оно не используется — в него не пишутся данные и не читаются, и служит оно не для туфты, что Fesor написал, а чтобы верно адресовать 8-битные поля той же структуры на LE и BE машинах.

              Впрочем, что я Си php-шникам объясняю. Купите книжку по Си и читайте.
                0
                Тяжёлый случай.
                Поле reserve это не поле union-а, а поле структуры.
                А ничего, что это поле другой структуры? Которая, в свою очередь, расположена в unionе?

                Если вы предлагаете урезать поле flags до 16 бит и/или перенести в него nTableSize — то так и пишите. Я не знаю — используют ли разработчики больше 16 бит в поле flags или нет, но если не используют, то, действительно, можно откуcить пару байт от поля flags пару байт и перенести из в nTableSize. Но использовать reserve вместо nTableSize, увы, нельзя — о чём вам Fesor и написал. И он совершенно прав.
                  0
                  Вы, похоже, никогда не писали на Си. Меня эта ветка уже достала, поэтому разжёвываю последний раз.

                  По негласной конвенции поля, именуемые reserved (или похожим образом) обозначают неиспользуемое, «зарезервированное» для будущего использования пространство в памяти. Это позволяет расширять контракт (протокол, ABI, что угодно) не ломая обратной совместимости. Также эти поля иногда зовутся «паддингами» (padding) и служат для установления размера структуры, к которой они принадлежат, или смещения следующего за ними поля.

                  В данном случае uint16_t reserve занимает два байта и «двигает» два следующих за ним поля на эти два байта. Таким образом в наименее значащих байтах структуры, при представлении структуры в виде машинного 32-битного значения, оказывается одно и то же поле, вне зависимости от порядка байтов в машинном слове.

                  Это нужно, в общем-то, только для одного: оптимизация доступа к памяти. На x86 архитектуре, на которой обычно запускается php-сервер, скорость доступа к 8-битным и 16-битным ячейкам памяти сильно меньше, чем к 32-битным. Кроме того, имена flags намекают на использовании этого отрезка памяти в виде флагов, а битовые операции с ними тоже быстрее работают с 32-битными значениями.

                  Таким образом появляется поле uint32_t flags, занимающее то же адресное пространство, что и структура с полем reserve. Наименее значащий байт этого поля занимает 8-битное поле flags. Поэтому битовые операции в пределах 8 бит над обоими полями flags будут полностью эквивалентны, и не будут модифицировать какие-либо биты в пределах наименее значащих восьми. (почему)

                  Именно поэтому вы можете просто взять и использовать простанство поля reserve для хранения каких-то данных. Ранее это уже было сделано, так появилось поле nApplyCount.

                  Я надеюсь это моё оъяснение внесло в вашу голову какое-то просветление. Если же нет, или вы хотите продолжить это путешествие в мир Си, я рекомендую вам последовать вашему же совету: купите книгу по Си и читайте. Я не против, если вы напишите пост на хабр о том, что вы узнали во время чтения.

                  /cc Fesor — также рекомендую к прочтению.
                    0
                    Вы, похоже, никогда не писали на Си.
                    А вы, похоже, писали, но не так чтобы много.

                    Вы здесь заложились на кучу факторов, которые из состава структуры напрямую никак не следуют. В частности на то, то что
                    Таким образом появляется поле uint32_t flags, занимающее то же адресное пространство, что и структура с полем reserve
                    что, я извиняюсь, ниоткуда не следует. Кто вам сказал, что uint32_t flags — это оптимизация и ничего больше? Кто вам сказал, что никаких других флагов, кроме тех, которые есть в 8-битном поле flags в природе не бывает? Почему не может быть так, что при некоторых условиях (скажем в зависимости от значение поля nTableSize, LOL) у вас не могут появится там дополнительные флаги? Я ничего не знаю про PHP, но в GCC, с которым я за последние лет 10 успел пообщаться изрядно так бывает сплошь и рядом: в одной ветке у вас будут использоваться uint8_t flags и nApplyCountтам, в этой ветке, поле reserved таки будет именно reserved), а в другой ветке — у вас будет 30 флагов поле uint32_t flags и никакого nApplyCount. И как вы тут будете избавляться от nTableSize в «большой» структуре?

                    Вот так просто бездумно перенести nTableSize нельзя, уж извините. Для этого нужно не только точно знать что там происходит в коде сейчас, но и иметь какую-то информацию о том, что там планируется делать в будущем. Как вы смогли залезть в голову к разработчикам PHP чтобы узнать об их планах — для меня, извините, загадка. Тем более странны удивляет ваша реакция на указание на вашу ошибку: вместо того, чтобы сказать что-нибудь типа «я знаю человека, который этот код разрабатывал и он мне сказал, что uint32_t flags — это оптимизация и ничего больше» (после чего весь ваш гонор имел бы смысл) вы поёте какие-то странные песни про названия полей и прочую муру.
                      0
                      Вы, уж пожалуйста, ни меня, ни разработчиков php за идиотов не держите. Они бы поставили какой-то комментарий, если бы были такие уловки, как вы описали. А я перед своим первым сообщением проверил исходный код. Там используется 7 бит во флагах. В паре местах придётся поправить код, но в остальном всё сооветствует здравому смыслу.
                        0
                        Они бы поставили какой-то комментарий, если бы были такие уловки, как вы описали.
                        С каких это пор стандартное использование union'ов требует отдельных комментариев? Это не уловки, это стандартный способ использования union'ов. Собственно единственно возможный в соответствии со стандартом.

                        Комментариев требует как раз использование нестандартных уловок, которые, строго говоря, делают вашу программу потенциально неработающей. В частности описанные вами трюки с двумя flags, вообще говоря, не работают (вернее работают на GCC в некоторых случаях, но может не работать на других компиляторах). Дело в том, что компилятор имеет право считать, что изменение 8-битового поля flags никак не влияет на 32-битовое поле flags. И наоборот. Несколько лет назад на Хабре была статья, где всё это осуждалось. А вне Хабра это обсуждалось ещё раньше.

                        А я перед своим первым сообщением проверил исходный код.
                        Вот с этого и нужно было начинать. С того, что вы проверили код, обнаружили, что в нём используется непереносимое расширение GCC (о чём, разумеется, разработчики даже не удосужились написать в комментариях к типу, что вряд ли делает им честь) и что исходя из вот всего этого можно перенести nTableMask.

                        А вот «наезжать» на людей, которые предполагают что unionы используются так, как они должны использоваться… не стоит.
                          +1
                          С каких это пор стандартное использование union'ов требует отдельных комментариев? Это не уловки, это стандартный способ использования union'ов. Собственно единственно возможный в соответствии со стандартом.
                          Вы как-то поздно начали стандартом махать. Надо было здесь начать: с каких это пор стандартное использование функций языка Си требует отдельных комментариев?

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

                          Разумеется, ничего подобного разработчики php не делают. И никакое расширение они не используют, и код портабельный.

                          Короче говоря, руководствуйтесь здравым смыслом, а не обвиняйте людей в халатности. А теперь уходите, еды тут больше нет.
                            +1
                            Разумеется, ничего подобного разработчики php не делают.
                            Ась? А это тогда что?

                            Поэтому битовые операции в пределах 8 бит над обоими полями flags будут полностью эквивалентны, и не будут модифицировать какие-либо биты в пределах наименее значащих восьми. (почему)

                            Ваши фантазии? Это как раз и есть непереносимость. Вы уж определитесь — у вас 32-битный flags существует для оптимизаций и используется вперемешку с 8-битным или нет. И если нет — тогда зачем он вообще нужен и зачем нужен обсуждаемый union?

                            поскольку для корректной работы по стандарту достаточно либо не брать указатели на непосредственно его поля
                            Недостаточно. Это — как раз и есть расширение GCC. По стандарту — если вы пишете uint8_t, то и читать должны uint8_t, если пишете uint32_t, то и читать должны uint32_t.

                            В противном случае — смотри «Annex J»: The values of bytes that correspond to union members other than the one last stored into… are unspecified.

                            То есть записав ваш любимый uint8_t и прочитав uint32_t вы обязательно получите-таки какое-то значение и винчестер вам никто не отформатирует, но вот беда: оно может не иметь никакого отношения к тому, что в union было записано.
                      0
                      Я просто не понимаю вот этот ваш комментарий:
                      Поле uint32_t nTableSize избыточно. Оно хранит 29 различных значений (5 бит), а рядом есть uint16_t reserved.


                      Насколько я понимаю, 29 вариантов взялось это 2^3 — 2^32. Это поле используется так же при формировании nTableMask (nTableSize-1) которое в свою очередь используется непосредственно для хеширования. Хранение этого поля в uint32 это неплохая такая оптимизация которая существенно ускоряет вычисление хэшей в массиве.

                      Вопрос о том что мы храним только степень двойки а не само значения я опущу, так как не вижу с этим проблем (разве что есть подозрения что это ударит по производительности при вставке, хотя тут нужно смотреть). Для всего остального мы уже храним nTableMask так что производительность не пострадает.

                      В остальном khim уже описал другие мои недопонимания в силу слишком малого опыта работы с Си.
                        +1
                        Я же не предлагал удалять nTableMask. Оно сейчас вычисляется при каждом изменении nTableSize, так что оверхеда не будет.

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

                        Даже с новым хранением размера операция (8 << packedSize) - 1 достаточно лёгкая, а сдвиг придётся делать в любом случае.

                        Так что, теперь можно убрать сразу 8 байтов!
                          +1
                          В остальном khim уже описал другие мои недопонимания в силу слишком малого опыта работы с Си.
                          Это не малый опыт работы с Си. Это малый опыт работы с разработчиками PHP. Потому что когда вы стреляете себе между яйцами из гранатомёта, то правила хорошего тона говорят, что этот момент стоит обговорить хотя бы в комментариях, а не предполагать, что «все так делают».

                          В качестве примера посмотрите на то, как в NaCl'е откомментированы функции, которые работают «на грани фола»: семейство функций SignExtendXXX — комментарий в строке 184). Потому что если вы сделаете что-нибудь странное (ну, скажем, используете int_fast32_t вместо int32_t), то можете легко свалиться в undefined behavior и потом долго ловить ошибки.
            0
              +3
              Это указано в первом абзаце
                0
                Пропустил, посыпаю голову пеплом. Сразу увидел знакомые значения из оригинальной статьи.
              +2
              Можно спросить, а там про юникод в ядре ничего не слышно?
              –1
              Удивился, что никто не заметил, но программа создающая массив из 100000 чисел это вот:
              $startMemory = memory_get_usage();
              $array[] = range(1, 100000);
              echo memory_get_usage() - $startMemory, " bytes\n";
              
                0
                Что?
                  +1
                  Ага, утро 31-го на работе дает о себе знать.
                  Понял свою ошибку, только когда допил вторую порцию кофеина.
                  +3
                  С ней что-то не так (или смутил двумерный массив)?
                  –3
                  Я правильно понимаю, что совершенно внутренняя вещь, такая, как конкретная реализация работы с hashtable, не появится в очередном минорном релизе, а ожидается в мажорном, причем таком, что мы увидим еще не скоро?

                  При этом в минорных без стеснения вводятся новинки, которые могут ломать совместимость с наследованным php-кодом.

                  В этом смысле логику понять не всегда можно, и хочется увидеть язык не как постоянно меняющийся, а как что-то, на чем можно сделать скрипт/страничку/сайт/панель управления, и оставить их в работе в надежде, что и через несколько лет не вылезет что-то новенькое, кардинально несовместимое со старым… Для недорогих хостингов, кстати, тема более чем важная: там часто обновление php/mysql накатывают не глядя, просто потому, что некая система обновлений нашла его, а такая drop-in замена старого php на новый совершенно неожиданно может в иных скриптах создать кое в чем новое поведение (

                  P.S. И таки да — Юникод. Говорим про 7-ку, даже не про 6-ку. Какой там год на дворе к ее выходу будет?..
                    +3
                    Можете привести пример где в минорном релизе ломается совместимость?

                    P.S. И таки да — Юникод. Говорим про 7-ку, даже не про 6-ку. Какой там год на дворе к ее выходу будет?..


                    Год будет предположительно 2015, подробнее тут wiki.php.net/rfc/php7timeline
                    6й версии не будет, об этом много написано, после 5-й будет 7-я ;)
                      +2
                      > Можете привести пример где в минорном релизе ломается совместимость?

                      Пример:

                      В функции htmlspecialchars есть параметр $encoding. Его значение по умолчанию — ISO-8859-1 для PHP до версии 5.4.0, и UTF-8 начиная с версии 5.4.0. Если строка содержит некорректные (в данной кодировке) символы, то функция возвращает пустую строку.

                      Если у вас строки имеют кодировку _не_ UTF-8 (1251, например) и вы используете эту функцию с параметром $encoding по умолчанию, то при переходе на php 5.4.0 все строки станут некорректными с точки зрения htmlspecialchars, и вывод этой функции обнулится.
                        +1
                        так в релизе написано что до 5.3 всё обратно совместимо, а после нет.
                        По этому часто есть хостинги 5.3, 5.4, 5.5
                        И пакеты в unix/linux именуются как php, php54, php55
                        Как можно не глядя закатать такое обновление?
                        Система не даст это сделать при обычном yum update

                        и вообще 5.4, 5.5 содержат половину функционала 6,0, которого не будет.
                          0
                          Суть коммента achekalin была как раз в этом — в PHP считается *нормальным* делать минорные версии несовместимыми. Хотя по правилам семантического версионирования, обновление 5.3 → 5.4 является минорным, и совместимость оно рушить не должно.
                            +1
                            Правила правилами, маркетинг маркетингом. Взгляните на java.
                            0
                            А можно подробнее, что они содержат и чего не будет в итоге в 7 версии. Можно ссылочкой ответить.
                            +3
                            И вы наверное с python ни когда не работали :)
                              0
                              Да, это самое ужасное несовместимое изменение, из-за которого желаю гореть в аду разработчикам php.
                              Масла в огонь подливает то, что на эту функцию не действует глобально установленная локаль, и единственный выход — заменять все вхождения htmlspecialchars на свою функцию, которая будет вызывать с нужной локалью. Что заставляет править не только свои проекты, но и чужие. Про закодированные вообще молчу, там жопа.
                              Ну не идиоты ли, есть ведь setlocale, зачем же всё ломать.
                                0
                                Обнимемся, коллега. Причём ведь никакой же срочной необходимости «улучшать» именно эту функцию не было. Просто у кого-то улучшалка зачесалась — и сломали совместимость на ровном месте.
                                  +1
                                  И ладно бы пофиксили в следующих релизах, добавили бы действие setlocale, так нет же, сделали говнецо без глобальных настроек на ровном месте.
                                    +1
                                    Вот я не могу понять, причем тут setlocale? Почему решение использовать директиву default_charset пришло только с релизом php5.6 это уже другой вопрос.

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

                                    Ну а закодированный код… что поделать. То есть как, варианты на самом деле имеются — пересобрать PHP с нужными параметрами, окольными путями переопределить функцию на свою… а еще лучше — не обновляться.
                                      +1
                                      Есть миллион проектов и сторонних библиотек, где htmlspecialchars никуда не выносилось, и вот так ломать обратную совместимость — это свинство. И мажорная или минорная версия тут не причём, в нормальном энтерпрайзе за это по башке бьют, но этим индусским разработчикам всё пофигу.
                                      default_charset или setlocale — это неважно, главное что никаким образом нельзя сменить кодировку по умолчанию для htmlspecialchars.
                                        0
                                        Да, конечно, и именно по той причине что миллионы проектов используют htmlspecialchars не указывая явно кодировку, но при этом данных у них хранятся в UTF-8.

                                        А вот то что никто так и не добавил никаакой информации по этому поводу в документации (кроме комментариев и багов) и вообще как-то этот вопрос опустился это уже странно. Значит не так много людей пострадало.

                                        Ну и да, замечу что с тех пор релиз менеджеры PHP стали чуть серьезнее относиться к подобным вещам.
                                          +1
                                          Если бы стали серьёзнее, то как я писал выше добавили бы учёт глобального setlocale. Ведь без него даже регистронезависимые функции не работают, и он уже правильно выставлен у многих.
                                            –1
                                            Да что вы пристали со своим setLocale, сделали через default_charset и славно, пускай и только в версии 5.6. И да, у многих что локаль что кодировка по умолчанию стоит дефолтная.
                                              +1
                                              Как я сказал выше — да, у многих, без этого не работают никакие строковые функции типа str_ireplace.
                              +1
                              На php5.5 полутора-двухлетней давности сайт на laravel3 валится с ошибкой «Cannot redeclare function yield()», например.
                              +5
                              А где Вы нашли хостинг, который сам, от балды, накатывает обновления? Я видел только 2 типа: либо как стоял 5.2, так и стоит по сей день, либо в панели управления есть возможность выбора желаемой версии и доступ к некоторым параметрам php.ini.

                              Я бы Вам советовал бежать с таких хостингов, причём как можно скорее
                                +3
                                Там не просто hashtable оптимизировали, там еще поменялся формат zval, структуры, которая хранит все значения в PHP, а так же существенно изменили и оптимизировали внутреннюю архитектуру рантайма.
                                Все это в комплексе и дает новую мажорную версию.

                                Подробнее про phpng:
                                habrahabr.ru/post/222219/
                                habrahabr.ru/company/badoo/blog/238153/
                                wiki.php.net/phpng
                                  0
                                  Да, действительно, поменялись многие внутренние структуры данных, просто описание всех изменений несколько обширно для одной статьи. Косвенно о новом формате zval упомянуто в статье.
                                  0
                                  совершенно внутренняя вещь, такая, как конкретная реализация работы с hashtable

                                  А как же бинарная совместимость (собственноручно написанных) нативных расширений? Подобные проблемы вылезут в виде segmentation fault черте где, а не в виде сообщения интерпретатора о синтаксической ошибке, так что не бекпортировать такие вещи вполне разумное решение.
                                    0
                                    Ну то есть остальные внутренние несовместимости побоку с 5.5 на 5.6? Т.е. эта оптимизация заслуживает bump мажорной версии, а многое другое — нет?

                                    По мне так не ломали бы в пределах минорных версиях совместимость, а делали бы только баги и проблемы безопасности, а уж революции бы творили в мажоных.
                                      +1
                                      Что значит не ломайте обратную совместимость?
                                      PHP ломает её в крайне малом количестве случаев. Плюс любые версии в пределах одной обратную совместимость не ломают.

                                      И не стоит рассматривать изменение второй цифры в версии как минорное обновление. У PHP это мажорные релизы. Минорные обновления у PHP это изменение изменение третьей цифры в версии. Изменение первой цифры версии при следующем релизе, обусловлено тем что в нём будет очень много важных изменений.
                                        +1
                                        Если упростить, то первая цифра это маркетинговая штука. То есть просто что бы обозначить поколение языка. Скажем после выхода PHP7 говорить что мол «Я пишу на версии PHP5» уже не так круто. Вот и все. А уже вторая цифра обозначает номер релиза. В пределах же минорных релизов обратная совместимость сохраняется почти всегда.
                                  +4
                                  Для справки на примере из статьи (всё для 64-бит):
                                  PHP 5.6 — 13.97 MiB
                                  PHP 7.0 — 4.00 MiB
                                  HHVM 3.5.0-dev — 2.00 MiB
                                    0
                                    Доклад Стогова на DevConf 2014 — www.youtube.com/watch?v=RnmyT7SLbVU — стоит учесть, что предполагаемая оптимизация не будет работать с динамическими паттернами типа «Фабрика». Но оно того стоит.
                                      0
                                      Изучаю как устроен PHP и вот для наглядности приведу пример, как увеличивается в 2 раза «Корзина».

                                      static HashTable *sort(HashTable *ht)
                                      {
                                         HashTable *result;
                                      
                                         zval *value;
                                      
                                         php_printf("size hashtable: %d used %d ", ht->nTableSize, ht->nNumUsed);
                                         php_printf("count: %d ", ht->nNumOfElements);
                                      ....
                                      }
                                      


                                      php > ms_sort([]);
                                      size hashtable: 8 used 0 count: 0
                                      php > ms_sort([0]);
                                      size hashtable: 8 used 1 count: 1 item: 0.000000
                                      php > ms_sort([0,1]);
                                      size hashtable: 8 used 2 count: 2 item: 0.000000 item: 1.000000
                                      php > ms_sort([0,1,2]);
                                      size hashtable: 8 used 3 count: 3 item: 0.000000 item: 1.000000 item: 2.000000
                                      php > ms_sort([0,1,2,3]);
                                      size hashtable: 8 used 4 count: 4 item: 0.000000 item: 1.000000 item: 2.000000 item: 3.000000
                                      php > ms_sort([0,1,2,3,4]);
                                      size hashtable: 8 used 5 count: 5 item: 0.000000 item: 1.000000 item: 2.000000 item: 3.000000 item: 4.000000
                                      php > ms_sort([0,1,2,3,4,5]);
                                      size hashtable: 8 used 6 count: 6 item: 0.000000 item: 1.000000 item: 2.000000 item: 3.000000 item: 4.000000 item: 5.000000
                                      php > ms_sort([0,1,2,3,4,5,6]);
                                      size hashtable: 8 used 7 count: 7 item: 0.000000 item: 1.000000 item: 2.000000 item: 3.000000 item: 4.000000 item: 5.000000 item: 6.000000
                                      php > ms_sort([0,1,2,3,4,5,6,7]);
                                      size hashtable: 8 used 8 count: 8 item: 0.000000 item: 1.000000 item: 2.000000 item: 3.000000 item: 4.000000 item: 5.000000 item: 6.000000 item: 7.000000
                                      php > ms_sort([0,1,2,3,4,5,6,7,8]);
                                      size hashtable: 16 used 9 count: 9 item: 0.000000 item: 1.000000 item: 2.000000 item: 3.000000 item: 4.000000 item: 5.000000 item: 6.000000 item: 7.000000 item: 8.000000
                                      
                                      


                                      Only users with full accounts can post comments. Log in, please.