Допустим у нас есть массив с целочисленными индексами
$arr = array( $val1, $val2, ..., $valn );
Этот массив можно перебрать в цикле двумя способами
foreach($arr as $k => $v ) {...}
и
$n = count( $arr ); for($k = 0; $k < $n; $k++ ) {...}
Кажется вполне очевидным, что второй способ должен быть, если и не быстрее, то уж точно не медленнее.
Давайте разберемся.
— Нет. Никаких бенчмарков. Только код!
На самом деле, второй способ работает медленнее! Особенно это кажется не очевидным для тех, кто имеет опыт программирования на C
и C++
, где индекс массива — это простое смещение указателя.
Я обнаружил эту особенность довольно давно, но все ни как не доходили руки посмотреть с чем это связано
Итак, почему так происходит
Давайте посмотрим на внутренне устройство массива в php.
Внутренне, в ядре Zend, массив представлен несколькими взаимосвязанными структурами:
Структура Bucket
описывает «указатель» на текущую позицию массива
typedef struct bucket { ulong h; // целочисленное значение индекса // (если индекс целочисленный) uint nKeyLength; // длина строки (строкового ключа) void *pData; // значение элемента массива // [ извлечь можно так: (zval **)pos->pData ] void *pDataPtr; struct bucket *pListNext; // Указатель на следующий элемент массива struct bucket *pListLast; // Указатель на предыдущий элемент массива struct bucket *pNext; // используется для внутренних операций с arBuckets struct bucket *pLast; // используется для внутренних операций с arBuckets const char *arKey; // строковое представление ключа, если ключ строковой } Bucket; typedef Bucket* HashPosition;
Структура HashTable
— это и есть сам массив во внутреннем представлении:
typedef struct _hashtable { uint nTableSize; uint nTableMask; uint nNumOfElements; // количество элементов в массиве ulong nNextFreeElement; Bucket *pInternalPointer; Bucket *pListHead; // Указатель на первый элемент массива Bucket *pListTail; // Указатель на последний элемент массива Bucket **arBuckets; // собственно, сама ХэшТаблица. // Используется для индексации как строковых // так и целочисленных ключей dtor_func_t pDestructor; zend_bool persistent; unsigned char nApplyCount; zend_bool bApplyProtection; } HashTable;
Даже представленной уже информации достаточно, что бы понять причину. Массивы реализованы в виде двусвязных списков.
Двусвязные списки позволяют осуществлять быструю вставку, достаточно быстрый перебор, но произвольный доступ в них — медленная операция.
Давайте посмотрим как, все-таки, Zend осуществляет итерацию элементов массива
// Переход к следующему элементу массива. int zend_hash_move_forward_ex(HashTable *ht, HashPosition *pos) { HashPosition *current = pos ? pos : &ht->pInternalPointer; if (*current) { *current = (*current)->pListNext; return SUCCESS; } else return FAILURE; } // Переход к предыдущему элементу массива. // В php нет обратной итерации, но она поддерживается Zend // и может быть использована при написании расширений. int zend_hash_move_backwards_ex(HashTable *ht, HashPosition *pos) { HashPosition *current = pos ? pos : &ht->pInternalPointer; if (*current) { *current = (*current)->pListLast; return SUCCESS; } else return FAILURE; }
Здесь, я думаю, все понятно без дополнительных объяснений — указатель на текущий элемент заменяется указателем на следующий элемент, ссылка на который хранится в текущем.
Теперь посмотрим как осуществляется доступ по индексу.
int zend_hash_index_find(const HashTable *ht, ulong h, void **pData) { uint nIndex; Bucket *p; nIndex = h & ht->nTableMask; p = ht->arBuckets[nIndex]; while (p != NULL) { if ((p->h == h) && (p->nKeyLength == 0)) { *pData = p->pData; return SUCCESS; } p = p->pNext; } return FAILURE; }
Здесь нужны некоторые пояснения.
Я не хотел бы утомлять читателей излишне подробным описанием того, как устроен массив arBuckets
(C
массив из HashPosition
— указателей на Bucket
).
Скажу только, что здесь осуществляется последовательный перебор части внутренней хэш таблицы arBuckets
до поиска нужного значения.
Выводы
foreach($arr as $i => $v ){...}
Достаточно быстро перебирает все элементы массива, получая их один за другим. Сложность этой операции O(n).
for($i = 0; $i < $n; $i++ ){...}
На каждой итерации ищет индекс во внутренней хэш таблице.
Оценочно, сложность последней операции выражалась бы суммой арифметической прогрессии n*(n+1)/2 т.е. и имела бы порядок O(n2), если бы при поиске значения по индексу перебиралась бы вся хэш таблица. На самом деле, перебираются не все значения, а только часть их, поэтому сложность будет варьироваться от O(n) до O(n2). И для больших массивов, она будет ближе к, O(n). Но даже в этом случае, перебор массива с доступом по ключу — более ресурсоемкая операция.
Что касается массивов со строковыми ключами (или смешанными — целочисленными и строковыми).
Все вышесказанное справедливо и для них с той разницей, что скорость доступа к строковому ключу в среднем в 8 (максимум в 16) раз больше чем к целочисленному
ссылка на оригинал статьи http://habrahabr.ru/post/216103/
Добавить комментарий