How to measure memory usage in PHP script
前言
PHP主要有兩個內建函式可以用來取得程式的記憶體用量,它們分別是 memory_get_usage
及 memory_get_peak_usage
。
本篇將會介紹 memory_get_usage
的用法,以及簡述何謂 real usage。
說明
memory_get_usage()
首先先看看 memory_get_usage
的使用方式,根據 PHP Manual 的定義:
int memory_get_usage ([ bool $real_usage = FALSE ] )
此方法將回傳當下被分配到你的 PHP 腳本的記憶體量,單位是位元組 (Byte)。
範例
我們想要衡量一個簡單的小程式:
當一個陣列有 1000 個 element,且每個 element 都是 1 時,會佔用多少記憶體?
$start = memory_get_usage();
$temp = array();
for ($i = 0; $i < 1000; $i++) {
$temp[] = 1;
}
$end = memory_get_usage();
echo "程式開始時的記憶體用量: {$start} Bytes".PHP_EOL;
echo "程式結束時的記憶體用量: {$end} Bytes".PHP_EOL;
echo "我的程式使用了多少記憶體: ".($end - $start)." Bytes".PHP_EOL;
輸出
程式開始時的記憶體用量: 396768 Bytes
程式結束時的記憶體用量: 433688 Bytes
我的程式使用了多少記憶體: 36920 Bytes
計算記憶體使用量的概念很簡單,只要計算某段程式執行的前後,記憶體總用量的差值即可。也就是說 $end - $start
的結果,就是會 $temp 所佔用的記憶體量了。
因此從輸出中,我們可以知道一個有 1000 個 element 的陣列,也就是 $temp 這個變數,共佔用了 36,920 個位元組。
這邊或許有人會有疑問,為什麼程式一開始的記憶體用量不是 0,而是 396,768 位元組呢?原因是,雖然我們只有執行一小段程式,但是 PHP 要能夠執行,需要許多預先載入的函式庫或是一些全域變數,所以這 396,768 個位元組至少包含了那些預先載入的程式所佔用的部分囉!
memory_get_usage()與$real_usage
從定義中,我們知道 memory_get_usage
有個參數 $real_usage
可以使用,只是預設值是 false
。
那麼,什麼是$real_usage
呢?
事實上,PHP 從作業系統中取得記憶體,並不是目前需要多少就拿多少,而是會預先先取得一大區塊,再交由 PHP 內部自行管理這些區塊,這些預先取得的大區塊的總和就是 real usage,這些區塊的總和,對於作業系統來說,才是我們的 PHP 程式真正的記憶體佔用量。
但是這些預先取得的大區塊,不見得會被 PHP 真的使用完,我們的 PHP 對於記憶體的實際使用量 internal usage,不見得會等於 real usage。每個區塊就像一個貨櫃一樣,PHP 程式只是先跟作業系統要了一個大貨櫃,然後再慢慢把東西塞進去貨櫃裡面。
因此當 $real_usage 設定為:
true
代表 PHP 程式對於整個作業系統實際的記憶體佔用量。(real usage)false
代表 PHP 程式內部的變數實際上真正使用的記憶體使用量。(internal usage)
我們可以將前例的 memory_get_usage
第一個參數傳入 true 進行觀察:
$start = memory_get_usage(true);
$temp = array();
for ($i = 0; $i < 1000; $i++) {
$temp[] = 1;
}
$end = memory_get_usage(true);
echo "程式開始時的記憶體佔用量: {$start} Bytes".PHP_EOL;
echo "程式結束時的記憶體佔用量: {$end} Bytes".PHP_EOL;
echo "我的程式多佔用了多少記憶體: ".($end - $start)." Bytes".PHP_EOL;
輸出:
程式開始時的記憶體佔用量: 2097152 Bytes
程式結束時的記憶體佔用量: 2097152 Bytes
我的程式多佔用了多少記憶體: 0 Bytes
我們可以看到,程式一開始就從作業系統中佔用了 2,097,152 個位元組,程式結束後依舊是佔用了 2,097,152 個位元組,所以整個程式的生命週期中,實際上就是佔用了 2,097,152 個位元組,沒有增加!
而從第一個例子中我們也知道,1000 個 element 的陣列只會佔用了 36,920 位元組,完全用不完一開始跟作業系統要的 2,097,152 位元組的記憶體,因此才不需要再跟作業系統要求更多記憶體。
另外,由於 memory_get_usage
回傳的單位是 Byte,若要取得 KB 量,可以除以 1024
,若要取得 MB 量,則可以除以 1024 * 1024
。
memory_limit
知道了如何衡量記憶體用量,那我們就可以來限制記憶體用量了。
我們經常會在php.ini
中限制 PHP 程式記憶體用量的 memory_limit
memory_limit = 128M
或透過ini_set()
來設定
ini_set('memory_limit', '256M');
值得一提的是,這邊所限制的記憶體是 real_usage 哦!
我們可以從這個範例來看:將記憶體限制為 4MB,並測試寫入 1000000 個 Datetime 物件。
ini_set('memory_limit', '4M');
echo '[i]internal/real'.PHP_EOL;
$temp = array();
for ($i = 0; $i < 1000000; $i++) {
$temp[] = new Datetime();
echo "[$i]".memory_get_usage().'/'.memory_get_usage(true).PHP_EOL;
}
輸出
[i]internal/real
[0]404688/2097152
[1]405008/2097152
[2]405328/2097152
... 略 ...
[4092]1873552/2097152
[4093]1873872/2097152
[4094]1874192/2097152 <--- 記憶體要用完了
[4095]1907280/4194304 <--- 跟作業系統多要了一大區塊
[4096]2038672/4194304
[4097]2038992/4194304
... 略 ...
[9256]4017552/4194304
[9257]4017872/4194304
[9258]4018192/4194304 <--- 記憶體要用完了
Fatal error: Allowed memory size of 4194304 bytes exhausted
(tried to allocate 4096 bytes)
一開始我們可以看到 internal 記憶體用量隨著新的 Datetime 物件不斷被實體化出來而增加,但是 real 則不會。
當程式執行到 i = 4094
時,PHP 會發現記憶體快要用完了,原本跟作業系統要的區塊將不夠用,所以跟作業系統新要了一大區塊,而因為記憶體上限是 4MB,所以此時可以成功要到新的記憶體區塊。
但是當程式執行到 i = 9258
時,又遇到記憶體要用完的窘境,此時 PHP 試圖要去跟作業系統要求記憶體則發現因為已經超過 memory_limit = 4MB
的限制,無法要求成功。
結語
是否使用 real usage,端看開發人員目前的需求,若想要精準地知道目前程式執行片段,需要多少記憶體量(塞了多少貨物),那麼就得使用 internal usage,若想要知道系統分配了多少記憶體給這支程式(拿了幾個貨櫃),那就使用 real usage 吧。
下次有機會將會介紹memory_get_peak_usage。
本篇程式碼執行環境
- OS: Mac OS Mojave 10.14.2
- PHP: PHP 7.3.0
參考
看更多 → PHP 系列