Главная » Хабрахабр » [Из песочницы] PHP, GDB и массивы

[Из песочницы] PHP, GDB и массивы

Зачем простому PHP разработчику может понадобится дебаг исходников? Ну например если он заметил какое-то не очевидное поведение и хочет разобраться в нем на максимально “низком” уровне. О таком интересном для меня поведение, а так же процессе “дебага сурсов” я и хотел бы поговорить.

Мотивация

Начнем с места в карьер, вот два похожих куска кода:

Раз

$arr = []; for ($i =0; $i < 300; $i++)
{ $arr[rand(1,1000)] = 1;
} $sum = 0;
for ($i = 1001; $i < 200000000; $i++)
}

Два

$arr = []; for ($i =0; $i < 300; $i++)
{ $arr[rand(1,1000)] = 1;
} sort($arr); $sum = 0;
for ($i = 1001; $i < 200000000; $i++){ if (array_key_exists($i, $arr)){ $sum++; }
}

Разница между ними в том, что во втором случае мы отсортировали массив $arr, тем самым обновили ключи 0..count($arr)-1. А заинтересовал меня тот момент, что первый скрипт выполняется 6.0 секунд, тогда как второй 4.7 секунды. Получается около 20 процентов разницы.

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

Настраиваем окружение

Нам понадобится:

  • исходники php.
  • дебагер gdb.
  • сам php скомпилированный в debug моде.

Клонируем к себе исходники php:

git clone https://git.php.net/repository/php-src.git
cd php-src
git checkout -b PHP-7.1 origin/PHP-7.1

Если у вас нет необходимых для компиляции библиотек, ставим и их:

sudo apt-get install build-essential autoconf automake libtool

И компилируем:

./buildconf
./configure --disable-all --enable-debug –prefix=$HOME/dev/bin/php
make && make install

Начинаем поиски

Для начала определимся с отправной точкой нашего расследования, это будет функция array_key_exists. Найти ее в исходниках труда не составит если знать как в PHP объявляются функции – это макрос PHP_FUNCTION(%function_name%). Делаем поиск по исходникам строки PHP_FUNCTION(array_key_exists) и находим нашу функцию в extension/standart/array.c. Код:

PHP_FUNCTION(array_key_exists)

/* {{{ proto bool array_key_exists(mixed key, array search) Checks if the given key or index exists in the array */
PHP_FUNCTION(array_key_exists)
{ zval *key; /* key to check for */ HashTable *array; /* array to check in */ ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_ZVAL(key) Z_PARAM_ARRAY_OR_OBJECT_HT(array) ZEND_PARSE_PARAMETERS_END(); switch (Z_TYPE_P(key)) { case IS_STRING: if (zend_symtable_exists_ind(array, Z_STR_P(key))) { RETURN_TRUE; } RETURN_FALSE; case IS_LONG: if (zend_hash_index_exists(array, Z_LVAL_P(key))) { RETURN_TRUE; } RETURN_FALSE; case IS_NULL: if (zend_hash_exists_ind(array, ZSTR_EMPTY_ALLOC())) { RETURN_TRUE; } RETURN_FALSE; default: php_error_docref(NULL, E_WARNING, "The first argument should be either a string or an integer"); RETURN_FALSE; }
}

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

Найти мы ее можем в Zend/zend_hash.c:

ZEND_API zend_bool ZEND_FASTCALL zend_hash_index_exists(const HashTable *ht, zend_ulong h)
{ Bucket *p; IS_CONSISTENT(ht); if (ht->u.flags & HASH_FLAG_PACKED) { if (h < ht->nNumUsed) { if (Z_TYPE(ht->arData[h].val) != IS_UNDEF) { return 1; } } return 0; } p = zend_hash_index_find_bucket(ht, h); return p ? 1 : 0;
}

Дебажим

Запускаем дебагер:

gdb --args /home/your_pc/dev/bin/php/bin/php /home/your_pc/array_w_sort_test.php

1й параметр – путь к скомпилированному php, 2й — путь к нашему тесту в котором используем сортировку.

После того как мы попали в дебагер стоит выполнить такую команду:

(gdb) source ~/dev/c/php-src/.gdbinit

Это даст нам несколько очень полезных команд, посмотреть список можно написав:

(gdb) help user-defined

Ставим breakpoint на функции zend_hash_index_exists:

(gdb) break zend_hash_index_exists

И запускаем скрипт:

(gdb) run

Видим на экране что-то вроде:

(gdb) run
Starting program: /home/your_pc/dev/bin/php/bin/php /home/your_pc/array_w_sort_test.php Breakpoint 1, zend_hash_index_exists (ht=0x7ffff7059780, h=1001) at /home/your_pc/dev/c/php-src/Zend/zend_hash.c:2030
2030 IS_CONSISTENT(ht);

Отлично, мы внутри потока выполнения!

Чтобы посмотреть где мы находимся в PHP коде, выполним команду:

(gdb) zbacktrace

Результат говорит нам, что точка входа была определена правильно:

[0x7ffff7015240] array_key_exists(1001, array(251)[0x7ffff70152a0]) [internal function] [0x7ffff7015030] (main) /home/your_pc/array_w_sort_test.php:14

Для перемещения по исходному коду используем команду:

(gdb) next

или

(gdb) step

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

Выполним команду для просмотра параметров с которыми вызвана функция:

(gdb) info args
ht = 0x7ffff7059780
h = 1001

ht — это указатель на хэш таблицу, h — искомый ключ.

И так, добираемся до строки:

if (ht->u.flags & HASH_FLAG_PACKED) {

Делаем next – условие выполняется! Отлично, проделаем те же шаги для теста без сортировки – условие не выполняется! Ну что, мы нашли ту “вилку”, которая и делает такую разницу в скорости. Разберемся подробнее.

Немного о структуре хэш таблиц PHP.

Выполним команду ptype чтобы посмотреть что из себя представляет хэш таблица ht

(gdb) ptype ht type = const struct _zend_array { zend_refcounted_h gc; union { struct {...} v; uint32_t flags; } u; uint32_t nTableMask; Bucket *arData; uint32_t nNumUsed; uint32_t nNumOfElements; uint32_t nTableSize; uint32_t nInternalPointer; zend_long nNextFreeElement; dtor_func_t pDestructor;
} *

Здесь нас интересует:

  • arData – С массив, содержащий бакеты. Бакет – структура C, содержащие zval – хранимый элемент, ключ в массиве php, а так же хэш этого ключа в виде числа.
  • NnumUsed – максимальный занятый индекс в массиве arData + 1.
  • u.flags — без знаковый int, каждый бит которого соответствует какому-либо флагу.
  • Кроме того, внимательно взглянув на структуру, вы спросите как же хэш ключа отображается на массив arData? Для этого разработчики php используют translation table. Она хранится “позади” массива arData (arData[-1], arData[-2] и тд) и представляет собой отображение хэшей в индексы массива arData.
  • И наконец, надо знать что к целочисленным ключам операция хэширования не применяется, и они транслируются в хэш ключи таблицы “как есть”.

Вернемся к коду

ht→u.flags набор флагов свернутый в число. Для флага HASH_FLAG_PACKED нас интересует 3й бит.

Чтобы получить значение флагов выполним команду:

(gdb) print ht->u.flags $1 = 30

Так и есть, в 3-м бите единица.

Так как условие верно мы попадаем сюда:

if (h < ht->nNumUsed) { if (Z_TYPE(ht->arData[h].val) != IS_UNDEF) { return 1; } } return 0;

В строке 1, проверяем является ли значение искомого ключа валидным по отношению к массиву arData. Если да, нам остается только проверить содержится ли в массиве arData по адресу h элемент.

Ну а если флаг HASH_FLAG_PACKED не выставлен, то мы попадем сюда

p = zend_hash_index_find_bucket(ht, h);

Эта функция будет искать требуемый элемент, но уже с использованием упомянутой выше translation table.

Дебагер можно закрывать. Ну вот и вся магия. А что же это собственно за флаг HASH_FLAG_PACKED и где он выставляется? Остался только 1 вопрос.

Относительно тестового примера догадаться не сложно, выставляется этот флаг внутри функции sort. Данный флаг сигнализирует нам о Packed hashtable optimization – все ключи php массива – числа и расположены в сортировке по возрастанию. Тут наверное ни одну еще статью можно написать. Как оказалось такая оптимизация влияет на довольно большое количество аспектов работы с языком.

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


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

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

Кто такой тимлид в Яндексе? Чем хороший отличается от плохого и стоит ли приглашать на эту должность человека со стороны — в нашем интервью с Алексеем Шаргаевым, занимающим одну из руководящих должностей в поисковых службах Яндекса. — Чем ты занимаешься ...

Job system и поиск пути

Карта В предыдущей статье я разобрал, что из себя представляет новая система задач Job system, как она работает, как создавать задачи, наполнять их данными и выполнять многопоточные вычисления и лишь в двух словах объяснил где можно использовать эту систему. В ...