一起学习PHP7内核之HashTable-PHP7

资源魔 45 0

以前的俩篇文章深化了解PHP7内核之zval 以及深化了解PHP7内核之Reference, 我引见了过后正在开发PHP7的时分对zval以及reference的一些革新考虑以及后果, 之后由于的确精力无限就不持续往下写,时隔一年多当前,由于这场从天而降的疫情,正在家办公的工夫不少, 于是终于有了工夫让我来持续引见一下PHP7的中Hashtable的变动, 和过后咱们做这些变动面前的考量.

PHP5


关于PHP内核不断无关注的同窗, 应该对PHP5的Hashtable会比拟相熟, 但咱们仍是先来简略回顾一下PHP5的Hashtable:

正在PHP5的完成中, Hashtable的外围是存储了一个个指向zval指针的指针, 也就是zval**(我遇到很多的同窗问为何是zval**, 而没有是zval*, 这个缘由其实很简略, 由于Hashtable中的多个地位均可能指向同一个zval, 那末最多见的一个可能就是正在COW的时分, 当咱们需求把一个变量指向一个新的zval的时分, 假如正在符号表中存的是zval*, 那们咱们就做没有到对一处修正, 一切的持无方都有感知, 以是必需是zval**), 这样的设计正在最后的登程点是为了让Hashtable能够存储任何尺寸的任何信息, 不只仅是指针, 还能够存储一段内存值(当然实际上年夜局部状况下,比方符号表仍是存的zval的指针).

PHP5的代码中也用了比拟Hack的形式来判别存储的是甚么:

#define UPDATE_DATA(ht, p, pData, nDataSize)                                            
    if (nDataSize == sizeof(void*)) {                                                   
        if ((p)->pData != &(p)->pDataPtr) {                                             
            pefree_rel((p)->pData, (ht)->persistent);                                   
        }                                                                               
        memcpy(&(p)->pDataPtr, pData, sizeof(void *));                                  
        (p)->pData = &(p)->pDataPtr;                                                    
    } else {                                                                            
        if ((p)->pData == &(p)->pDataPtr) {                                             
            (p)->pData = (void *) pemalloc_rel(nDataSize, (ht)->persistent);            
            (p)->pDataPtr=NULL;                                                         
        } else {                                                                        
            (p)->pData = (void *) perealloc_rel((p)->pData, nDataSize, (ht)->persistent);   \
            /* (p)->pDataPtr is already NULL so no need to initialize it */             \
        }                                                                              
        memcpy((p)->pData, pData, nDataSize);                                           
    }

它来判别存储的size是否是一个指针巨细, 从而采纳没有同的形式来更新存储的内容。十分Hack的形式。

PHP5的Hashtable关于每个Bucket都是离开请求开释的。

而存储正在Hashtable中的数据是也会经过pListNext指针串成一个list,能够间接遍历,对于这块能够参看我很早的一篇文章深化了解PHP之数组

成绩

正在写PHP7的时分,咱们具体考虑了几点可能优化的点,包罗也从功能角度总结了如下今朝这类完成的几个成绩:

  • Hashtable正在PHP中,使用最多的是保留各类zval, 而PHP5的HashTable设计的太通用,能够设计为专门为了存储zval而优化, 从而能缩小内存占用。
  • 2. 缓存部分性成绩, 由于PHP5的Hashtable的Bucket,包罗zval都是自力调配的,而且采纳了List来串Hashtable中的一切元素,会招致正在遍历或许程序拜访一个数组的时分,缓存没有敌对。

    比方如图所示正在PHP代码中常见的foreach一个数组, 就会发作屡次的内存腾跃.
  • 3. 以及1相似,正在PHP5的Hashtable中,要拜访一个zval,由于是zval**, 那需求至多解指针俩次,一方面是缓存没有敌对,另一方面也是效率低下。
    比方上图中,蓝色框的局部,咱们找到数组中的bucket当前,还需求解开zval**,才能够读取到实际的zval的内容。 也就是需求发作俩次内存读取。效率低下。

当然另有不少的其余的成绩,此处再也不赘述,说真实的究竟结果俩年多了,过后怎样想的,如今有些也想没有起来了, 如今咱们来看看PHP7的

PHP7

起首正在PHP7中,咱们过后的思考是可能由于担忧Hashtable用的太多,咱们新设计的构造体可能纷歧定能Cover一切的场景,于是咱们新界说了一个构造体叫做zend_array, 当然最初通过一系列的致力,发现zend_array能够齐全代替Hashtable, 最初仍是保存了Hashtable以及zend_array俩个名字,只不外互为Alias.
再上面的文章中,我会用HashTable来特指PHP5中的Hashtable,而用zend_array来指代PHP7中的Hashtable.

咱们先来看看zend_array的界说:

struct _zend_array {
    zend_refcounted_h gc;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    flags,
                zend_uchar    _unused,
                zend_uchar    nIteratorsCount,
                zend_uchar    _unused2)
        } v;
        uint32_t flags;
    } u;
    uint32_t          nTableMask;
    Bucket           *arData;
    uint32_t          nNumUsed;
    uint32_t          nNumOfElements;
    uint32_t          nTableSize;
    uint32_t          nInternalPointer;
    zend_long         nNextFreeElement;
    dtor_func_t       pDestructor;
};

相比PHP5时代的Hashtable,zend_array的内存占用从PHP5点72个字节,升高到了56个字节,想一想一个PHP生命过程中不计其数的数组,内存升高显著。

此处再次特地阐明下下面zend_array界说中的ZEND_ENDIAN_LOHT_4这个作用,这个是为理解决巨细端成绩,正在年夜端小端上都能让此中的元素保障一样的内存存储程序,能够不便咱们写出通用的位操作。 正在PHP7中,位操作使用的不少,由于这样一来一个字节就能够保留8个状态位, 很节流内存:)

#ifdef WORDS_BIGENDIAN
# define ZEND_ENDIAN_LOHI_4(a, b, c, d)    d; c; b; a;
#else
# define ZEND_ENDIAN_LOHI_4(a, b, c, d)    a; b; c; d;
#endif

而数据会外围保留正在arData中,arData是一个Bucket数组,Bucket界说:

typedef struct _Bucket {
    zval              val;
    zend_ulong        h;   /* hash value (or numeric index)   */
    zend_string      *key; /* string key or NULL for numerics */
} Bucket

再比照看下PHP5多Bucket:

typedef struct bucket {
    ulong h;               /* Used for numeric indexing */
    uint nKeyLength;
    void *pData;
    void *pDataPtr;
    struct bucket *pListNext;
    struct bucket *pListLast;
    struct bucket *pNext;
    struct bucket *pLast;
    const char *arKey;
} Bucket;

内存占用从72字节,升高到了32个字节,想一想一个PHP过程中几十万的数组元素,这个内存升高就更显著了.

比照的看,

  • 如今的抵触拉链被bauck.zval->u2.next代替, 于是bucket->pNext, bucket->pLast能够去掉了。
  • zend_array->arData是一个数组,以是也就再也不需求pListNext, pListLast来放弃程序了, 他们也能够去掉了。 如今数组中元素的前后程序,齐全依据它正在arData中的index程序决议,先退出的元素正在低的index中。
  • PHP7中的Bucket如今间接保留一个zval, 庖代了PHP5时代bucket中的pData以及pDataPtr。
  • 最初就是PHP7中如今应用zend_string作为数组的字符串key,庖代了PHP5时代bucket的*key, nKeylength.

如今咱们来全体看下zend_array的组织图:

回顾下深化了解PHP7内核之ZVAL, 如今的zend_array就能够应酬各类场景下的HashTable的作用了。
特地阐明对是除了了一个成绩就是以前提到过的IS_INDIRECT, 没有晓得各人能否还记患上. 上一篇我说过原来HashTable为何要设计保留zval**, 那末如今由于_Bucket间接保留的是zval了,那怎样处理COW的时分一处修正多处可见的需要呢? IS_INDIRECT就使用而生了,IS_INDIRECT类型其实能够了解为就是一个zval*的构造体。它被宽泛使用正在GLOBALS,Properties等多个需求俩个HashTable指向同于一个ZVAL的场景。

另外,关于原来一些扩大会应用HashTable来保留一些本人的内存,如今能够经过IS_PTR这类zval类型来完成。

如今arData由于是一个延续的数组了,那末当foreach的时分,就能够程序拜访一块延续的内存,而如今zval间接保留正在bucket中,那末正在绝年夜局部状况下(没有需求内部指针的内容,比方long,bool之类的)就能够没有需求任何额定的zval指针解援用了,缓存部分性敌对,功能晋升十分显著。

另有就是PHP5的时代,查找数组元素的时分,由于通报出去的是char *key,一切需求每一次查找都较量争论key的Hash值,而如今查找的时分通报出去的key是zend_string, Hash值没有需求从新较量争论,此处也有局部功能晋升。

ZEND_API zval* ZEND_FASTCALL zend_hash_find(const HashTable *ht, zend_string *key);
ZEND_API zval* ZEND_FASTCALL zend_hash_str_find(const HashTable *ht, const char *key, size_t len);
ZEND_API zval* ZEND_FASTCALL zend_hash_index_find(const HashTable *ht, zend_ulong h);
ZEND_API zval* ZEND_FASTCALL _zend_hash_index_find(const HashTable *ht, zend_ulong h);

当然,PHP7也保存了间接经过char* 查找的zend_hash_str_find API,这关于一些只有char*的场景,能够防止天生zend_string的内存开支,也是颇有用的。

另外,咱们还做了很多进一步的优化:

Packed array

关于字符串key的数组来讲, zend_array正在arHash中保留了Hash值到arData的对应,有同窗可能会猎奇怎样不正在zend_array中看到arHash? 这是由于arHash以及arData是一次调配的:

HashTable Data Layout
=====================
 
         +=============================+
pointer->| HT_HASH(ht, ht->nTableMask) |
         | ...                         |
         | HT_HASH(ht, -1)             |
         +-----------------------------+
arData ->| Bucket[0]                   |
         | ...                         |
         | Bucket[ht->nTableSize-1]    |
         +=============================+

如图,现实上arData是一块调配的内存的两头局部,调配的内存真实的肇始地位实际上是pointer,而arData是较量争论过的一处两头地位,这样就能够用一个指针来表白俩个地位,辨别经过先后偏偏移来猎取地位, 比方-1对应的是arHash[0], 这个技术正在PHP7的进程中咱们也年夜量使用了,比方由于zend_object是变长的,以是不克不及正在他前面有其余元素,为了完成一些自界说的object,那末咱们会正在zend_object后面调配自界说的元素等等。

而关于全副是数字key的数组,arHash就显患上没那末须要了,以是此时咱们就用了一种新的数组, packed array来优化这个场景。

关于HASH_FLAG_PACKED的数组(标记正在zend_array->u.flags)中,它们是只有延续数字key的数组,它们没有需求Hash值来映照,以是这样的数组读取的时分,就相称于你间接拜访C数组,间接依据偏偏移来猎取zval.

<?php
echo "Packed array:\n";
$begin = memory_get_usage();
$array = range(0, 10000);
echo "Memory: ", memory_get_usage() - $begin, " bytes\n";
$begin = memory_get_usage();
$array[10001] = 1;
echo "Memory Increased: ", memory_get_usage() - $begin, " bytes\n";
 
$start = microtime(true);
for ($i = 0; $i < 10000; $i++) {
    $array[$i];
}
echo "Time: ", (microtime(true) - $start) * 1000 , " ms\n";
 
unset($array);
 
echo "\nMixed array:\n";
$begin = memory_get_usage();
$array = range(0, 10000);
echo "Memory: ", memory_get_usage() - $begin, " bytes\n";
$begin = memory_get_usage();
$array["foo"] = 1;
echo "Memory Increased: ", memory_get_usage() - $begin, " bytes\n";
 
$start = microtime(true);
for ($i = 0; $i < 10000; $i++) {
    $array[$i];
}
echo "Time: ", (microtime(true) - $start) * 1000 ," ms\n";

如图所示的简略测试,正在我的机械上输入以下(请留意,这个测试的局部后果可能会受你的机械,包罗装了甚么扩大相干,以是记患上要-n):

$ /home/huixinchen/local/php74/bin/php -n /tmp/1.php
Packed array:
Memory: 528480 bytes
Memory Increased: 0 bytes
Time: 0.49519538879395 ms
 
Mixed array:
Memory: 528480 bytes
Memory Increased: 131072 bytes
Time: 0.63300132751465 ms

能够看到, 当咱们应用$array[“foo”]=1, 强制一个数组从PACKED ARRAY变为一个Mixed Array当前,内存增进很显著,这局部是由于需求为10000个arHash调配内存。
而经过Index遍历的耗时,Packed Array仅仅是Mixed Array的78%。

Static key array

关于字符串array来讲, destructor的时分是需求开释字符串key的, 数组copy的时分也要添加key的计数,然而假如一切的key都是INTERNED字符串,那现实上咱们没有需求管这些了。于是就有了这个HASH_FLAG_STATIC_KEYS。

Empty array

咱们剖析发现,正在实际应用中,有年夜量的空数组,针对这些, 咱们正在初始化数组的时分,假如没有非凡声明,默许是没有会调配arData的,此时会把数组标记为HASH_FLAG_UNINITIALIZED, 只有当发作实际的写入的时分,才会调配arData。

I妹妹utable array

相似于INTERNED STRING,PHP7中咱们也引入了一种Imuutable array, 标记正在array->gc.flags中的IS_ARRAY_IMMUTABLE, 各人能够了解为不成更改的数组,关于这类数组,没有会发作COW,没有需求计数,这个也会极年夜的进步这类数据的操作功能,我的Yaconf中也年夜量使用了这类数据特点。

SIMD

起初的PHP7的版本中,我完成了一套SIMD指令集优化的框架,比方SIMD的base64_encode, 而正在HashTable的初始化中,咱们也使用了局部这样的指令集(此处使用尽管很巨大,但有须要提一下):

ifdef __SSE2__
        do {
            __m128i x妹妹0 = _妹妹_setzero_si128();
            x妹妹0 = _妹妹_cmpeq_epi8(x妹妹0, x妹妹0);
            _妹妹_storeu_si128((__m128i*)&HT_HASH_EX(data,  0), x妹妹0);
            _妹妹_storeu_si128((__m128i*)&HT_HASH_EX(data,  4), x妹妹0);
            _妹妹_storeu_si128((__m128i*)&HT_HASH_EX(data,  8), x妹妹0);
            _妹妹_storeu_si128((__m128i*)&HT_HASH_EX(data, 12), x妹妹0);
        } while (0);
#else
        HT_HASH_EX(data,  0) = -1;
        HT_HASH_EX(data,  1) = -1;
        HT_HASH_EX(data,  2) = -1;
        HT_HASH_EX(data,  3) = -1;
        HT_HASH_EX(data,  4) = -1;
        HT_HASH_EX(data,  5) = -1;
        HT_HASH_EX(data,  6) = -1;
        HT_HASH_EX(data,  7) = -1;
        HT_HASH_EX(data,  8) = -1;
        HT_HASH_EX(data,  9) = -1;
        HT_HASH_EX(data, 10) = -1;
        HT_HASH_EX(data, 11) = -1;
        HT_HASH_EX(data, 12) = -1;
        HT_HASH_EX(data, 13) = -1;
        HT_HASH_EX(data, 14) = -1;
        HT_HASH_EX(data, 15) = -1;
#endif

存正在的成绩

正在完成zend_array交换HashTable中咱们遇到了不少的成绩,绝年夜部份它们都被处理了,但遗留了一个成绩,由于如今arData是延续调配的,那末当数组增进巨细到需求扩容到时分,咱们只能从新realloc内存,但零碎其实不保障你realloc当前,地点没有会发作变动,那末就有可能:

<?php
$array = range(0, 7);
 
set_error_handler(function($err, $msg) {
    global $array;
    $array[] = 1; //force resize;
});
 
function crash() {
    global $array;
    $array[0] += $var; //undefined notice
}
 
crash();

比方下面的例子, 起首是一个全局数组,而后正在函数crash中, 正在+= opcode handler中,zend vm会起首猎取array[0]的内容,而后+$var, 但var是undefined variable, 以是此时会触发一个不决义变量的notice,而同时咱们设置了error_handler, 正在此中咱们给这个数组添加了一个元素, 由于PHP中的数组依照2^n的空间事后请求,此时数组满了,需求resize,于是发作了realloc,从error_handler前往当前,array[0]指向的内存就可能发作了变动,此时会呈现内存读写谬误,乃至segfault,有兴味的同窗,能够测验考试用valgrind跑这个例子看看。

但这个成绩的触发前提比拟多,修复需求额定对数据构造,或许需求拆分add_assign对功能会有影响,另外绝年夜局部状况下由于数组的事后调配战略存正在,和其余年夜局部多opcode handler读写操作根本都很邻近,这个成绩其实很难被实际代码触发,以是这个成绩不断悬停着。

保举教程:《php教程》

以上就是一同学习PHP7内核之HashTable的具体内容,更多请存眷资源魔其它相干文章!

标签: PHP应用 php7开发教程 php7开发资料 php7开发自学

抱歉,评论功能暂时关闭!