PHP內核探索:寫時復制COW機制

在寫入時才真正復制一份內存進行修改
服務器君一共花費了227.810 ms進行了7次數據庫查詢,努力地為您提供了這個頁面。
試試閱讀模式?希望聽取您的建議

寫時復制(Copy-on-Write,也縮寫為COW),顧名思義,就是在寫入時才真正復制一份內存進行修改。 COW最早應用在*nix系統中對線程與內存使用的優化,后面廣泛的被使用在各種編程語言中,如C++的STL等。 在PHP內核中,COW也是主要的內存優化手段。 在前面關于變量和內存的討論中,引用計數對變量的銷毀與回收中起著至關重要的標識作用。 引用計數存在的意義,就是為了使得COW可以正常運作,從而實現對內存的優化使用。

寫時復制的作用

經過上面的描述,大家可能會COW有了個主觀的印象,下面讓我們看一個小例子, 非常容易看到COW在內存使用優化方面的明顯作用:

<?php
$j = 1;
var_dump(memory_get_usage());
 
$tipi = array_fill(0, 100000, 'php-internal');
var_dump(memory_get_usage());
 
$tipi_copy = $tipi;
var_dump(memory_get_usage());
 
foreach($tipi_copy as $i){
    $j += count($i); 
}
var_dump(memory_get_usage());
 
?>
//-----執行結果-----
$ php t.php 
int(630904)
int(10479840)
int(10479944)
int(10480040)

上面的代碼比較典型的突出了COW的作用,在一個數組變量$tipi被賦值給$tipi_copy時, 內存的使用并沒有立刻增加一半,甚至在循環遍歷數 $tipi_copy時, 實際上遍歷的,仍是$tipi指向的同一塊內存。

也就是說,即使我們不使用引用,一個變量被賦值后,只要我們不改變變量的值 ,也與使用引用一樣。 進一步講,就算變量的值立刻被改變,新值的內存分配也會洽如其分。 據此我們很容易就可以想到一些COW可以非常有效的控制內存使用的場景, 如函數參數的傳遞,大數組的復制等等。

在這個例子中,如果$tipi_copy的值發生了變化,$tipi的值是不應該發生變化的, 那么,此時PHP內核又會如何去做呢?我們引入下面的示例:

<?php
//$tipi = array_fill(0, 3, 'php-internal');  
//這里不再使用array_fill來填充 ,為什么?
$tipi[0] = 'php-internal';
$tipi[1] = 'php-internal';
$tipi[2] = 'php-internal';
var_dump(memory_get_usage());
 
$copy = $tipi;
xdebug_debug_zval('tipi', 'copy');
var_dump(memory_get_usage());
 
$copy[0] = 'php-internal';
xdebug_debug_zval('tipi', 'copy');
var_dump(memory_get_usage());
 
?>
//-----執行結果-----
$ php t.php 
int(629384)
tipi: (refcount=2, is_ref=0)=array (0 => (refcount=1, is_ref=0)='php-internal', 1 => (refcount=1, is_ref=0)='php-internal', 2 => (refcount=1, is_ref=0)='php-internal')
copy: (refcount=2, is_ref=0)=array (0 => (refcount=1, is_ref=0)='php-internal', 1 => (refcount=1, is_ref=0)='php-internal', 2 => (refcount=1, is_ref=0)='php-internal')
int(629512)
tipi: (refcount=1, is_ref=0)=array (0 => (refcount=1, is_ref=0)='php-internal', 1 => (refcount=2, is_ref=0)='php-internal', 2 => (refcount=2, is_ref=0)='php-internal')
copy: (refcount=1, is_ref=0)=array (0 => (refcount=1, is_ref=0)='php-internal', 1 => (refcount=2, is_ref=0)='php-internal', 2 => (refcount=2, is_ref=0)='php-internal')
int(630088)

從上面例子我們可以看出,當一個數組整個被賦給一個變量時,只是將內存將內存地址賦值給變量。 當數組的值被改變時,Zend內核重新申請了一塊內存,然后賦之以新值,但不影響其他值的內存狀態。 寫時復制的最小粒度,就是zval結構體, 而對于zval結構體組成的集合(如數組和對象等),在需要復制內存時,將復雜對象分解為最小粒度來處理。 這樣做就使內存中復雜對象中某一部分做修改時,不必將該對象的所有元素全部“分離”出一份內存拷貝, 從而節省了內存的使用。

寫時復制的實現

由于內存塊沒有辦法標識自己被幾個指針同時使用, 僅僅通過內存本身并沒有辦法知道什么時候應該進行復制工作, 這樣就需要一個變量來標識這塊內存是“被多少個變量名指針同時指向的”, 這個變量,就是前面關于變量的章節提到的:引用計數。

這里有一個比較典型的例子:

<?php
    $foo = 1;
    xdebug_debug_zval('foo');
    $bar = $foo;
    xdebug_debug_zval('foo');
    $bar = 2;
    xdebug_debug_zval('foo');   
?>
//-----執行結果-----
foo: (refcount=1, is_ref=0)=1
foo: (refcount=2, is_ref=0)=1
foo: (refcount=1, is_ref=0)=1

經過前文對變量的章節,我們可以理解當$foo被賦值時,$foo變量的引用計數為1。 當$foo的值被賦給$bar時,PHP并沒有將內存直接復制一份交給$bar, 而是直接把$foo和$bar指向同一個地址。這時,我們可以看到refcount=2; 最后,我們更改了$bar的值,這時如果兩個變量再指向同一個內存地址的話, 其值就會同時改變,于是,PHP內核這時將內存復制出來一份,并將其值寫為2 ,(這個操作也稱為分離操作), 同時維護原$foo變量的引用計數:refcount=1。

上面小例子中的xdebug_debug_zval()是xdebug擴展中的一個函數,用于輸出變量在zend內部的引用信息。 如果你沒有安裝xdebug擴展,也可以使用debug_zval_dump()來代替。 參考:http://www.php.net/manual/zh/function.debug-zval-dump.php

寫時復制應用的場景很多,最常見是賦值和函數傳參。 在上面的例子中,就使用了zend_assign_to_variable()函數(Zend/zend_execute.c) 對變量的賦值進行了各種判斷和處理。 其中最終處理代碼如下:

if (PZVAL_IS_REF(value) && Z_REFCOUNT_P(value) > 0) {
    ALLOC_ZVAL(variable_ptr);
    *variable_ptr_ptr = variable_ptr;
    *variable_ptr = *value;
    Z_SET_REFCOUNT_P(variable_ptr, 1);
    zval_copy_ctor(variable_ptr);
} else {
    *variable_ptr_ptr = value;
    Z_ADDREF_P(value);
}

從這段代碼可以看出,如果要進行操作的值已經是引用類型(如已經被&操作符操作過), 則直接重新分配內存,否則只是將value的地址賦與變量,同時將值的zval_value.refcount進行加1操作。

如果大家看過前面的章節, 應該對變量存儲的結構體zval(Zend/zend.h)還有印象:

typedef struct _zval_struct zval;
...
struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* value */
    zend_uint refcount__gc;
    zend_uchar type;    /* active type */
    zend_uchar is_ref__gc;
};

PHP對值的寫時復制的操作,主要依賴于兩個參數:refcount__gc與is_ref__gc。 如果是引用類型,則直接進行“分離”操作,即時分配內存, 否則會寫時復制,也就是在修改其值的時候才進行內存的重新分配。

寫時復制的規則比較繁瑣,什么情況會導致寫時復制及分離,是有非常多種情況的。 在這里只是舉一個簡單的例子幫助大家理解,后續會在附錄中列舉PHP中所有寫時復制的相關規則。

寫時復制的矛盾,PHP中不推薦使用&操作符的部分解釋

上面是一個比較典型的例子,但現實中的PHP實現經過各種權衡, 甚至有時對一個特性的支持與否,是互相矛盾且難以取舍的。 比如,unset()看上去是用來把變量釋放,然后把內存標記于空閑的。 可是,在下面的例子中,unset并沒有使內存池的內存增加:

<?php
$nowamagic = 10;
$o_o  = &$nowamagic;
unset($o_o);
echo $nowamagic;
?>

理論上$o_o是$nowamagic的引用,這兩者應該指向同一塊內存,其中一個被標識為回收, 另一個也應該被回收才是。但這是不可能的,因為內存本身并不知道都有哪些指針 指向了自已。在C中,o_o這時的值應該是無法預料的, 但PHP不想把這種維護變量引用的工作交給用戶,于是, 使用了折中的方法,unset()此時只會把nowamagic變量名從hashtable中去掉, 而內存值的引用計數減1。實際的內存使用完全沒有變化。

試想,如果$nowamagic是一個非常大的數組,或者是一個資源型的變量。 這種情形絕對是我們不想看到的。

上面這個例子我們還可以理解,如果每個這種類似操作都要用戶來關心。 那PHP就是變換了語法的C了。而下面的這個例子,與其說是語言特性, 倒不如說是更像BUG多一些。(事實上對此在PHP官方的郵件組里有也爭論)

<?php
$foo ['love'] = 1;
$bar  = &$foo['love'];
$tipi = $foo;
$tipi['love'] = '2';
echo $foo['love'];
?>

這個例子最后會輸出 2 , 大家會非常驚訝于$nowamagic怎么會影響到$foo, 這完全是兩個不同的變量么!至少我們希望是這樣。

最后,不推薦大家使用 & ,讓PHP自己決定什么時候該使用引用好了, 除非你知道自己在做什么。

延伸閱讀

此文章所在專題列表如下:

  1. PHP內核探索:從SAPI接口開始
  2. PHP內核探索:一次請求的開始與結束
  3. PHP內核探索:一次請求生命周期
  4. PHP內核探索:單進程SAPI生命周期
  5. PHP內核探索:多進程/線程的SAPI生命周期
  6. PHP內核探索:Zend引擎
  7. PHP內核探索:再次探討SAPI
  8. PHP內核探索:Apache模塊介紹
  9. PHP內核探索:通過mod_php5支持PHP
  10. PHP內核探索:Apache運行與鉤子函數
  11. PHP內核探索:嵌入式PHP
  12. PHP內核探索:PHP的FastCGI
  13. PHP內核探索:如何執行PHP腳本
  14. PHP內核探索:PHP腳本的執行細節
  15. PHP內核探索:操作碼OpCode
  16. PHP內核探索:PHP里的opcode
  17. PHP內核探索:解釋器的執行過程
  18. PHP內核探索:變量概述
  19. PHP內核探索:變量存儲與類型
  20. PHP內核探索:PHP中的哈希表
  21. PHP內核探索:理解Zend里的哈希表
  22. PHP內核探索:PHP哈希算法設計
  23. PHP內核探索:翻譯一篇HashTables文章
  24. PHP內核探索:哈希碰撞攻擊是什么?
  25. PHP內核探索:常量的實現
  26. PHP內核探索:變量的存儲
  27. PHP內核探索:變量的類型
  28. PHP內核探索:變量的值操作
  29. PHP內核探索:變量的創建
  30. PHP內核探索:預定義變量
  31. PHP內核探索:變量的檢索
  32. PHP內核探索:變量的類型轉換
  33. PHP內核探索:弱類型變量的實現
  34. PHP內核探索:靜態變量的實現
  35. PHP內核探索:變量類型提示
  36. PHP內核探索:變量的生命周期
  37. PHP內核探索:變量賦值與銷毀
  38. PHP內核探索:變量作用域
  39. PHP內核探索:詭異的變量名
  40. PHP內核探索:變量的value和type存儲
  41. PHP內核探索:全局變量Global
  42. PHP內核探索:變量類型的轉換
  43. PHP內核探索:內存管理開篇
  44. PHP內核探索:Zend內存管理器
  45. PHP內核探索:PHP的內存管理
  46. PHP內核探索:內存的申請與銷毀
  47. PHP內核探索:引用計數與寫時復制
  48. PHP內核探索:PHP5.3的垃圾回收機制
  49. PHP內核探索:內存管理中的cache
  50. PHP內核探索:寫時復制COW機制
  51. PHP內核探索:數組與鏈表
  52. PHP內核探索:使用哈希表API
  53. PHP內核探索:數組操作
  54. PHP內核探索:數組源碼分析
  55. PHP內核探索:函數的分類
  56. PHP內核探索:函數的內部結構
  57. PHP內核探索:函數結構轉換
  58. PHP內核探索:定義函數的過程
  59. PHP內核探索:函數的參數
  60. PHP內核探索:zend_parse_parameters函數
  61. PHP內核探索:函數返回值
  62. PHP內核探索:形參return value
  63. PHP內核探索:函數調用與執行
  64. PHP內核探索:引用與函數執行
  65. PHP內核探索:匿名函數及閉包
  66. PHP內核探索:面向對象開篇
  67. PHP內核探索:類的結構和實現
  68. PHP內核探索:類的成員變量
  69. PHP內核探索:類的成員方法
  70. PHP內核探索:類的原型zend_class_entry
  71. PHP內核探索:類的定義
  72. PHP內核探索:訪問控制
  73. PHP內核探索:繼承,多態與抽象類
  74. PHP內核探索:魔術函數與延遲綁定
  75. PHP內核探索:保留類與特殊類
  76. PHP內核探索:對象
  77. PHP內核探索:創建對象實例
  78. PHP內核探索:對象屬性讀寫
  79. PHP內核探索:命名空間
  80. PHP內核探索:定義接口
  81. PHP內核探索:繼承與實現接口
  82. PHP內核探索:資源resource類型
  83. PHP內核探索:Zend虛擬機
  84. PHP內核探索:虛擬機的詞法解析
  85. PHP內核探索:虛擬機的語法分析
  86. PHP內核探索:中間代碼opcode的執行
  87. PHP內核探索:代碼的加密與解密
  88. PHP內核探索:zend_execute的具體執行過程
  89. PHP內核探索:變量的引用與計數規則
  90. PHP內核探索:新垃圾回收機制說明

本文地址:http://www.824886.live/librarys/veda/detail/1454,歡迎訪問原出處。

不打個分嗎?

轉載隨意,但請帶上本文地址:

http://www.824886.live/librarys/veda/detail/1454

如果你認為這篇文章值得更多人閱讀,歡迎使用下面的分享功能。
小提示:您可以按快捷鍵 Ctrl + D,或點此 加入收藏。

大家都在看

閱讀一百本計算機著作吧,少年

很多人覺得自己技術進步很慢,學習效率低,我覺得一個重要原因是看的書少了。多少是多呢?起碼得看3、4、5、6米吧。給個具體的數量,那就100本書吧。很多人知識結構不好而且不系統,因為在特定領域有一個足夠量的知識量+足夠良好的知識結構,系統化以后就足以應對大量未曾遇到過的問題。

奉勸自學者:構建特定領域的知識結構體系的路徑中再也沒有比學習該專業的專業課程更好的了。如果我的知識結構體系足以囊括面試官的大部分甚至吞并他的知識結構體系的話,讀到他言語中的一個詞我們就已經知道他要表達什么,我們可以讓他坐“上位”畢竟他是面試官,但是在知識結構體系以及心理上我們就居高臨下。

所以,閱讀一百本計算機著作吧,少年!

《致加西亞的信》 阿爾伯特·哈伯德(Hubbard.E.) (作者), 趙立光 (譯者), 艾柯 (譯者)

《致加西亞的信(經典盒裝版)》內容簡介:美西戰爭爆發以后,美國必須立即與古巴起義軍首領加西亞取得聯系,并獲得他的合作。但當時,加西亞身在古巴的深山里——沒有人知道他的確切地點,所以沒法與他取得聯系。這時,有人向總統推薦一個名叫羅文的人,說他有辦法找到加西亞,而且也只有他才能找得到。他們找來羅文,交給他一封寫給加西亞的信。三周后,羅文徒步走過一個危機四伏的國家,最終把那封信交給了加西亞。 此后,羅文的事跡被傳為佳話,“送信”成為了敬業、忠誠、勤奮的象征,羅文便成了每個領導都想找到的人和每個員工都應該學習和效仿的榜樣。

更多計算機寶庫...

云南快乐十分走势一定牛 体育彩票7星彩开奖结果 幸运赛车走势图及奖金 中小盘股票推荐 2020体育彩票开奖结果 海南体彩4十 规律 网赌北京快乐8有输的人吗 股票配资平台 一直牛 甘肃11选五下期推荐 浙江省体彩十一选五开奖结果 全国开奖彩票开奖详情