天天看點

四十七、PHP核心探索:引用計數與寫時複制 ☞ 引用計數是節省記憶體的一個超棒的模式

對于PHP這種需要同時處理多個請求的程式來說,申請和釋放記憶體的時候應該慎之又慎,一不小心便會釀成大錯。另一方面,除了要安全的申請和釋放記憶體外,還應該做到記憶體的最小化使用,因為它可能要處理每秒鐘數以千計的請求,為了提高系統整體的性能,每一次操作都應該隻使用最少的記憶體,對于不必要的相同資料的複制則應該能免則免。我們來看下面這段PHP代碼:

<?php
$a = 'Hello NowaMagic!';
$b = $a;
unset($a);  
?>      

第一條語句執行後,PHP建立了$a這個變量,并為它申請了12B的記憶體來存放"hello world"這個字元串(最後加個NULL字元,你懂的)。緊接着把$a賦給了$b,并釋放掉$a;

對于PHP來說,如果每一次變量指派都執行一次記憶體複制的話,那需要額外申請12B的記憶體來存放這個重複的資料,當然為了複制記憶體,還需要cpu執行某些計算,這當然會加重cpu的負載。當第三句執行後,$a被釋放了,我們剛才的設想突然變的這麼滑稽,這次指派顯得好多餘哦。如果早就知道$a不用了,那我們直接讓$b用$a的記憶體不就行了,還指派幹嘛?如果你覺得12B沒什麼,那設想下如果$a是個10M的檔案内容,或者20M,是不是我們的計算機資源消耗的有點冤枉呢?

别擔心,PHP很聰明!

前面說過,PHP變量的名稱和值在核心中是儲存在兩個不同的地方的,值是通過一個與名字毫無關系的zval結構來儲存,而這個變量的名字a則儲存在符号表裡,兩者之間通過指針聯系着。在我們上面的例子裡,$a是一個字元串,我們通過zend_hash_add把它添加到符号表裡,然後又把它指派給$b,兩者擁有相同的内容!如果兩者指向完全相同的内容,我們有什麼優化措施嗎?

zval *helloval;
  MAKE_STD_ZVAL(helloval);
  ZVAL_STRING(helloval, "Hello NowaMagic!", 1);
  zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL);
  zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval, sizeof(zval*), NULL);
  //通過這個例子我們看出了,我們可以把$a和$b都指向helloval~!      

現在我們檢查$a和$b兩個變量,他們的值指向了"Hello NowaMagic!"這個字元串在記憶體中的位置。但是在第三行:unset($a);這條語句釋放了$a。在這種情況下,unset函數并不知道$a的值同時被$b用着,是以如果它直接釋放記憶體,則會導緻$b的值也被清空了,進而導緻邏輯錯誤,甚至可能會導緻系統崩潰。

呵呵,其實你心裡明白,PHP不會讓上述問題發生的!回顧一下zval的四個成員value、type、is_ref__gc、refcount__gc,我們對value和type已經很熟了,現在則是後兩個成員發揮威力的時候了,這裡我們主要講解refcount__gc這個成員。當一個變量被第一次建立的時候,它對應的zval結構體的refcount__gc成員的值會被初始化為1,理由很簡單,因為隻有這個變量自己在用它。但是當你把這個變量指派給别的變量時,refcount__gc屬性便會加1變成2,因為現在有兩個變量在用這個zval結構了!

以上描述轉為核心中的代碼大體如下:

zval *helloval;
  MAKE_STD_ZVAL(helloval);
  ZVAL_STRING(helloval, "Hello World", 1);
  zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL);
  ZVAL_ADDREF(helloval); //這句很特殊,我們顯示的增加了helloval結構體的refcount
  zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval, sizeof(zval*), NULL);      

這個時候當我們再用unset删除$a的時候,它删除符号表裡的$a的資訊,然後清理它的值部分,這時它發現$a的值對應的zval結構的refcount值是2,也就是有另外一個變量在一起用着這個zval,是以unset隻需把這個zval的refcount減去1就行了!

寫時複制機制

引用計數絕對是節省記憶體的一個超棒的模式!但是當我們修改$b的值,而且還需要繼續使用$a時,該怎麼辦呢?

$a = 1;
  $b = $a;
  $b += 5;      

從代碼邏輯來看,我們希望語句執行後$a仍然是1,而$b則需要變成6。我們知道在第二句完成後核心通過讓$a和$b共享一個zval結構來達到節省記憶體的目的,但是現在第三句來了,這時$b的改變應該怎樣在核心中實作呢?

答案非常簡單,核心首先檢視refcount__gc屬性,如果它大于1則為這個變化的變量從原zval結構中複制出一份新的專屬與$b的zval來,并改變其值。

zval *get_var_and_separate(char *varname, int varname_len TSRMLS_DC)
{
  zval **varval, *varcopy;
  if (zend_hash_find(EG(active_symbol_table),varname, varname_len + 1, (void**)&varval) == FAILURE)
  {
    /* 如果在符号表裡找不到這個變量則直接return */
    return NULL;
  }

  if ((*varval)->refcount < 2)
  { 
    //如果這個變量的zval部分的refcount小于2,代表沒有别的變量在用,return
    return *varval;
  }
  
  /* 否則,複制一份zval*的值 */
  MAKE_STD_ZVAL(varcopy);
  varcopy = *varval;
  
  /* Duplicate any allocated structures within the zval* */
  zval_copy_ctor(varcopy);

  /* 從符号表中删除原來的變量
   * This will decrease the refcount of varval in the process
   */
  zend_hash_del(EG(active_symbol_table), varname, varname_len + 1);

  /* 初始化新的zval的refcount,并在符号表中重新添加此變量資訊,并将其值與我們的新zval相關聯。*/
  varcopy->refcount = 1;
  varcopy->is_ref = 0;
  zend_hash_add(EG(active_symbol_table), varname, varname_len + 1,&varcopy, sizeof(zval*), NULL);
  
  /* 傳回新zval的位址 */
  return varcopy;
}      

現在$b變量擁有了自己的zval,并且可以自由的修改它的值了。

Change on Write 寫時複制

如果使用者在PHP腳本中顯式的讓一個變量引用另一個變量時,我們的核心是如何處理的呢?

$a = 1;
  $b = &$a;
  $b += 5;      

作為一個标準的PHP程式猿,我們都知道$a的值也變成6了。當我們更改$b的值時,核心發現$b是$a的一個使用者端引用,也就是所它可以直接改變$b對應的zval的值,而無需再為它生成一個新的不同與$a的zval。因為他知道$a和$b都想得到這次變化!

但是核心是怎麼知道這一切的呢?簡單的講,它是通過zval的is_ref__gc成員來擷取這些資訊的。這個成員隻有兩個值,就像開關的開與關一樣。它的這兩個狀态代表着它是否是一個使用者在PHP語言中定義的引用。在第一條語句($a = 1;)執行完畢後,$a對應的zval的refcount__gc等于1,is_ref__gc等于0;。 當第二條語句執行後($b = &$a;),refcount__gc屬性向往常一樣增長為2,而且is_ref__gc屬性也同時變為了1!

最後,在執行第三條語句的時候,核心再次檢查$b的zval以确定是否需要複制出一份新的zval結構來,這次不需要複制,因為我們剛才上面的get_var_and_separate函數其實是個簡化版,并且少寫了一個條件:

/* 如果這個zval在php語言中是通過引用的形式存在的,或者它的refcount小于2,則不許要複制。*/

if ((*varval)->is_ref || (*varval)->refcount < 2) {

return *varval;

}

這一次,盡管它的refcount等于2,但是因為它的is_ref等于1,是以也不會被複制。核心會直接的修改這個zval的值。

Separation Anxiety

我們已經了解了php語言中變量的複制和引用的一些事,但是如果複制和引用這兩個事件被組合起來使用了該怎麼辦呢?看下面這段代碼:

$a = 1;
  $b = $a;
  $c = &$a;      

這裡我們可以看到,$a,$b,$c這三個變量現在共用一個zval結構,有兩個屬于change-on-write組合($a,$c),有兩個屬于copy-on-write組合($a,$b),我們的is_ref__gc和refcount__gc該怎樣工作,才能正确的處理好這段複雜的關系呢?

The answer is: 不可能!在這種情況下,變量的值必須分離成兩份完全獨立的存在!$a與$c共用一個zval,$b自己用一個zval,盡管他們擁有同樣的值,但是必須至少通過兩個zval來實作。見下圖【在引用時強制複制!】

​​

四十七、PHP核心探索:引用計數與寫時複制 ☞ 引用計數是節省記憶體的一個超棒的模式

​​

同樣,下面的這段代碼同樣會在核心中産生歧義,是以需要強制複制!

​​

四十七、PHP核心探索:引用計數與寫時複制 ☞ 引用計數是節省記憶體的一個超棒的模式

​​

//上圖對應的代碼
  $a = 1;
  $b = &$a;
  $c = $a;