PHP代码实现2 [从变量和数据的角度] 1

PHP代码实现2 [从变量和数据的角度] 1

数据类型

1.静态类型语言,比如:C/Java等,在静态语言类型中,类型的检查是在<编译>(compile-time)确定的, 也就是说在运行时变量的类型是不会发生变化的。

2.动态语言类型,比如:PHP,python等各种脚本语言,这类语言中的类型是在[运行时]确定的, 那么也就是说类型通常可以在运行时发生变化

3.无类型语言,比如:汇编语言,汇编语言操作的是底层存储,他们对类型毫无感知。

PHP 8种变量类型

String Int float Boolean Null Resource Object array

在官方的PHP实现内部,所有变量使用同一种数据结构(zval)来保存,而这个结构同时表示PHP中的各种数据类型。 它不仅仅包含变量的值,也包含变量的类型。这就是PHP弱类型的核心。

PHP变量存储结构

1
2
3
4
5
6
7
8
9
typedef struct _zval_struct zval;
...
struct _zval_struct {
/* Variable information */
zvalue_value value; /* value */ 变量值
zend_uint refcount__gc; // 引用计数 GC的时候用 默认1
zend_uchar type; /* active type */ 变量类型
zend_uchar is_ref__gc; // 是否为引用 默认0
};

PS:在PHP5.3之后引入了新垃圾收集机制 则refcount -> refcount_gc is_ref -> is_ref_gc

【注意 变量的值存在了zvalue_value这个struct中,那么也就是其的内存占用很玄学,也就是是用union 来巧妙的避开多内存占用】

type:IS_NULL、IS_BOOL、IS_LONG、IS_DOUBLE、IS_STRING、IS_ARRAY、IS_OBJECT和IS_RESOURCE

PHP变量的值的存储

1
2
3
4
5
6
7
8
9
10
typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct { /*string value*/
char *val;
int len;
} str;
HashTable *ht; /* hash table value */
zend_object_value obj;
} zvalue_value;

这里使用联合体而不是用结构体是出于空间利用率的考虑,因为一个变量同时只能属于一种类型。 如果使用结构体的话将会不必要的浪费空间,而PHP中的所有逻辑都围绕变量来进行的,这样的话, 内存浪费将是十分大的。这种做法成本小但收益非常大。

注意到字符串value中加入了int的 len, 这和MyIsam存储结构存储长度的原理是相同的,因为获取到字符串长度的时间复杂度是O(n),字符串在PHP操作中很频繁,为了节约时间开销,直接存储进来。是一种空间换时间的做法

【哈希相关】

通过合理设计的哈希函数,我们就能将key映射到合适的范围,因为我们的key空间可以很大(例如字符串key), 在映射到一个较小的空间中时可能会出现两个不同的key映射被到同一个index上的情况, 这就是我们所说的出现了冲突。 目前解决hash冲突的方法主要有两种:链接法和开放寻址法。

  • 链接法通过使用一个链表来保存slot值的方式来解决冲突,也就是当不同的key映射到一个槽中的时候使用链表来保存这些值。 所以使用链接法是在最坏的情况下,也就是所有的key都映射到同一个槽中了,这样哈希表就退化成了一个链表, 这样的话操作链表的时间复杂度则成了O(n),这样哈希表的性能优势就没有了, 所以选择一个合适的哈希函数是最为关键的。
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _Bucket /** 采用链接法解决哈希冲突的HashTable结构 最坏情况 O(n) **/
{
char *key;
void *value;
struct _Bucket *next;
} Bucket;

typedef struct _HashTable
{
int size;
int elem_num;
Bucket** buckets;
} HashTable;
  • 键(key):用于操作数据的标示,例如PHP数组中的索引,或者字符串键等等。
  • 槽(slot/bucket):哈希表中用于保存数据的一个单元,也就是数据真正存放的容器。
  • 哈希函数(hash function):将key映射(map)到数据应该存放的slot所在位置的函数。
  • 哈希冲突(hash collision):哈希函数将两个不同的key映射到同一个索引的情况。

hash_Insert函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
int hash_insert(HashTable *ht, char *key, void *value)
{
// check if we need to resize the hashtable
resize_hash_table_if_needed(ht);

int index = HASH_INDEX(ht, key);

Bucket *org_bucket = ht->buckets[index]; // 获得计算出来的index所在的bucket
Bucket *tmp_bucket = org_bucket;

// check if the key-value exits already
while(tmp_bucket)
{
if(strcmp(key, tmp_bucket->key) == 0)
{
LOG_MSG("[update]\tkey: %s\n", key);
tmp_bucket->value = value;

return SUCCESS; // 查看当前的key对应的value是否已经存在了,value是不允许重复的
}

tmp_bucket = tmp_bucket->next;
}

Bucket *bucket = (Bucket *)malloc(sizeof(Bucket)); 分配新的bucket的地址

bucket->key = key;
bucket->value = value;
bucket->next = NULL; 进行相关bucket的赋值

ht->elem_num += 1; HashTable计数增加

if(org_bucket != NULL)
{
LOG_MSG("[collision]\tindex:%d key:%s\n", index, key); 出现冲突
bucket->next = org_bucket; 将新的bucket放在index的第一位 后面进行顺延
}

ht->buckets[index]= bucket;

LOG_MSG("[insert]\tindex:%d key:%s\tht(num:%d)\n",
index, key, ht->elem_num);

return SUCCESS;
}

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static int hash_resize(HashTable *ht)
{
// double the size
int org_size = ht->size;
ht->size = ht->size * 2;
ht->elem_num = 0; //后续重新插入,因为Double size之后 之前的冲突有可能被解开,所以重新Insert

LOG_MSG("[resize]\torg size: %i\tnew size: %i\n", org_size, ht->size);

Bucket **buckets = (Bucket **)calloc(ht->size, sizeof(Bucket *));

Bucket **org_buckets = ht->buckets; 获取老的buckets
ht->buckets = buckets;// 新的table置为空

int i = 0;
for(i=0; i < org_size; ++i)
{
Bucket *cur = org_buckets[i];
Bucket *tmp;
while(cur)
{
// rehash: insert again
hash_insert(ht, cur->key, cur->value);

// free the org bucket, but not the element
tmp = cur;
cur = cur->next;
free(tmp);
}
}
free(org_buckets);

LOG_MSG("[resize] done\n");

return SUCCESS;
}

数组是PHP中最常用,也是最强大变量类型,它可以存储其他类型的数据,而且提供各种内置操作函数。数组的存储相对于其他变量要复杂一些, 数组的值存储在zvalue_value.ht字段中,它是一个HashTable类型的数据。 PHP的数组使用哈希表来存储关联数据。哈希表是一种高效的键值对存储结构。PHP的哈希表实现中使用了两个数据结构HashTable和Bucket。 PHP所有的工作都由哈希表实现,在下节HashTable中将进行哈希表基本概念的介绍以及PHP的哈希表实现。(WOC PHP竟然用HashTable存储array,O(1)!)

对象的存储:

1
2
3
4
typedef struct _zend_object_value {
zend_object_handle handle; // unsigned int类型,EG(objects_store).object_buckets的索引
zend_object_handlers *handlers;
} zend_object_value;

PHP的对象只有在运行时才会被创建,前面的章节介绍了EG宏,这是一个全局结构体用于保存在运行时的数据。 其中就包括了用来保存所有被创建的对象的对象池

而object对象值内容的zend_object_handle域就是当前 对象在对象池中所在的索引,handlers字段则是将对象进行操作时的处理函数保存起来。 这个结构体及对象相关的类的结构_zend_class_entry,将在第五章作详细介绍

PHP HashTable实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _hashtable { 
uint nTableSize; // hash Bucket的大小,最小为8,以2x增长。
uint nTableMask; // nTableSize-1 , 索引取值的优化
uint nNumOfElements; // hash Bucket中当前存在的元素个数,count()函数会直接返回此值
ulong nNextFreeElement; // 下一个数字索引的位置
Bucket *pInternalPointer; // 当前遍历的指针(foreach比for快的原因之一)
Bucket *pListHead; // 存储数组头元素指针
Bucket *pListTail; // 存储数组尾元素指针
Bucket **arBuckets; // 存储hash数组
dtor_func_t pDestructor; // 在删除元素时执行的回调函数,用于资源的释放
zend_bool persistent; //指出了Bucket内存分配的方式。如果persisient为TRUE,则使用操作系统本身的内存分配函数为Bucket分配内存,否则使用PHP的内存分配函数。
unsigned char nApplyCount; // 标记当前hash Bucket被递归访问的次数(防止多次递归)
zend_bool bApplyProtection;// 标记当前hash桶允许不允许多次访问,不允许时,最多只能递归3次
#if ZEND_DEBUG
int inconsistent;
#endif
} HashTable;

PS:mask的作用就是将哈希值映射到槽位所能存储的索引范围内。 例如:某个key的索引值是21, 哈希表的大小为8,则mask为7,则求与时的二进制表示为: 10101 & 111 = 101 也就是十进制的5。 因为2的整数次方-1的二进制比较特殊:后面N位的值都是1,这样比较容易能将值进行映射, 如果是普通数字进行了二进制与之后会影响哈希值的结果。那么哈希函数计算的值的平均分布就可能出现影响。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct bucket {
ulong h; // 对char *key进行hash后的值,或者是用户指定的数字索引值
uint nKeyLength; // hash关键字的长度,如果数组索引为数字,此值为0
void *pData; // 指向value,一般是用户数据的副本,如果是指针数据,则指向pDataPtr
void *pDataPtr; //如果是指针数据,此值会指向真正的value,同时上面pData会指向此值
struct bucket *pListNext; // 整个hash表的下一元素
struct bucket *pListLast; // 整个哈希表该元素的上一个元素
struct bucket *pNext; // 存放在同一个hash Bucket内的下一个元素
struct bucket *pLast; // 同一个哈希bucket的上一个元素
// 保存当前值所对于的key字符串,这个字段只能定义在最后,实现变长结构体
char arKey[1]; // 是变长结构体 在插入的时候进行单独申请空间就可以
} Bucket;

PS:如上面各字段的注释。h字段保存哈希表key哈希后的值。这里保存的哈希值而不是在哈希表中的索引值, 这是因为索引值和哈希表的容量有直接关系,如果哈希表扩容了,那么这些索引还得重新进行哈希在进行索引映射, 这也是一种优化手段。

在PHP数组中如果索引字符串可以被转换成数字也会被转换成数字索引。 所以在PHP中例如’10’,’11’这类的字符索引和数字索引10, 11没有区别。

HashTable的操作接口常见:

  • 初始化操作,例如zend_hash_init()函数,用于初始化哈希表接口,分配空间等。
  • 查找,插入,删除和更新操作接口,这是比较常规的操作。
  • 迭代和循环,这类的接口用于循环对哈希表进行操作。
  • 复制,排序,倒置和销毁等操作。

PS:在PHP中不管是对数组的添加操作(zend_hash_add),还是对数组的更新操作(zend_hash_update), 其最终都是调用_zend_hash_add_or_update函数完成,这在面向对象编程中相当于两个公有方法和一个公共的私有方法的结构, 以实现一定程度上的代码复用。

PHP List链表实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _zend_llist_element {
struct _zend_llist_element *next;
struct _zend_llist_element *prev;
char data[1]; /* Needs to always be last in the struct */
} zend_llist_element;

typedef struct _zend_llist {
zend_llist_element *head;
zend_llist_element *tail;
size_t count;
size_t size;
llist_dtor_func_t dtor;
unsigned char persistent;
zend_llist_element *traverse_ptr;
} zend_llist;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ZEND_API void zend_llist_add_element(zend_llist *l, void *element) // List插入操作
{
zend_llist_element *tmp = pemalloc(sizeof(zend_llist_element)+l->size-1, l->persistent);

tmp->prev = l->tail;
tmp->next = NULL;
if (l->tail) {
l->tail->next = tmp;
} else {
l->head = tmp;
}
l->tail = tmp;
memcpy(tmp->data, element, l->size);

++l->count;
}

PS:PHP中很多的函数都会有*_ex()以及不带ex两个版本的函数,这主要是为了方便使用, 和上面的代码一样,ex版本的通常是一个功能较全或者可选参数较多的版本, 而在代码中很多地方默认的参数值都一样,为了方便使用,再封装一个普通版本。

posted @ 2018-09-10 23:28  guoguoqingzhe  阅读(107)  评论(0编辑  收藏  举报