PHP核心探索:寫時複製COW機制
寫時複製(Copy-on-Write,也縮寫為COW),顧名思義,就是在寫入時才真正複製一份記憶體進行修改。 COW最早應用在*nix系統中對執行緒與記憶體使用的優化,後面廣泛的被使用在各種程式語言中,如C++的STL等。 在PHP核心中,COW也是主要的記憶體優化手段。 在前面關於變數和記憶體的討論中,引用計數對變數的銷燬與回收中起著至關重要的標識作用。 引用計數存在的意義,就是為了使得COW可以正常運作,從而實現對記憶體的優化使用。
寫時複製的作用
經過上面的描述,大家可能會COW有了個主觀的印象,下面讓我們看一個小例子, 非常容易看到COW在記憶體使用優化方面的明顯作用:
<span style="font-size:14px;">$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)</span>
上面的程式碼比較典型的突出了COW的作用,在一個陣列變數$tipi被賦值給$tipi_copy時, 記憶體的使用並沒有立刻增加一半,甚至在迴圈遍歷數 $tipi_copy時, 實際上遍歷的,仍是$tipi指向的同一塊記憶體。
也就是說,即使我們不使用引用,一個變數被賦值後,只要我們不改變變數的值 ,也與使用引用一樣。 進一步講,就算變數的值立刻被改變,新值的記憶體分配也會洽如其分。 據此我們很容易就可以想到一些COW可以非常有效的控制記憶體使用的場景, 如函式引數的傳遞,大陣列的複製等等。
在這個例子中,如果$tipi_copy的值發生了變化,$tipi的值是不應該發生變化的, 那麼,此時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結構體組成的集合(如陣列和物件等),在需要複製記憶體時,將複雜物件分解為最小粒度來處理。 這樣做就使記憶體中複雜物件中某一部分做修改時,不必將該物件的所有元素全部“分離”出一份記憶體拷貝, 從而節省了記憶體的使用。
寫時複製的實現
由於記憶體塊沒有辦法標識自己被幾個指標同時使用, 僅僅通過記憶體本身並沒有辦法知道什麼時候應該進行復制工作, 這樣就需要一個變數來標識這塊記憶體是“被多少個變數名指標同時指向的”, 這個變數,就是前面關於變數的章節提到的:引用計數。
這裡有一個比較典型的例子:
$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自己決定什麼時候該使用引用好了, 除非你知道自己在做什麼。
相關文章
- Java 中的寫時複製 (Copy on Write, COW)Java
- Linux--寫時複製(Copy-On-Write,COW)技術簡述Linux
- MongoDB 複製機制MongoDB
- Apache Kakfa複製機制Apache
- Redis系列(四):Redis的複製機制(主從複製)Redis
- COW奶牛!CopyOnWrite機制瞭解一下
- MongoDB學習4:MongoDB複製集機制和原理,搭建複製集MongoDB
- MySQL組複製(MGR)全解析 Part 3 組複製機制細節MySql
- COW奶牛!Copy On Write機制瞭解一下
- [PHP核心探索]PHP中的雜湊表PHP
- 一文讀懂MySQL複製機制MySql
- MongoDB原理:複製集狀態同步機制MongoDB
- PHP核心探索之PHP中的雜湊表PHP
- 深入分析Redis的主從複製機制Redis
- 探索JavaScript的this機制JavaScript
- 映象分層原理及容器層寫時複製
- Redis持久化之父子程式與寫時複製Redis持久化
- 一文讀懂MySql主從複製機制MySql
- redis安裝,主從複製,哨兵機制,叢集Redis
- mongoDB研究筆記:複製集故障轉移機制MongoDB筆記
- mongoDB研究筆記:複製集資料同步機制MongoDB筆記
- ASP.Net請求處理機制初步探索之旅(2)核心ASP.NET
- requestAnimationFrame 執行機制探索requestAnimationFrame
- 探索JavaScript執行機制JavaScript
- 探索新技術機制
- Flutter 瀑布流如何仿寫原生的複用機制?Flutter
- PHP 鎖機制PHP
- Java核心反射機制Java反射
- 核心同步機制 RCU
- 每天一個 PHP 語法五引用計數與寫時複製的實現PHP
- PostgreSQL複製槽相關機制在各版本調整SQL
- 圖解Redis,Redis主從複製與Redis哨兵機制圖解Redis
- 自動化HDFS資料複製機制的簡單方法!
- MySQL 8 複製(九)——組複製聯機配置MySql
- 二進位制檔案複製
- Dubbo的微核心機制
- ETCD核心機制解析
- 概述javascript部分核心機制JavaScript