PHP内核探索之变量(4)- 数组操作
上一節(jié)(PHP內(nèi)核探索之變量(3)- hash table),我們已經(jīng)知道,數(shù)組在PHP的底層實(shí)際上是HashTable(鏈接法解決沖突),本文將對(duì)最常用的函數(shù)系列-數(shù)組操作的相關(guān)函數(shù)做進(jìn)一步的跟蹤。
本文主要內(nèi)容:
一、PHP中提供的數(shù)組操作函數(shù)
可以說,數(shù)組是PHP中使用最廣泛的數(shù)據(jù)結(jié)構(gòu)之一,正因如此,PHP為開發(fā)者提供了豐富的數(shù)組操作函數(shù)(參見http://cn2.php.net/manual/en/ref.array.php ), 大約有80個(gè),這對(duì)于絕大多數(shù)的數(shù)組操作而言,已經(jīng)足夠了。如果按照數(shù)組操作的類別來分,這些函數(shù)大致可以分為如下幾類(不完全分類):
PHP中,數(shù)組相關(guān)的操作有如下特點(diǎn):
接下來,我們以幾個(gè)具體的函數(shù)為例,深入探索PHP中數(shù)組函數(shù)的實(shí)現(xiàn)。
二、數(shù)組操作的實(shí)現(xiàn)
由于數(shù)組的操作實(shí)際上是對(duì)HashTable的相關(guān)操作,因而,我們?cè)俅钨N出HashTable的結(jié)構(gòu)和結(jié)構(gòu)圖,以便參考。
HashTable的結(jié)構(gòu):
typedef struct _hashtable {uint nTableSize;uint nTableMask;uint nNumOfElements;ulong nNextFreeElement;Bucket *pInternalPointer; /* Used for element traversal */Bucket *pListHead;Bucket *pListTail;Bucket **arBuckets;dtor_func_t pDestructor;zend_bool persistent;unsigned char nApplyCount;zend_bool bApplyProtection; #if ZEND_DEBUG int inconsistent; #endif } HashTable;對(duì)應(yīng)的結(jié)構(gòu)圖:
?
接下來,我們以幾個(gè)數(shù)組操作函數(shù)為例,來查看具體的操作實(shí)現(xiàn)。
1. 數(shù)組定義和初始化
在高級(jí)語言中,一條簡(jiǎn)單的語句往往需要在底層中經(jīng)過很多的操作步驟才能實(shí)現(xiàn),對(duì)于數(shù)組的操作亦是如此,例如:$arr = array(1, 2, 3);這樣的賦值語句,實(shí)際上會(huì)經(jīng)歷數(shù)組初始化(array_init)、添加數(shù)組元素(ADD_ARRAY_ELEMENT)、賦值這些步驟才會(huì)實(shí)現(xiàn)。 (1)數(shù)組的初始化這是通過array_init來實(shí)現(xiàn)的,實(shí)際上是調(diào)用_array_init來完成數(shù)組的初始化:
ZEND_API int _array_init(zval *arg, uint size ZEND_FILE_LINE_DC) {ALLOC_HASHTABLE_REL(Z_ARRVAL_P(arg));_zend_hash_init(Z_ARRVAL_P(arg), size, NULL, ZVAL_PTR_DTOR, 0 ZEND_FILE_LINE_RELAY_CC);Z_TYPE_P(arg) = IS_ARRAY;return SUCCESS; }
其中zval *arg即為我們要初始化的數(shù)組,第一句ALLOC_HASHTABLE_REL(Z_ARRVAL_P(arg));宏展開后,實(shí)際上是:
(*arg).value.ht = (HashTable *) emalloc_rel(sizeof(HashTable));之后則通過_zend_hash_init函數(shù)實(shí)現(xiàn)初始化HashTable,并把a(bǔ)rg的zval類型設(shè)置為IS_ARRAY:
Z_TYPE_P(arg) = IS_ARRAY;(2)? zend_hash_init 上一節(jié)已經(jīng)介紹過,這里不再贅述
2. 數(shù)組遍歷 prev, next和current
在PHP中,我們可以使用prev, next,current等完成對(duì)數(shù)組的訪問,例如:
$traverse = array('one', 'after', 'another');$cur = current($traverse); echo "cur:", $cur.PHP_EOL;$next = next($traverse); echo "next: ", $next.PHP_EOL;$nextnext = next($traverse); echo "nextnext: ", $nextnext.PHP_EOL;$prev = prev($traverse); echo "prev: ", $prev.PHP_EOL;我們知道,HashTable結(jié)構(gòu)體中,有一個(gè)成員pInternalPointer, 這個(gè)成員便是控制數(shù)組的訪問指針的。以prev函數(shù)為例,對(duì)HashTable的遍歷實(shí)現(xiàn)如下:
(1)將訪問指針移動(dòng)一步
這是通過zend_hash_move_backwards(array);來實(shí)現(xiàn)的,具體來說,先找到數(shù)組的當(dāng)前位置或指針:
HashPosition *current = pos ? pos : &ht->pInternalPointer然后訪問這個(gè)指針的pListLast找到上一個(gè)元素:
*current = (*current)->pListLast;移動(dòng)指針的過程如下(可以看出,在不傳遞pos參數(shù)時(shí),實(shí)際上移動(dòng)的是ht-> pInternalPointer這個(gè)指針):
ZEND_API int zend_hash_move_backwards_ex(HashTable *ht, HashPosition *pos) { HashPosition *current = pos ? pos : &ht->pInternalPointer;IS_CONSISTENT(ht); if (*current) {*current = (*current)->pListLast;return SUCCESS;} elsereturn FAILURE; }(2)如果需要返回值,由于訪問指針已經(jīng)移動(dòng)到了適當(dāng)?shù)奈恢?#xff0c;則直接獲取當(dāng)前指針指向的元素:
if (return_value_used) {if (zend_hash_get_current_data(array, (void **) &entry) == FAILURE) {RETURN_FALSE;}RETURN_ZVAL(*entry, 1, 0); }獲取當(dāng)前指針指向的元素是通過zend_hash_get_current_data來實(shí)現(xiàn)的:
#define zend_hash_get_current_data(ht, pData) \zend_hash_get_current_data_ex(ht, pData, NULL)ZEND_API int zend_hash_get_current_data_ex(HashTable *ht, void **pData, HashPosition *pos) { Bucket *p;/* 獲取當(dāng)前指針 */ p = pos ? (*pos) : ht->pInternalPointer;IS_CONSISTENT(ht);if (p) {*pData = p->pData;return SUCCESS;} else {return FAILURE;} }知道了prev函數(shù)的原理,我們不難想象next, current, reset等函數(shù)的實(shí)現(xiàn)機(jī)制。
prev函數(shù)的源碼:
PHP_FUNCTION(prev) {HashTable *array;zval **entry;if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "H", &array) == FAILURE) {return;}zend_hash_move_backwards(array);if (return_value_used) {if (zend_hash_get_current_data(array, (void **) &entry) == FAILURE) {RETURN_FALSE;}RETURN_ZVAL(*entry, 1, 0);} }3. 數(shù)組排序 asort,arsort,ksort等
php中提供了大量的函數(shù)用于數(shù)組的排序,如用于普通排序的sort函數(shù),用于逆序排序的rsort函數(shù),用于按照鍵名排序的函數(shù)ksort和krsort, 用于自定義比較函數(shù)的usort和uksort等,可以說非常豐富。我們以sort函數(shù)的實(shí)現(xiàn)為例,探索PHP中排序算法的實(shí)現(xiàn)。
sort函數(shù)的簽名為:
bool sort ( array &$array [, int $sort_flags = SORT_REGULAR ] )其中sort_flags會(huì)影響排序的結(jié)果,該值可以是:SORT_REGULAR,SORT_NUMERIC,SORT_STRING,SORT_LOCALE_STRING,SORT_NATURAL等
( http://cn2.php.net/manual/zh/function.sort.php )
sort函數(shù)的實(shí)現(xiàn)過程如下:
(1)由于sort_flags會(huì)影響比較函數(shù)的行為,因此首先需要根據(jù)sort_type確定用于元素比較的函數(shù)(自然排序,整數(shù)排序,還是字符串排序,區(qū)分大小寫還是不區(qū)分)。這是通過php_set_compare_func來實(shí)現(xiàn)的:
static void php_set_compare_func(int sort_type TSRMLS_DC) { switch (sort_type & ~PHP_SORT_FLAG_CASE) {case PHP_SORT_NUMERIC:ARRAYG(compare_func) = numeric_compare_function;break;case PHP_SORT_STRING:ARRAYG(compare_func) = sort_type & PHP_SORT_FLAG_CASE ?string_case_compare_function : string_compare_function;break;case PHP_SORT_NATURAL:ARRAYG(compare_func) = sort_type & PHP_SORT_FLAG_CASE ?
string_natural_case_compare_function : string_natural_compa re_function;break;#if HAVE_STRCOLLcase PHP_SORT_LOCALE_STRING:ARRAYG(compare_func) = string_locale_compare_function;break; #endif case PHP_SORT_REGULAR:default:ARRAYG(compare_func) = compare_function;//默認(rèn)使用compare_functionbreak;} }
switch (sort_type & ~PHP_SORT_FLAG_CASE)這是什么意思呢?首先,PHP針對(duì)排序設(shè)置的sort_type常量有:
#define PHP_SORT_REGULAR 0 #define PHP_SORT_NUMERIC 1 #define PHP_SORT_STRING 2 #define PHP_SORT_DESC 3 #define PHP_SORT_ASC 4 #define PHP_SORT_LOCALE_STRING 5 #define PHP_SORT_NATURAL 6 #define PHP_SORT_FLAG_CASE 8其次,sort函數(shù)的第二個(gè)參數(shù)可以設(shè)置為SORT_NATURAL | SORT_FLAG_CASE或者SORT_STRING | SORT_FLAG_CASE. 因此sort_type & ~PHP_SORT_FLAG_CASE的含義為:排除PHP_SORT_FLAG_CASE標(biāo)志之后的值,得到的值可以是PHP_SORT_NUMERIC,PHP_SORT_STRING,PHP_SORT_NATURAL,PHP_SORT_LOCALE_STRING,PHP_SORT_REGULAR。而在PHP_SORT_STRING和PHP_SORT_NATURAL中,還需要通過sort_type & PHP_SORT_FLAG_CASE來判斷是否是不區(qū)分大小寫的排序(即是否使用了SORT_FLAG_CASE標(biāo)志)。
(2) 設(shè)置完sort_type之后,調(diào)用zend_hash_sort完成實(shí)際的排序:
zend_hash_sort(Z_ARRVAL_P(array), zend_qsort, php_array_data_compare, 1 TSRMLS_CC);zend_hash_sort的函數(shù)簽名是:
ZEND_API int zend_hash_sort(HashTable *ht, sort_func_t sort_func, compare_func_t compar, int renumber TSRMLS_DC);其中:
我們首先跟蹤zend_hash_sort的基本過程,而后再追蹤zend_qsort的具體實(shí)現(xiàn)。
由于數(shù)組排序并不會(huì)改變數(shù)組中的元素,而只是改變了數(shù)組中元素的位置,因而,對(duì)底層而言,實(shí)際上只是對(duì)全局的雙鏈表進(jìn)行排序,這顯然需要n個(gè)額外的空間(n是數(shù)組元素個(gè)數(shù)):
arTmp = (Bucket **) pemalloc(ht->nNumOfElements * sizeof(Bucket *), ht->persistent);然后遍歷雙鏈表,將雙鏈表的每個(gè)節(jié)點(diǎn)存儲(chǔ)到臨時(shí)空間(c數(shù)組,每個(gè)元素是個(gè)bucket *)中:
p = ht->pListHead; i = 0; while (p) {arTmp[i] = p;p = p->pListNext;i++; }現(xiàn)在,可以調(diào)用排序函數(shù)對(duì)數(shù)組進(jìn)行排序了:
(*sort_func)((void *) arTmp, i, sizeof(Bucket *), compar TSRMLS_CC);實(shí)際上是:
zend_qsort((void *) arTmp, i, sizeof(Bucket *), compar TSRMLS_CC);
排序之后,雙鏈表中節(jié)點(diǎn)的位置發(fā)生了變化,因而需要調(diào)整指針的指向。首先調(diào)整pListHead,并設(shè)置pListTail為NULL:
ht->pListHead = arTmp[0]; ht->pListTail = NULL;然后遍歷數(shù)組,分別設(shè)置每一個(gè)節(jié)點(diǎn)的pListLast和pListNext:
arTmp[0]->pListLast = NULL; if (i > 1) {arTmp[0]->pListNext = arTmp[1];for (j = 1; j < i-1; j++) {arTmp[j]->pListLast = arTmp[j-1];arTmp[j]->pListNext = arTmp[j+1];}arTmp[j]->pListLast = arTmp[j-1];arTmp[j]->pListNext = NULL; } else {arTmp[0]->pListNext = NULL; }最后設(shè)置HashTable的pListTail:
ht->pListTail = arTmp[i-1];排序過程如下所示:
?
排序之后,調(diào)整指針走向之后的HashTable:
?
現(xiàn)在,已經(jīng)知道zend_hash_sort的基本過程了,我們接著跟蹤一下zend_qsort的實(shí)現(xiàn)(函數(shù)位于Zend/zend_qsort.c),該函數(shù)的簽名為:
ZEND_API void zend_qsort(void *base, size_t nmemb, size_t siz, compare_func_t compare TSRMLS_DC);這實(shí)際上是Zend實(shí)現(xiàn)的快速排序算法,主要包括兩個(gè)部分:
1. _zend_qsort_swap(void *a, void *b, size_t siz) 用于交換任意類型的兩個(gè)值,與我們經(jīng)常使用的swap(int *a ,int *b), 或者swap(char *a, char *b), _zend_qsort_swap有更好的通用性,因而它的實(shí)現(xiàn)也略微復(fù)雜, 具體交換過程為:
(1) . 以sizeof(int)為步長(zhǎng), 交換指針指向的值:
for (i = sizeof(int); i <= siz; i += sizeof(int)) {t_i = *tmp_a_int;*tmp_a_int++ = *tmp_b_int;*tmp_b_int++ = t_i; }這個(gè)循環(huán)執(zhí)行完畢后,有兩種可能的情況:一種是siz剛好是sizeof(int)的整倍數(shù),那么交換就已經(jīng)完成了,因?yàn)橹羔榓和指針b指向的內(nèi)存空間的值已經(jīng)完全得到了交換。另一種情況是, siz并不是sizeof(int)的整倍數(shù),那么實(shí)際上上述交換步驟多交換了一些字節(jié)的值(例如對(duì)于sizeof(int)=4的情況,可能多交換了1,2,3個(gè)字節(jié)的內(nèi)存的值),那么對(duì)于這多交換出來的一部分,還需要交換回去。怎么做呢?
(2). 使用char指針一個(gè)一個(gè)字節(jié)的交換:
tmp_a_char = (char *) tmp_a_int; tmp_b_char = (char *) tmp_b_int;for (i = i - sizeof(int) + 1; i <= siz; ++i) {//i控制交換次數(shù)t_c = *tmp_a_char;*tmp_a_char++ = *tmp_b_char;*tmp_b_char++ = t_c; }這樣就完成了交換。
2. zend_qsort(void *base, size_t nmemb, size_t siz, compare_func_t compare TSRMLS_DC). 快速排序算法,與常見的快速排序算法不同,這是非遞歸版本的快速排序。算法的基本思想是:使用QSORT_STACK_SIZE大小的棧(實(shí)際上是數(shù)組,不過每次都取數(shù)組的末尾元素,當(dāng)做棧使用)存儲(chǔ)快排的開始索引和結(jié)束索引(指針),從而將遞歸的快排過程轉(zhuǎn)換為非遞歸的。
綜上,我們可以得出PHP排序函數(shù)的一般特點(diǎn):
a. 需要額外的空間,空間復(fù)雜度是O(n), 因而應(yīng)該盡量避免對(duì)很大的數(shù)組排序.
b. 底層使用快速排序,平均時(shí)間復(fù)雜度是O(n*lgn)
zend_qsort的 實(shí)現(xiàn)代碼(有興趣的童鞋可以研究一下實(shí)現(xiàn)細(xì)節(jié)):
ZEND_API void zend_qsort(void *base, size_t nmemb, size_t siz, compare_func_t compare TSRMLS_DC) {/* 存儲(chǔ)開始和結(jié)束指針的棧 */void *begin_stack[QSORT_STACK_SIZE];void *end_stack[QSORT_STACK_SIZE];register char *begin;register char *end;register char *seg1;register char *seg2;/* partition index */register char *seg2p;register int loop;/* pivot index */uint offset;begin_stack[0] = (char *) base;end_stack[0] = (char *) base + ((nmemb - 1) * siz);for (loop = 0; loop >= 0; --loop) {begin = begin_stack[loop];end = end_stack[loop];/* partition的過程 */while (begin < end) {offset = (end - begin) >> 1;_zend_qsort_swap(begin, begin + (offset - (offset % siz)), siz);seg1 = begin + siz;seg2 = end;while (1) {/* 從左向右找 */for (; seg1 < seg2 && compare(begin, seg1 TSRMLS_CC) > 0;seg1 += siz);/* 從右向左找 */for (; seg2 >= seg1 && compare(seg2, begin TSRMLS_CC) > 0;seg2 -= siz);if (seg1 >= seg2)break;/* 交換seg1和seg2指向的值 */_zend_qsort_swap(seg1, seg2, siz);/* 指針移動(dòng),每次都是siz步長(zhǎng) */seg1 += siz;seg2 -= siz;}_zend_qsort_swap(begin, seg2, siz);seg2p = seg2;/* 右半部分 */if ((seg2p - begin) <= (end - seg2p)) {if ((seg2p + siz) < end) {begin_stack[loop] = seg2p + siz;end_stack[loop++] = end;}end = seg2p - siz;}else { /* 左半部分 */if ((seg2p - siz) > begin) {begin_stack[loop] = begin;end_stack[loop++] = seg2p - siz;}begin = seg2p + siz;}}} }4.? 數(shù)組合并 array_merge
array_merge用于合并兩個(gè)或者多個(gè)數(shù)組(實(shí)際上,array_merge可以僅傳入一個(gè)數(shù)組參數(shù)如array_merge($a)? )例如:
$a = array('index' => "a",1 =>'a'); $b = array('index' => "b",1 =>'b'); print_r(array_merge($a, $b));結(jié)果是:
Array ([index] => b[0] => a[1] => b )那么,對(duì)于array_merge, PHP底層是如何處理字符串索引和數(shù)字索引的呢?
PHP_FUNCTION(array_merge) {php_array_merge_or_replace_wrapper(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0, 0); }因此,實(shí)際上是通過php_array_merge_or_replace_wrapper來完成的,繼續(xù)查看php_array_merge_or_replace_wrapper的實(shí)現(xiàn):
static void php_array_merge_or_replace_wrapper(INTERNAL_FUNCTION_PARAMETERS, int recursive, int replace);注意傳入的參數(shù),recursive=0, replace=0 ( 不遞歸merge,數(shù)字索引不替換 ) ,而INTERNAL_FUNCTION_PARAMETERS是:
#define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DCarray_merge的基本過程是:
(1)???? 確定初始化數(shù)組的大小(使用元素最多的數(shù)組的大小作為結(jié)果數(shù)組的初始大小),初始化數(shù)組:
for (i = 0; i < argc; i++) {/* 不是數(shù)組 */if (Z_TYPE_PP(args[i]) != IS_ARRAY) {php_error_docref(NULL TSRMLS_CC, E_WARNING, "Argument #%d is not an array", i + 1);efree(args);RETURN_NULL();} else {int num = zend_hash_num_elements(Z_ARRVAL_PP(args[i])); /* 使用元素最多的數(shù)組的大小作為init_size的大小 */if (num > init_size) {init_size = num;}} }array_init_size(return_value, init_size);return_value是個(gè)zval *, 它指向返回值的zval
(2)???? 對(duì)array_merge參數(shù)中的每個(gè)數(shù)組,依次執(zhí)行php_array_merge(由于replace=0和recursive=0), 我們只看第一個(gè)分支:
for (i = 0; i < argc; i++) { SEPARATE_ZVAL(args[i]);if (!replace) {php_array_merge(Z_ARRVAL_P(return_value), Z_ARRVAL_PP(args[i]), recursive TSRMLS_CC);} }SEPARATE_ZVAL用于創(chuàng)建一個(gè)與原始數(shù)據(jù)相同的zval,避免在操作的過程中修改參數(shù)的值(參數(shù)是非引用傳遞的情況下)。而真正的merge過程是通過php_array_merge來實(shí)現(xiàn)的。
(3) ??? merge的過程
由于PHP數(shù)組中包含字符串索引和數(shù)字索引,對(duì)于這兩類不同的索引,merge的處理是不同的(replace=0, recursive=0,只看對(duì)應(yīng)的分支):
switch (zend_hash_get_current_key_ex(src, &string_key, &string_key_len, &num_key, 0, &pos)){case HASH_KEY_IS_STRING:Z_ADDREF_PP(src_entry);zend_hash_update(dest, string_key, string_key_len, src_entry, sizeof(zval *), NULL);break;case HASH_KEY_IS_LONG:Z_ADDREF_PP(src_entry);zend_hash_next_index_insert(dest, src_entry, sizeof(zval *), NULL);break; }上述代碼表明:對(duì)于字符串索引,PHP在執(zhí)行array_merge的時(shí)候,會(huì)更新字符串索引的值,其結(jié)果就是參數(shù)靠后數(shù)組的值會(huì)覆蓋靠前的數(shù)組的值。而對(duì)于數(shù)字型索引,PHP執(zhí)行的zend_hash_next_index_insert操作,也就是插入一個(gè)新的元素,這同時(shí)也更改了鍵(例如原來的key=2, array_merge之后,可能變成了0)。這也解釋了最開始array_merge腳本的輸出:
$a = array('index' => "a",1 =>'a'); $b = array('index' => "b",1 =>'b'); print_r(array_merge($a, $b));更多的數(shù)組操作函數(shù)我們不再一一介紹,只要知道了HashTable的結(jié)構(gòu),要理解這些實(shí)現(xiàn),并不困難。
由于寫作匆忙,本文難免會(huì)有錯(cuò)誤之處,敬請(qǐng)批評(píng)指正。
ps: 近期正在補(bǔ)習(xí)C語言/操作系統(tǒng)的相關(guān)基礎(chǔ),尤其是指針/內(nèi)存管理這一塊,有一起的同學(xué),歡迎交流。
?? 三、參考文獻(xiàn)
總結(jié)
以上是生活随笔為你收集整理的PHP内核探索之变量(4)- 数组操作的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 压力不是来自于任务本身,而是任务在大脑中
- 下一篇: [PHP]php发布和调用Webserv