Zend读码笔记(1)-双向链表

Zend 读码笔记 (1)  zend_llist 双向链表

版本:php 7.1.4

阅读著名的开源项目源码确实不是一件容易的事情,而且自身水平未到,未必能明白每一个细节实现的优劣;但是读起来确实有味,这些作为学习的笔记,也和大家一起分享了。水平有限,有错误的或者模糊的或者我没理解到没提到的,欢迎指正和补充。

zend引擎所使用的链表是一个双向链表。在Zend工程中,这个链表不再是数据结构书本上的“玩具实现”,而是确实作为Zend引擎的一个基础数据结构模块,嵌入在Zend的各个角落。因此,除了数据结构本身的特性,还做了很多优化和扩展,还规划了非常好的接口以供各个模块之间的耦合。

在 zend_llist.h 中,定义了如下的链表结点的原型:

1 typedef struct _zend_llist_element {
2     struct _zend_llist_element *next;
3     struct _zend_llist_element *prev;
4     char data[1]; /* Needs to always be last in the struct */
5 } zend_llist_element;

这个同普通的链表结点没有太大的区别,只是最后一个字段 char data[1] 较为特殊,通过柔性数组构成了一个变长结构体。GCC是支持零长度数组这一特性的,然而某些C编译器不支持零长度的数组,因此这里为了可移植性采取了折中的做法,即声明长度为1,然后再使用时,对其长度减去1,就得到和直接声明 char data[0] 的同样的效果了。至于为啥要用变长结构体,可能是考虑减少一次空间分配,减少了内存碎片,同时连续的内存地址是cache friendly的,进而提升性能。

接下来还有一个结构体定义,定义了链表本身的相关属性:

1 typedef struct _zend_llist {
2     zend_llist_element *head;
3     zend_llist_element *tail;
4     size_t count;
5     size_t size;
6     llist_dtor_func_t dtor;
7     unsigned char persistent;
8     zend_llist_element *traverse_ptr;
9 } zend_llist;

为了泛型化,需要额外储存链表中数据类型的大小,因此定义了size_t 的 size 。persistent 貌似是与空间分配和垃圾回收相关的;dtor 是元素的析构函数,虽然C是面向过程的,但是思想还是可以通用的嘛。头尾指针和当前的遍历指针,用额外的一点空间换取操作时的简单化,是值得的。

除此之外,zend_llist.h 中还定义了一些函数指针,比如元素的比较,对结点里的元素应用某些操作,等等,作为接口。还有导出的函数原型声明,就不列出了。

下面重点就是 zend_llist.c 具体的实现部分了。

作为一个基础的数据结构,一些常规的routine就不写了,在任意一本数据结构教材上都可以找得到。

zend_llist_add_element 这个函数是将新的元素插入链表的操作,默认将元素插入尾部。而 zend_llist_prepend_element 也是将新的元素插入链表的操作,只不过是头插入法。链表本身插入没什么好说的,也就是这里元素的具体存储方式有点特别,需要单独看一下:

 1 ZEND_API void zend_llist_add_element(zend_llist *l, void *element)
 2 {
 3     zend_llist_element *tmp = pemalloc(sizeof(zend_llist_element)+l->size-1, l->persistent);
 4     /*    pemalloc是zend自己的空间配置器,作用相当于普通的库函数malloc    
 5           申请的大小为结构体的大小和内容本身的大小
 6           因为data占用了1个byte,因此需要最后减去1                              */  
 7     /*    下面是双向链表的插入过程,
 8           这里额外维护的tail指针和普通的带头链表的head处理方法相类似    */
 9     tmp->prev = l->tail;
10     tmp->next = NULL;
11     if (l->tail) {
12         l->tail->next = tmp;
13     } else {
14         l->head = tmp;
15     }
16     l->tail = tmp;
17     /*    利用memcpy函数复制元素的内容.
18           这里可以很清楚的看到变长结构体的最后一个字段如何使用.*/
19     memcpy(tmp->data, element, l->size);
20     ++l->count;
21 }            

这个函数也演示了对于变长结构体最后一个元素如何去使用。在变长结构体中,最后一个元素定义为0长度或者1长度。其实定义为任何长度都可以,只要所分配的可用空间总大小不出错就可以,只不过0更符合它叫“变长”的特点。此时 data 就当作一个指向 char 的指针来用就可以了。对于 memcpy 直接复制元素这个处理方法而言,非常简单实用,而且库中的 memcpy 的优化往往也是非常到位。 memcpy 是不会被inline的,也就是说,在小数据(内置数据类型)复制时,function call本身的开销会比较大,相比于内置的“=”(直接寄存器赋值)运算符来说。当然C++的class重载的“=”就不好说了,本质还是函数调用,不一定可以inline的,或者inline的副作用更大。所以这种方法处理内置的数据类型,还是不大好用的。

zend_llist_del_element 删除元素。本身没什么好说的,就是函数指针这个东西对于像我这样的新手而言确实很冷门,但是很实用。

 1 ZEND_API void zend_llist_del_element(zend_llist *l, void *element, int (*compare)(void *element1, void *element2))
 2 {
 3     zend_llist_element *current=l->head;
 4     while (current) {
 5         if (compare(current->data, element)) {
 6             DEL_LLIST_ELEMENT(current, l);
 7             break;
 8         }
 9         current = current->next;
10     }
11 }

销毁整个链表也很简单,注意要手动通过函数指针调用节点里元素的析构函数,然后调用空间配置器的 pefree 释放内存空间,确实没C++的 delete 省心。不过记得Meyer在Effective C++书中也看到过,有时候C++项目也要自己重载delete的,而且在STL源码剖析中提到,STL也是自己实现了一套空间配置器,所以这样来看手工进行析构+释放内存也是不足为奇的。

 1 ZEND_API void zend_llist_destroy(zend_llist *l)
 2 {
 3     zend_llist_element *current=l->head, *next;
 4     while (current) {
 5         next = current->next;
 6         if (l->dtor) {
 7             l->dtor(current->data);
 8         }
 9         pefree(current, l->persistent);
10         current = next;
11     }
12     l->count = 0;
13 }
 至于zend_llist_copy,就是把一个链表中的所有组成全部复制到另外一个链表中,理解上相当于C++中重载了“=”运算符。只不过实现方式,并不同往常的“复制”概念一样。
这里有一个很容易犯的错误,就是带指针的结构体进行复制,一定要注意处理其指针的指向。有些时候将带指针的结构体写入文件,也是同样的道理。

 1 ZEND_API void zend_llist_copy(zend_llist *dst, zend_llist *src)
 2 {
 3     zend_llist_element *ptr;
 4     /*    假定新链表没有被初始化.    */
 5     zend_llist_init(dst, src->size, src->dtor, src->persistent);
 6     ptr = src->head;
 7     /*    直接用add_element函数代替memcpy进行复制.
 8           因为memcpy复制整个结点,也会复制其指针.
 9           复制完了再修改指针的指向,效率未必更高.
10           反而让代码的复杂程度增加,看起来更凌乱.        */
11     while (ptr) {
12         zend_llist_add_element(dst, ptr->data);
13         ptr = ptr->next;
14     }
15 }

 随后有两个函数,用来给链表的元素应用某些操作。这不是一个抽象的数据结构必须要有的routine,但是在某些实际项目中是必要的。同样是使用了函数指针作为接口,看来函数指针对于C来说确实是非常重要的东西。就放一下zend_llist_apply_with_del,zend_llist_apply是其简化版就不列出了

 1 ZEND_API void zend_llist_apply_with_del(zend_llist *l, int (*func)(void *data))
 2 {
 3     zend_llist_element *element, *next;
 4     element=l->head;
 5     while (element) {
 6         /*    保存临时的next指针,因为删除后本身就失效了.    */
 7         next = element->next;
 8         if (func(element->data)) {
 9             DEL_LLIST_ELEMENT(element, l);
10         }
11         element = next;
12     }
13 }

下面对链表的排序就是非常有意思的地方。这里首先定义了一个swap函数,用来处理指针的交换。在C中交换变量的值,需要通过指针来实现;而交换指针,显然就需要“指针的指针”来实现了。

1 static void zend_llist_swap(zend_llist_element **p, zend_llist_element **q)
2 {
3     zend_llist_element *t;
4     t = *p;
5     *p = *q;
6     *q = t;
7 }

完成swap这个准备工作以后,就正式进入了sort的环节。其实一开始我也在纠结为什么swap不是交换数据,而是交换的指针。按照传统的想法,对链表进行排序,可以通过比较后交换数据来解决,但是这里交换了指针,说明不是这条路;或者,也可以比较后调节指针的指向,但是这里直接简单交换指针显然也不符合这种想法的。

对数组基于比较的排序,有着下界O(nlgn)的时间复杂度,但是无论是快速排序、归并排序、堆排序,都是要基于数组可以随机寻址这一操作的,对于链表而言不大容易实现;因此链表通常使用O(n2)的,比如插入排序,来完成。

基于上面的疑惑,Zend所实现的排序确实让我感到眼前一亮。它首先创建一个数组,而后将所有结点的指针放入这个数组,因而便可以用这些nlgn的算法对数组排序;当然排序完成了,指针的指向也错乱了,下面就是根据有序性来恢复指针指向的步骤了。因为数组是有序的,所以只要依次将结点挂在已有的链表尾部就可以了,非常简单。

所以这个算法整体的时间复杂度取决于中间调用的sort的时间复杂度,而此处的sort作为通用排序算法,很容易使用快速排序来达到O(nlgn)的期望时间。常数可能稍微大一点(在空间分配和回收上)。

顺便一提的就是STL也可以利用iterator作为接口调用sort函数,从而对链表进行排序。这说明了基础数据结构的设计一定要处理好和其他模块的通用接口。


 1 ZEND_API void zend_llist_sort(zend_llist *l, llist_compare_func_t comp_func)
 2 {
 3     size_t i;
 4     zend_llist_element **elements;
 5     zend_llist_element *element, **ptr;
 6     if (l->count <= 0) {
 7         return;
 8     }
 9     /*    将所有结点的指针单独以连续数组的方式存储.    */
10     elements = (zend_llist_element **) emalloc(l->count * sizeof(zend_llist_element *));
11     ptr = &elements[0];
12     for (element=l->head; element; element=element->next) {
13         *ptr++ = element;
14     }
15     /*    调用自己实现的sort函数进行排序.    */
16     zend_sort(elements, l->count, sizeof(zend_llist_element *),
17             (compare_func_t) comp_func, (swap_func_t) zend_llist_swap);
18     /*    恢复链表的所有结点的指针指向.    */
19     l->head = elements[0];
20     elements[0]->prev = NULL;
21     for (i = 1; i < l->count; i++) {
22         elements[i]->prev = elements[i-1];
23         elements[i-1]->next = elements[i];
24     }
25     elements[i-1]->next = NULL;
26     l->tail = elements[i-1];
27     /*    释放临时构造的数组.    */
28     efree(elements);
29 }

 

posted @ 2017-05-15 21:57  g63  阅读(403)  评论(0编辑  收藏  举报