久久久久久久视色,久久电影免费精品,中文亚洲欧美乱码在线观看,在线免费播放AV片

<center id="vfaef"><input id="vfaef"><table id="vfaef"></table></input></center>

    <p id="vfaef"><kbd id="vfaef"></kbd></p>

    
    
    <pre id="vfaef"><u id="vfaef"></u></pre>

      <thead id="vfaef"><input id="vfaef"></input></thead>

    1. 站長(zhǎng)資訊網(wǎng)
      最全最豐富的資訊網(wǎng)站

      一起學(xué)習(xí)PHP7內(nèi)核之HashTable

      一起學(xué)習(xí)PHP7內(nèi)核之HashTable

      之前的倆篇文章深入理解PHP7內(nèi)核之zval 和深入理解PHP7內(nèi)核之Reference, 我介紹了當(dāng)時(shí)在開發(fā)PHP7的時(shí)候?qū)val和reference的一些改造思考和結(jié)果, 之后因?yàn)榇_實(shí)精力有限就沒有繼續(xù)往下寫,時(shí)隔一年多以后,因?yàn)檫@場(chǎng)突如其來的疫情,在家辦公的時(shí)間很多, 于是終于有了時(shí)間讓我來繼續(xù)介紹一下PHP7的中Hashtable的變化, 以及當(dāng)時(shí)我們做這些變化背后的考量.

      PHP5

      一起學(xué)習(xí)PHP7內(nèi)核之HashTable

      對(duì)于PHP內(nèi)核一直有關(guān)注的同學(xué), 應(yīng)該對(duì)PHP5的Hashtable會(huì)比較熟悉, 但我們還是先來簡(jiǎn)單回顧一下PHP5的Hashtable:

      在PHP5的實(shí)現(xiàn)中, Hashtable的核心是存儲(chǔ)了一個(gè)個(gè)指向zval指針的指針, 也就是zval**(我遇到不少的同學(xué)問為什么是zval**, 而不是zval*, 這個(gè)原因其實(shí)很簡(jiǎn)單, 因?yàn)镠ashtable中的多個(gè)位置都可能指向同一個(gè)zval, 那么最常見的一個(gè)可能就是在COW的時(shí)候, 當(dāng)我們需要把一個(gè)變量指向一個(gè)新的zval的時(shí)候, 如果在符號(hào)表中存的是zval*, 那們我們就做不到對(duì)一處修改, 所有的持有方都有感知, 所以必須是zval**), 這樣的設(shè)計(jì)在最初的出發(fā)點(diǎn)是為了讓Hashtable可以存儲(chǔ)任何尺寸的任何信息, 不僅僅是指針, 還可以存儲(chǔ)一段內(nèi)存值(當(dāng)然實(shí)際上大部分情況下,比如符號(hào)表還是存的zval的指針).

      PHP5的代碼中也用了比較Hack的方式來判斷存儲(chǔ)的是什么:

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

      它來判斷存儲(chǔ)的size是不是一個(gè)指針大小, 從而采用不同的方式來更新存儲(chǔ)的內(nèi)容。非常Hack的方式。

      PHP5的Hashtable對(duì)于每一個(gè)Bucket都是分開申請(qǐng)釋放的。

      而存儲(chǔ)在Hashtable中的數(shù)據(jù)是也會(huì)通過pListNext指針串成一個(gè)list,可以直接遍歷,關(guān)于這塊可以參看我很早的一篇文章深入理解PHP之?dāng)?shù)組

      問題

      在寫PHP7的時(shí)候,我們?cè)敿?xì)思考了幾點(diǎn)可能優(yōu)化的點(diǎn),包括也從性能角度總結(jié)了以下目前這種實(shí)現(xiàn)的幾個(gè)問題:

      • Hashtable在PHP中,應(yīng)用最多的是保存各種zval, 而PHP5的HashTable設(shè)計(jì)的太通用,可以設(shè)計(jì)為專門為了存儲(chǔ)zval而優(yōu)化, 從而能減少內(nèi)存占用。
      • 2. 緩存局部性問題, 因?yàn)镻HP5的Hashtable的Bucket,包括zval都是獨(dú)立分配的,并且采用了List來串Hashtable中的所有元素,會(huì)導(dǎo)致在遍歷或者順序訪問一個(gè)數(shù)組的時(shí)候,緩存不友好。

        比如如圖所示在PHP代碼中常見的foreach一個(gè)數(shù)組, 就會(huì)發(fā)生多次的內(nèi)存跳躍.

      • 3. 和1類似,在PHP5的Hashtable中,要訪問一個(gè)zval,因?yàn)槭莦val**, 那需要至少解指針倆次,一方面是緩存不友好,另外一方面也是效率低下。
        比如上圖中,藍(lán)色框的部分,我們找到數(shù)組中的bucket以后,還需要解開zval**,才可以讀取到實(shí)際的zval的內(nèi)容。 也就是需要發(fā)生倆次內(nèi)存讀取。效率低下。

      當(dāng)然還有很多的其他的問題,此處不再贅述,說實(shí)在的畢竟倆年多了,當(dāng)時(shí)怎么想的,現(xiàn)在有些也想不起來了, 現(xiàn)在我們來看看PHP7的

      PHP7

      首先在PHP7中,我們當(dāng)時(shí)的考慮是可能因?yàn)閾?dān)心Hashtable用的太多,我們新設(shè)計(jì)的結(jié)構(gòu)體可能不一定能Cover所有的場(chǎng)景,于是我們新定義了一個(gè)結(jié)構(gòu)體叫做zend_array, 當(dāng)然最后經(jīng)過一系列的努力,發(fā)現(xiàn)zend_array可以完全替代Hashtable, 最后還是保留了Hashtable和zend_array倆個(gè)名字,只不過互為Alias.
      再下面的文章中,我會(huì)用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時(shí)代的Hashtable,zend_array的內(nèi)存占用從PHP5點(diǎn)72個(gè)字節(jié),降低到了56個(gè)字節(jié),想想一個(gè)PHP生命進(jìn)程中成千上萬(wàn)的數(shù)組,內(nèi)存降低明顯。

      此處再次特別說明下上面zend_array定義中的ZEND_ENDIAN_LOHT_4這個(gè)作用,這個(gè)是為了解決大小端問題,在大端小端上都能讓其中的元素保證同樣的內(nèi)存存儲(chǔ)順序,可以方便我們寫出通用的位操作。 在PHP7中,位操作應(yīng)用的很多,因?yàn)檫@樣一來一個(gè)字節(jié)就可以保存8個(gè)狀態(tài)位, 很節(jié)省內(nèi)存:)

      #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

      而數(shù)據(jù)會(huì)核心保存在arData中,arData是一個(gè)Bucket數(shù)組,Bucket定義:

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

      再對(duì)比看下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;

      內(nèi)存占用從72字節(jié),降低到了32個(gè)字節(jié),想想一個(gè)PHP進(jìn)程中幾十萬(wàn)的數(shù)組元素,這個(gè)內(nèi)存降低就更明顯了.

      對(duì)比的看,

      • 現(xiàn)在的沖突拉鏈被bauck.zval->u2.next替代, 于是bucket->pNext, bucket->pLast可以去掉了。
      • zend_array->arData是一個(gè)數(shù)組,所以也就不再需要pListNext, pListLast來保持順序了, 他們也可以去掉了。 現(xiàn)在數(shù)組中元素的先后順序,完全根據(jù)它在arData中的index順序決定,先加入的元素在低的index中。
      • PHP7中的Bucket現(xiàn)在直接保存一個(gè)zval, 取代了PHP5時(shí)代bucket中的pData和pDataPtr。
      • 最后就是PHP7中現(xiàn)在使用zend_string作為數(shù)組的字符串key,取代了PHP5時(shí)代bucket的*key, nKeylength.
      一起學(xué)習(xí)PHP7內(nèi)核之HashTable

      現(xiàn)在我們來整體看下zend_array的組織圖:

      回顧下深入理解PHP7內(nèi)核之ZVAL, 現(xiàn)在的zend_array就可以應(yīng)付各種場(chǎng)景下的HashTable的作用了。
      特別說明對(duì)是除了一個(gè)問題就是之前提到過的IS_INDIRECT, 不知道大家是否還記得. 上一篇我說過原來HashTable為什么要設(shè)計(jì)保存zval**, 那么現(xiàn)在因?yàn)開Bucket直接保存的是zval了,那怎么解決COW的時(shí)候一處修改多處可見的需求呢? IS_INDIRECT就應(yīng)用而生了,IS_INDIRECT類型其實(shí)可以理解為就是一個(gè)zval*的結(jié)構(gòu)體。它被廣泛應(yīng)用在GLOBALS,Properties等多個(gè)需要倆個(gè)HashTable指向同于一個(gè)ZVAL的場(chǎng)景。

      另外,對(duì)于原來一些擴(kuò)展會(huì)使用HashTable來保存一些自己的內(nèi)存,現(xiàn)在可以通過IS_PTR這種zval類型來實(shí)現(xiàn)。

      現(xiàn)在arData因?yàn)槭且粋€(gè)連續(xù)的數(shù)組了,那么當(dāng)foreach的時(shí)候,就可以順序訪問一塊連續(xù)的內(nèi)存,而現(xiàn)在zval直接保存在bucket中,那么在絕大部分情況下(不需要外部指針的內(nèi)容,比如long,bool之類的)就可以不需要任何額外的zval指針解引用了,緩存局部性友好,性能提升非常明顯。

      一起學(xué)習(xí)PHP7內(nèi)核之HashTable

      還有就是PHP5的時(shí)代,查找數(shù)組元素的時(shí)候,因?yàn)閭鬟f進(jìn)來的是char *key,所有需要每次查找都計(jì)算key的Hash值,而現(xiàn)在查找的時(shí)候傳遞進(jìn)來的key是zend_string, Hash值不需要重新計(jì)算,此處也有部分性能提升。

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

      當(dāng)然,PHP7也保留了直接通過char* 查找的zend_hash_str_find API,這對(duì)于一些只有char*的場(chǎng)景,可以避免生成zend_string的內(nèi)存開銷,也是很有用的。

      另外,我們還做了不少進(jìn)一步的優(yōu)化:

      Packed array

      對(duì)于字符串key的數(shù)組來說, zend_array在arHash中保存了Hash值到arData的對(duì)應(yīng),有同學(xué)可能會(huì)好奇怎么沒有在zend_array中看到arHash? 這是因?yàn)閍rHash和arData是一次分配的:

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

      如圖,事實(shí)上arData是一塊分配的內(nèi)存的中間部分,分配的內(nèi)存真正的起始位置其實(shí)是pointer,而arData是計(jì)算過的一處中間位置,這樣就可以用一個(gè)指針來表達(dá)倆個(gè)位置,分別通過前后偏移來獲取位置, 比如-1對(duì)應(yīng)的是arHash[0], 這個(gè)技巧在PHP7的過程中我們也大量應(yīng)用了,比如因?yàn)閦end_object是變長(zhǎng)的,所以不能在他后面有其他元素,為了實(shí)現(xiàn)一些自定義的object,那么我們會(huì)在zend_object前面分配自定義的元素等等。

      而對(duì)于全部是數(shù)字key的數(shù)組,arHash就顯得沒那么必要了,所以此時(shí)我們就用了一種新的數(shù)組, packed array來優(yōu)化這個(gè)場(chǎng)景。

      對(duì)于HASH_FLAG_PACKED的數(shù)組(標(biāo)志在zend_array->u.flags)中,它們是只有連續(xù)數(shù)字key的數(shù)組,它們不需要Hash值來映射,所以這樣的數(shù)組讀取的時(shí)候,就相當(dāng)于你直接訪問C數(shù)組,直接根據(jù)偏移來獲取zval.

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

      如圖所示的簡(jiǎn)單測(cè)試,在我的機(jī)器上輸出如下(請(qǐng)注意,這個(gè)測(cè)試的部分結(jié)果可能會(huì)受你的機(jī)器,包括裝了什么擴(kuò)展相關(guā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

      可以看到, 當(dāng)我們使用$array[“foo”]=1, 強(qiáng)迫一個(gè)數(shù)組從PACKED ARRAY變成一個(gè)Mixed Array以后,內(nèi)存增長(zhǎng)很明顯,這部分是因?yàn)樾枰獮?0000個(gè)arHash分配內(nèi)存。
      而通過Index遍歷的耗時(shí),Packed Array僅僅是Mixed Array的78%。

      Static key array

      對(duì)于字符串a(chǎn)rray來說, destructor的時(shí)候是需要釋放字符串key的, 數(shù)組copy的時(shí)候也要增加key的計(jì)數(shù),但是如果所有的key都是INTERNED字符串,那事實(shí)上我們不需要管這些了。于是就有了這個(gè)HASH_FLAG_STATIC_KEYS。

      Empty array

      我們分析發(fā)現(xiàn),在實(shí)際使用中,有大量的空數(shù)組,針對(duì)這些, 我們?cè)诔跏蓟瘮?shù)組的時(shí)候,如果不特殊申明,默認(rèn)是不會(huì)分配arData的,此時(shí)會(huì)把數(shù)組標(biāo)志為HASH_FLAG_UNINITIALIZED, 只有當(dāng)發(fā)生實(shí)際的寫入的時(shí)候,才會(huì)分配arData。

      Immutable array

      類似于INTERNED STRING,PHP7中我們也引入了一種Imuutable array, 標(biāo)志在array->gc.flags中的IS_ARRAY_IMMUTABLE, 大家可以理解為不可更改的數(shù)組,對(duì)于這種數(shù)組,不會(huì)發(fā)生COW,不需要計(jì)數(shù),這個(gè)也會(huì)極大的提高這種數(shù)據(jù)的操作性能,我的Yaconf中也大量應(yīng)用了這種數(shù)據(jù)特性。

      SIMD

      后來的PHP7的版本中,我實(shí)現(xiàn)了一套SIMD指令集優(yōu)化的框架,比如SIMD的base64_encode, 而在HashTable的初始化中,我們也應(yīng)用了部分這樣的指令集(此處應(yīng)用雖然很微小,但有必要提一下):

      ifdef __SSE2__         do {             __m128i xmm0 = _mm_setzero_si128();             xmm0 = _mm_cmpeq_epi8(xmm0, xmm0);             _mm_storeu_si128((__m128i*)&HT_HASH_EX(data,  0), xmm0);             _mm_storeu_si128((__m128i*)&HT_HASH_EX(data,  4), xmm0);             _mm_storeu_si128((__m128i*)&HT_HASH_EX(data,  8), xmm0);             _mm_storeu_si128((__m128i*)&HT_HASH_EX(data, 12), xmm0);         } 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

      存在的問題

      在實(shí)現(xiàn)zend_array替換HashTable中我們遇到了很多的問題,絕大部份它們都被解決了,但遺留了一個(gè)問題,因?yàn)楝F(xiàn)在arData是連續(xù)分配的,那么當(dāng)數(shù)組增長(zhǎng)大小到需要擴(kuò)容到時(shí)候,我們只能重新realloc內(nèi)存,但系統(tǒng)并不保證你realloc以后,地址不會(huì)發(fā)生變化,那么就有可能:

      <?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();

      比如上面的例子, 首先是一個(gè)全局?jǐn)?shù)組,然后在函數(shù)crash中, 在+= opcode handler中,zend vm會(huì)首先獲取array[0]的內(nèi)容,然后+$var, 但var是undefined variable, 所以此時(shí)會(huì)觸發(fā)一個(gè)未定義變量的notice,而同時(shí)我們?cè)O(shè)置了error_handler, 在其中我們給這個(gè)數(shù)組增加了一個(gè)元素, 因?yàn)镻HP中的數(shù)組按照2^n的空間預(yù)先申請(qǐng),此時(shí)數(shù)組滿了,需要resize,于是發(fā)生了realloc,從error_handler返回以后,array[0]指向的內(nèi)存就可能發(fā)生了變化,此時(shí)會(huì)出現(xiàn)內(nèi)存讀寫錯(cuò)誤,甚至segfault,有興趣的同學(xué),可以嘗試用valgrind跑這個(gè)例子看看。

      但這個(gè)問題的觸發(fā)條件比較多,修復(fù)需要額外對(duì)數(shù)據(jù)結(jié)構(gòu),或者需要拆分add_assign對(duì)性能會(huì)有影響,另外絕大部分情況下因?yàn)閿?shù)組的預(yù)先分配策略存在,以及其他大部分多opcode handler讀寫操作基本都很臨近,這個(gè)問題其實(shí)很難被實(shí)際代碼觸發(fā),所以這個(gè)問題一直懸停著。

      推薦教程:《php教程》

      贊(0)
      分享到: 更多 (0)
      網(wǎng)站地圖   滬ICP備18035694號(hào)-2    滬公網(wǎng)安備31011702889846號(hào)