2021年10月22日

PHP 節省 Array 記憶體用量的神秘機制:Copy on write

本篇為PHP系列

Copy-on-write 是一種用來的節省記憶體用量的機制,

在很多地方都有使用到,而 PHP 的 Array 也有用到這樣的機制,

不過在開始正式討論 Array Copy-on-write 之前,我們先來稍微看一下 PHP Array 的複製機制。

PHP Array 複製

PHP Array 複製預設的做法是:copy by value

也就是值的複製,

將一個 array 複製到另外一個變數後,這兩個 array 的值都是一樣的,

而且修改其中任何一個,都不會影響到另外一個。

舉例來說,將$student1 assign 給 $student2後,兩個變數所包含的值,都是一樣的:

$students1 = ["王小明", "陳阿美", "周小弟"];
$students2 = $students1;

print_r($students1);
print_r($students2);

分別會印出一樣的結果:

Array
(
    [0] => 王小明
    [1] => 陳阿美
    [2] => 周小弟
)
Array
(
    [0] => 王小明
    [1] => 陳阿美
    [2] => 周小弟
)

接續著,我們修改$students2的值,

<?php
$students2[0] = "李小白";

print_r($students1);
print_r($students2);

會看到$student1維持不變,代表兩個是互不影響的:

Array
(
    [0] => 王小明  <---維持不變
    [1] => 陳阿美
    [2] => 周小弟
)
Array
(
    [0] => 李小白  <---改變
    [1] => 陳阿美
    [2] => 周小弟
)

記憶體用量

兩個變數若要完全不互相影響,代表兩個變數必須擁有獨立的記憶體空間,

讓兩個 Array 可以把各自所有的填入到自己的空間中。

按照這個邏輯來看,記憶體用量應該會是原本的兩倍

$students1$students2 都會各自使用一倍的記憶體,

但其實PHP在這邊有做一點小小的記憶體用量優化,並不一定會真的使用到兩倍,

我們來看下面的例子:

<?php
print_r("1. 程式開始執行: " . round(memory_get_usage()/1024) . " KB\n");

// 塞一萬個test進入$a
$a = array_fill(0, 10000, 'test');

print_r("2. 產生大Array後: " . round(memory_get_usage()/1024) . " KB\n");

// 複製$a到$b
$b = $a;

print_r("3. 複製大Array後: " . round(memory_get_usage()/1024) . " KB\n");

// 修改$b
$b[0] = 'ttttt';

print_r("4. 修改複製後的Array: " . round(memory_get_usage()/1024) . " KB\n");

輸出:

1. 程式開始執行: 388 KB
2. 產生大Array後: 904 KB
3. 複製大Array後: 904 KB
4. 修改複製後的Array: 1420 KB

大家可以發現第 2 個紀錄點及第 3 個紀錄點,雖然複製了,但顯示的記憶體用量還是一樣的,

直到第 4 個紀錄點,修改$b後,記憶體量才暴增。

再稍微看一下數字,

紀錄點 2 和紀錄點 1 的差距: 904 - 388 = 516。我們可以把這個 516KB 視為 $a 的大小。

紀錄點 4和紀錄點 3 的差距:1420 - 904 = 516。可以看出,新增的用量是 516KB ,

而這個 516KB 剛好等同於 $a 的大小,也就符合前面所推論的,array的複製需要新增一倍的空間。

關於 memory_get_usage 的用法可以參考我之前的文章:PHP如何衡量程式記憶體用量

Copy-on-write(COW),寫入時複製

PHP 在這邊採用的優化方式就叫做:Copy-on-Write。

當我們將一個變數的值複製給另外一個變數時,其實不會立刻做複製,

而是會透過指標的方式,讓兩個變數使用同一份記憶體空間,

直到真正需要將兩個變數的值區分開來的時候,才會複製所有的值,記憶體用量這個時候才會增加,

例如前面範例的 $b ,原本 $a$b 長得一模一樣,所以實際上是指向同一個記憶體空間,

直到修改了 $b,兩者的值不一樣了以後,PHP才自動複製另外一份出來。

 

順帶一提,如果繼續使用同一份記憶體空間,

那麼修改$b就會影響到$a,這樣就違反 copy by value 的原則了。

衍伸

$a 複製給 $b後,反過來修改 $a 也會是一樣的喔:

<?php
print_r("1. 程式開始執行: " . round(memory_get_usage()/1024) . " KB\n");

// 塞一萬個test進入$a
$a = array_fill(0, 10000, 'test');

print_r("2. 產生大Array後: " . round(memory_get_usage()/1024) . " KB\n");

// 複製$a到$b
$b = $a;

print_r("3. 複製大Array後: " . round(memory_get_usage()/1024) . " KB\n");

// 修改$a
$a[0] = 'ttttt';

print_r("4. 修改複製後的Array: " . round(memory_get_usage()/1024) . " KB\n");

結果:

1. 程式開始執行: 388 KB
2. 產生大Array後: 904 KB
3. 複製大Array後: 904 KB
4. 修改複製後的Array: 1420 KB

 

執行環境

PHP 8.0.11

 

參考

PHP Internals Book - Memory management

更多PHP相關文章,請參考:PHP系列