PHP内核探索之变量(4)- 数组操作

来源:互联网 发布:手机掌上贵金属软件 编辑:程序博客网 时间:2024/05/19 01:30

上一节(PHP内核探索之变量(3)- hash table),我们已经知道,数组在PHP的底层实际上是HashTable(链接法解决冲突),本文将对最常用的函数系列-数组操作的相关函数做进一步的跟踪。

本文主要内容:

  1. PHP中提供的数组操作函数
  2. 数组操作函数的实现
  3. 结语参考文献

一、PHP中提供的数组操作函数

可以说,数组是PHP中使用最广泛的数据结构之一,正因如此,PHP为开发者提供了丰富的数组操作函数(参见http://cn2.php.net/manual/en/ref.array.php ), 大约有80个,这对于绝大多数的数组操作而言,已经足够了。如果按照数组操作的类别来分,这些函数大致可以分为如下几类(不完全分类):

  1. 数组遍历相关函数:如prev, next, current, end,reset, each等
  2. 数组排序相关:如sort, rsort, asort, arsort, ksort, krsort, uasort, uksort
  3. 数组查找相关: 如in_array, array_search, array_key_exists等
  4. 数组分割、合并相关: array_slice, array_splice, implode, array_chunk, array_combine等
  5. 数组交并差:如array_merge, array_diff, array_diff_*, array_intersect, array_intersect_*
  6. 作为stack/queue容器的数组: 如array_push, array_pop, array_shift
  7. 其他的数组操作:array_fill, array_flip, array_sum, array_reverse等

PHP中,数组相关的操作有如下特点:

  1. 数组操作函数是通过扩展的形式(ext/standard/array.c)提供的,因此也会经历扩展的MINIT,RINIT,RSHUTDOWN,MSHUTDOWN等过程。
  2. 在底层,定义PHP函数的方式是PHP_FUNCTION(function_name),例如数组操作函数array_merge在底层是PHP_FUNCTION(array_merge)
  3. 由于数组的底层实现是HashTable,因而数组的绝大多数操作实际上都是针对HashTable的操作,这是通过HashTable API实现的。

接下来,我们以几个具体的函数为例,深入探索PHP中数组函数的实现。

二、数组操作的实现

由于数组的操作实际上是对HashTable的相关操作,因而,我们再次贴出HashTable的结构和结构图,以便参考。

HashTable的结构:

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;

对应的结构图:

 

接下来,我们以几个数组操作函数为例,来查看具体的操作实现。

1.  数组定义和初始化

在高级语言中,一条简单的语句往往需要在底层中经过很多的操作步骤才能实现,对于数组的操作亦是如此,例如:$arr = array(1, 2, 3);这样的赋值语句,实际上会经历数组初始化(array_init)、添加数组元素(ADD_ARRAY_ELEMENT)、赋值这些步骤才会实现。
(1)数组的初始化这是通过array_init来实现的,实际上是调用_array_init来完成数组的初始化:
[cpp] view plain copy print?
  1. ZEND_API int _array_init(zval *arg, uint size ZEND_FILE_LINE_DC)  
  2. {  
  3.     ALLOC_HASHTABLE_REL(Z_ARRVAL_P(arg));  
  4.       
  5.     _zend_hash_init(Z_ARRVAL_P(arg), size, NULL, ZVAL_PTR_DTOR, 0 ZEND_FILE_LINE_RELAY_CC);  
  6.     Z_TYPE_P(arg) = IS_ARRAY;  
  7.     return SUCCESS;  
  8. }  

其中zval *arg即为我们要初始化的数组,第一句ALLOC_HASHTABLE_REL(Z_ARRVAL_P(arg));宏展开后,实际上是:

[cpp] view plain copy print?
  1. (*arg).value.ht = (HashTable *) emalloc_rel(sizeof(HashTable));  

之后则通过_zend_hash_init函数实现初始化HashTable,并把arg的zval类型设置为IS_ARRAY:

1
Z_TYPE_P(arg) = IS_ARRAY;

(2)  zend_hash_init 上一节已经介绍过,这里不再赘述

2.  数组遍历 prev, next和current

在PHP中,我们可以使用prev, next,current等完成对数组的访问,例如:

[cpp] view plain copy print?
  1. $traverse = array('one''after''another');  
  2.    
  3. $cur = current($traverse);  
  4. echo "cur:", $cur.PHP_EOL;  
  5.    
  6. $next = next($traverse);  
  7. echo "next: ", $next.PHP_EOL;  
  8.    
  9. $nextnext = next($traverse);  
  10. echo "nextnext: ", $nextnext.PHP_EOL;  
  11.    
  12. $prev = prev($traverse);  
  13. echo "prev: ", $prev.PHP_EOL;  

我们知道,HashTable结构体中,有一个成员pInternalPointer, 这个成员便是控制数组的访问指针的。以prev函数为例,对HashTable的遍历实现如下:

(1)将访问指针移动一步

这是通过zend_hash_move_backwards(array);来实现的,具体来说,先找到数组的当前位置或指针:

1
HashPosition *current = pos ? pos : &ht->pInternalPointer

然后访问这个指针的pListLast找到上一个元素:

1
*current = (*current)->pListLast;

移动指针的过程如下(可以看出,在不传递pos参数时,实际上移动的是ht-> pInternalPointer这个指针):

[cpp] view plain copy print?
  1. ZEND_API int zend_hash_move_backwards_ex(HashTable *ht, HashPosition *pos)  
  2. {      
  3.     HashPosition *current = pos ? pos : &ht->pInternalPointer;  
  4.     IS_CONSISTENT(ht);   
  5.    
  6.     if (*current) {  
  7.         *current = (*current)->pListLast;  
  8.         return SUCCESS;  
  9.     } else  
  10.         return FAILURE;  
  11. }  

(2)如果需要返回值,由于访问指针已经移动到了适当的位置,则直接获取当前指针指向的元素

[cpp] view plain copy print?
  1. if (return_value_used) {  
  2.   if (zend_hash_get_current_data(array, (void **) &entry) == FAILURE) {  
  3.     RETURN_FALSE;  
  4.   }  
  5.   RETURN_ZVAL(*entry, 1, 0);  
  6. }  

获取当前指针指向的元素是通过zend_hash_get_current_data来实现的:

[cpp] view plain copy print?
  1. #define zend_hash_get_current_data(ht, pData) \  
  2.     zend_hash_get_current_data_ex(ht, pData, NULL)  
  3.    
  4. ZEND_API int zend_hash_get_current_data_ex(HashTable *ht, void **pData, HashPosition *pos)  
  5. {      
  6.     Bucket *p;  
  7.        
  8.     /* 获取当前指针 */  
  9.     p = pos ? (*pos) : ht->pInternalPointer;  
  10.     IS_CONSISTENT(ht);  
  11.    
  12.     if (p) {  
  13.         *pData = p->pData;  
  14.         return SUCCESS;  
  15.     } else {  
  16.         return FAILURE;  
  17.     }  
  18. }  

知道了prev函数的原理,我们不难想象next,current,reset等函数的实现机制。

prev函数的源码:

[cpp] view plain copy print?
  1. PHP_FUNCTION(prev)  
  2. {  
  3.     HashTable *array;  
  4.     zval **entry;  
  5.    
  6.     if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "H", &array) == FAILURE) {  
  7.         return;  
  8.     }  
  9.    
  10.     zend_hash_move_backwards(array);  
  11.    
  12.     if (return_value_used) {  
  13.         if (zend_hash_get_current_data(array, (void **) &entry) == FAILURE) {  
  14.             RETURN_FALSE;  
  15.         }  
  16.         RETURN_ZVAL(*entry, 1, 0);  
  17.     }  
  18. }  

3.  数组排序 asort,arsort,ksort等

php中提供了大量的函数用于数组的排序,如用于普通排序的sort函数,用于逆序排序的rsort函数,用于按照键名排序的函数ksortkrsort, 用于自定义比较函数的usortuksort等,可以说非常丰富。我们以sort函数的实现为例,探索PHP中排序算法的实现。

sort函数的签名为:

bool sort ( array &$array [, int $sort_flags = SORT_REGULAR ] )

其中sort_flags会影响排序的结果,该值可以是:SORT_REGULARSORT_NUMERICSORT_STRINGSORT_LOCALE_STRINGSORT_NATURAL

( http://cn2.php.net/manual/zh/function.sort.php )

sort函数的实现过程如下:

(1)由于sort_flags会影响比较函数的行为,因此首先需要根据sort_type确定用于元素比较的函数(自然排序,整数排序,还是字符串排序,区分大小写还是不区分)。这是通过php_set_compare_func来实现的:

[php] view plain copy print?
  1. static void php_set_compare_func(int sort_type TSRMLS_DC)  
  2. {       
  3.     switch (sort_type & ~PHP_SORT_FLAG_CASE) {  
  4.         case PHP_SORT_NUMERIC:  
  5.             ARRAYG(compare_func) = numeric_compare_function;  
  6.             break;  
  7.    
  8.         case PHP_SORT_STRING:  
  9.             ARRAYG(compare_func) = sort_type & PHP_SORT_FLAG_CASE ? <br>                 string_case_compare_function : string_compare_function;  
  10.             break;  
  11.    
  12.         case PHP_SORT_NATURAL:  
  13.             ARRAYG(compare_func) = sort_type & PHP_SORT_FLAG_CASE ? <br>                 string_natural_case_compare_function : string_natural_compa     re_function;  
  14.             break;  
  15.    
  16. #if HAVE_STRCOLL  
  17.         case PHP_SORT_LOCALE_STRING:  
  18.             ARRAYG(compare_func) = string_locale_compare_function;  
  19.             break;  
  20. #endif  
  21.    
  22.         case PHP_SORT_REGULAR:  
  23.         default:  
  24.             ARRAYG(compare_func) = compare_function;//默认使用compare_function  
  25.             break;  
  26.     }  
  27. }  

switch (sort_type & ~PHP_SORT_FLAG_CASE)这是什么意思呢?首先,PHP针对排序设置的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函数的第二个参数可以设置为SORT_NATURAL | SORT_FLAG_CASE或者SORT_STRING | SORT_FLAG_CASE. 因此sort_type & ~PHP_SORT_FLAG_CASE的含义为:排除PHP_SORT_FLAG_CASE标志之后的值,得到的值可以是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来判断是否是不区分大小写的排序(即是否使用了SORT_FLAG_CASE标志)。

(2) 设置完sort_type之后,调用zend_hash_sort完成实际的排序:

1
zend_hash_sort(Z_ARRVAL_P(array), zend_qsort, php_array_data_compare, 1 TSRMLS_CC);

zend_hash_sort的函数签名是:

ZEND_API int zend_hash_sort(HashTable *ht, sort_func_t sort_func, compare_func_t compar, int renumber TSRMLS_DC);

其中:

  1. HashTable * ht  指向HashTable的指针
  2. Sort_func_t sort_func  用于排序的函数,因此,实际上是调用zend_qsort来完成排序。
  3. Compare_func_t compar: 用于排序的比较函数,前一步骤已经设置。

我们首先跟踪zend_hash_sort的基本过程,而后再追踪zend_qsort的具体实现。

由于数组排序并不会改变数组中的元素,而只是改变了数组中元素的位置,因而,对底层而言,实际上只是对全局的双链表进行排序,这显然需要n个额外的空间(n是数组元素个数):

1
arTmp = (Bucket **) pemalloc(ht->nNumOfElements *sizeof(Bucket *), ht->persistent);

然后遍历双链表,将双链表的每个节点存储到临时空间(c数组,每个元素是个bucket *)中:

[cpp] view plain copy print?
  1. p = ht->pListHead;  
  2. i = 0;  
  3. while (p) {  
  4.     arTmp[i] = p;  
  5.     p = p->pListNext;  
  6.     i++;  
  7. }  

现在,可以调用排序函数对数组进行排序了:

1
(*sort_func)((void*) arTmp, i,sizeof(Bucket *), compar TSRMLS_CC);

实际上是:

zend_qsort((void *) arTmp, i,sizeof(Bucket *), compar TSRMLS_CC);

排序之后,双链表中节点的位置发生了变化,因而需要调整指针的指向。首先调整pListHead,并设置pListTail为NULL:

1
2
ht->pListHead = arTmp[0];
ht->pListTail = NULL;

然后遍历数组,分别设置每一个节点的pListLast和pListNext:

[cpp] view plain copy print?
  1. arTmp[0]->pListLast = NULL;  
  2. if (i > 1) {  
  3.     arTmp[0]->pListNext = arTmp[1];  
  4.     for (j = 1; j < i-1; j++) {  
  5.         arTmp[j]->pListLast = arTmp[j-1];  
  6.         arTmp[j]->pListNext = arTmp[j+1];  
  7.     }  
  8.     arTmp[j]->pListLast = arTmp[j-1];  
  9.     arTmp[j]->pListNext = NULL;  
  10. else {  
  11.     arTmp[0]->pListNext = NULL;  
  12. }  

最后设置HashTable的pListTail:

1
ht->pListTail = arTmp[i-1];

排序过程如下所示:

 

排序之后,调整指针走向之后的HashTable:

 

现在,已经知道zend_hash_sort的基本过程了,我们接着跟踪一下zend_qsort的实现(函数位于Zend/zend_qsort.c),该函数的签名为:

ZEND_API void zend_qsort(void *base, size_t nmemb, size_t siz, compare_func_t compare TSRMLS_DC);

这实际上是Zend实现的快速排序算法,主要包括两个部分:

1. _zend_qsort_swap(void *a, void *b, size_t siz) 用于交换任意类型的两个值,与我们经常使用的swap(int *a ,int *b), 或者swap(char *a, char *b), _zend_qsort_swap有更好的通用性,因而它的实现也略微复杂, 具体交换过程为:

(1) . 以sizeof(int)为步长, 交换指针指向的值:

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;}

这个循环执行完毕后,有两种可能的情况:一种是siz刚好是sizeof(int)的整倍数,那么交换就已经完成了,因为指针a和指针b指向的内存空间的值已经完全得到了交换。另一种情况是, siz并不是sizeof(int)的整倍数,那么实际上上述交换步骤多交换了一些字节的值(例如对于sizeof(int)=4的情况,可能多交换了1,2,3个字节的内存的值),那么对于这多交换出来的一部分,还需要交换回去。怎么做呢?

(2). 使用char指针一个一个字节的交换:

[cpp] view plain copy print?
  1. tmp_a_char = (char *) tmp_a_int;  
  2. tmp_b_char = (char *) tmp_b_int;  
  3.    
  4. for (i = i - sizeof(int) + 1; i <= siz; ++i) {//i控制交换次数  
  5.     t_c = *tmp_a_char;  
  6.     *tmp_a_char++ = *tmp_b_char;  
  7.     *tmp_b_char++ = t_c;  
  8. }  

这样就完成了交换。

2. zend_qsort(void *base,size_t nmemb,size_t siz,compare_func_t compare TSRMLS_DC). 快速排序算法,与常见的快速排序算法不同,这是非递归版本的快速排序。算法的基本思想是:使用QSORT_STACK_SIZE大小的(实际上是数组,不过每次都取数组的末尾元素,当做栈使用)存储快排的开始索引和结束索引(指针),从而将递归的快排过程转换为非递归的。

综上,我们可以得出PHP排序函数的一般特点:

  a. 需要额外的空间,空间复杂度是O(n), 因而应该尽量避免对很大的数组排序.

  b. 底层使用快速排序,平均时间复杂度是O(n*lgn)

zend_qsort的 实现代码(有兴趣的童鞋可以研究一下实现细节):

[cpp] view plain copy print?
  1. ZEND_API void zend_qsort(void *base, size_t nmemb, size_t siz, compare_func_t compare TSRMLS_DC)  
  2. {  
  3.     /* 存储开始和结束指针的栈 */  
  4.     void           *begin_stack[QSORT_STACK_SIZE];  
  5.     void           *end_stack[QSORT_STACK_SIZE];  
  6.     register char  *begin;  
  7.     register char  *end;  
  8.     register char  *seg1;  
  9.     register char  *seg2;  
  10.        
  11.     /* partition index */  
  12.     register char  *seg2p;  
  13.     register int    loop;  
  14.        
  15.     /* pivot index */  
  16.     uint            offset;  
  17.        
  18.     begin_stack[0] = (char *) base;  
  19.     end_stack[0]   = (char *) base + ((nmemb - 1) * siz);  
  20.    
  21.     for (loop = 0; loop >= 0; --loop) {  
  22.         begin = begin_stack[loop];  
  23.         end   = end_stack[loop];  
  24.            
  25.         /* partition的过程 */  
  26.         while (begin < end) {  
  27.           offset = (end - begin) >> 1;  
  28.           _zend_qsort_swap(begin, begin + (offset - (offset % siz)), siz);  
  29.    
  30.           seg1 = begin + siz;  
  31.           seg2 = end;  
  32.    
  33.           while (1) {  
  34.             /* 从左向右找 */  
  35.             for (; seg1 < seg2 && compare(begin, seg1 TSRMLS_CC) > 0;  
  36.                seg1 += siz);  
  37.                    
  38.               /* 从右向左找 */  
  39.               for (; seg2 >= seg1 && compare(seg2, begin TSRMLS_CC) > 0;  
  40.                 seg2 -= siz);  
  41.                    
  42.               if (seg1 >= seg2)  
  43.                 break;  
  44.                    
  45.               /* 交换seg1和seg2指向的值 */  
  46.               _zend_qsort_swap(seg1, seg2, siz);  
  47.                    
  48.               /* 指针移动,每次都是siz步长 */  
  49.               seg1 += siz;  
  50.               seg2 -= siz;  
  51.             }  
  52.    
  53.             _zend_qsort_swap(begin, seg2, siz);  
  54.    
  55.             seg2p = seg2;  
  56.                
  57.             /* 右半部分 */  
  58.             if ((seg2p - begin) <= (end - seg2p)) {  
  59.                 if ((seg2p + siz) < end) {  
  60.                   begin_stack[loop] = seg2p + siz;  
  61.                   end_stack[loop++] = end;  
  62.                 }  
  63.                 end = seg2p - siz;  
  64.             }  
  65.             else { /* 左半部分 */  
  66.                 if ((seg2p - siz) > begin) {  
  67.                     begin_stack[loop] = begin;  
  68.                     end_stack[loop++] = seg2p - siz;  
  69.                 }  
  70.                 begin = seg2p + siz;  
  71.             }  
  72.         }  
  73.     }  
  74. }  

4.  数组合并 array_merge

array_merge用于合并两个或者多个数组(实际上,array_merge可以仅传入一个数组参数如array_merge($a)  )例如:

[cpp] view plain copy print?
  1. $a = array('index' => "a",1 =>'a');  
  2. $b = array('index' => "b",1 =>'b');  
  3. print_r(array_merge($a, $b));  

结果是:

Array(    [index] => b    [0] => a    [1] => b)

那么,对于array_merge, PHP底层是如何处理字符串索引和数字索引的呢?

1
2
3
4
PHP_FUNCTION(array_merge)
{
    php_array_merge_or_replace_wrapper(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0, 0);
}

因此,实际上是通过php_array_merge_or_replace_wrapper来完成的,继续查看php_array_merge_or_replace_wrapper的实现:

static void php_array_merge_or_replace_wrapper(INTERNAL_FUNCTION_PARAMETERS, int recursive, int replace);

注意传入的参数,recursive=0, replace=0 ( 不递归merge,数字索引不替换 ) ,而INTERNAL_FUNCTION_PARAMETERS是:

#define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value, zval **return_value_ptr, zval *this_ptr, int return_value_used     TSRMLS_DC

array_merge的基本过程是:

(1)     确定初始化数组的大小(使用元素最多的数组的大小作为结果数组的初始大小),初始化数组:

[cpp] view plain copy print?
  1. for (i = 0; i < argc; i++) {  
  2.       /* 不是数组 */  
  3.     if (Z_TYPE_PP(args[i]) != IS_ARRAY) {  
  4.         php_error_docref(NULL TSRMLS_CC, E_WARNING, "Argument #%d is not an array", i + 1);  
  5.         efree(args);  
  6.         RETURN_NULL();  
  7.     } else {  
  8.         int num = zend_hash_num_elements(Z_ARRVAL_PP(args[i]));             
  9.            
  10.         /* 使用元素最多的数组的大小作为init_size的大小 */  
  11.         if (num > init_size) {  
  12.             init_size = num;  
  13.         }  
  14.     }  
  15. }  
  16.    
  17. array_init_size(return_value, init_size);  

return_value是个zval *, 它指向返回值的zval

(2)     对array_merge参数中的每个数组,依次执行php_array_merge(由于replace=0和recursive=0), 我们只看第一个分支:

[cpp] view plain copy print?
  1. for (i = 0; i < argc; i++) {  
  2. SEPARATE_ZVAL(args[i]);  
  3.    
  4. if (!replace) {  
  5.         php_array_merge(Z_ARRVAL_P(return_value), Z_ARRVAL_PP(args[i]), recursive TSRMLS_CC);  
  6.     }  
  7. }  

SEPARATE_ZVAL用于创建一个与原始数据相同的zval,避免在操作的过程中修改参数的值(参数是非引用传递的情况下)。而真正的merge过程是通过php_array_merge来实现的。

(3)     merge的过程

由于PHP数组中包含字符串索引和数字索引,对于这两类不同的索引,merge的处理是不同的(replace=0, recursive=0,只看对应的分支):

[cpp] view plain copy print?
  1. switch (zend_hash_get_current_key_ex(src, &string_key, &string_key_len, &num_key, 0, &pos)){  
  2.     case HASH_KEY_IS_STRING:  
  3.         Z_ADDREF_PP(src_entry);  
  4.         zend_hash_update(dest, string_key, string_key_len, src_entry, sizeof(zval *), NULL);  
  5.     break;  
  6.    
  7.     case HASH_KEY_IS_LONG:  
  8.         Z_ADDREF_PP(src_entry);  
  9.         zend_hash_next_index_insert(dest, src_entry, sizeof(zval *), NULL);  
  10.     break;  
  11. }  

上述代码表明:对于字符串索引,PHP在执行array_merge的时候,会更新字符串索引的值,其结果就是参数靠后数组的值会覆盖靠前的数组的值。而对于数字型索引,PHP执行的zend_hash_next_index_insert操作,也就是插入一个新的元素,这同时也更改了键(例如原来的key=2, array_merge之后,可能变成了0)。这也解释了最开始array_merge脚本的输出:

[php] view plain copy print?
  1. $a = array('index' => "a",1 =>'a');  
  2. $b = array('index' => "b",1 =>'b');  
  3. print_r(array_merge($a$b));  

更多的数组操作函数我们不再一一介绍,只要知道了HashTable的结构,要理解这些实现,并不困难。

由于写作匆忙,本文难免会有错误之处,敬请批评指正。

ps: 近期正在补习C语言/操作系统的相关基础,尤其是指针/内存管理这一块,有一起的同学,欢迎交流。


来源:http://blog.csdn.net/ohmygirl/article/details/42145895

原创粉丝点击