2018年12月25日

PHP如何衡量程式記憶體用量 - memory_get_usage 與 real_usage

How to measure memory usage in PHP script

前言

PHP主要有兩個內建函式可以用來取得程式的記憶體用量,它們分別是 memory_get_usagememory_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 系列