プログラミング

プログラミング初心者のPHP修羅の道#19〜PHP8上級問題を徹底追求する〜

PHP8上級資格取得へ向けたアウトプット記事になります。

PHP試験運営団体より公開されている模擬試験を1つ1つみていく形で事細かくみていきますが、勉強途中ゆえ間違っている部分に関してはご了承ください。

PHP上級資格のメイン教材はPHPマニュアルと指定されていますので、このマニュアルを主に踏襲した内容となっています。

主な勉強教材は以下になります。

1.独習PHP第4版

2.はじめてのPHP

 

3.PHP公式マニュアル

 

PHP8上級試験模擬問題

試験問題21

 

PHPのメモリ消費 に関する説明の中で、誤っているものを1つ選びなさい。
なお「¥」はバックスラッシュに読み替えること。

 

選択肢①

 

PHPでは、「プログラムが動的に確保したメモリ領域のうち、不要になった領域を自動的に解放する」いわゆるガベージコレクションが機能として存在する。
PHPのガベージコレクションは「参照カウント法」という方式で管理されている。
参照された数は、Xdebugがインストール済みであれば  xdebug_debug_zval()関数によって得る事ができる。
そのため、以下のコード

declare(strict_types=1);
error_reporting(-1);

$obj = new stdClass();
xdebug_debug_zval('obj');
$obj2 = $obj;
xdebug_debug_zval('obj');
xdebug_debug_zval('obj2');
unset($obj);
xdebug_debug_zval('obj2');

は正しく実行でき、結果は

obj: (refcount=1, is_ref=0)=class stdClass {  }
obj: (refcount=2, is_ref=0)=class stdClass {  }
obj2: (refcount=2, is_ref=0)=class stdClass {  }
obj2: (refcount=1, is_ref=0)=class stdClass {  }

となる。

 

 

選択肢のコードでは、objというオブジェクトの参照カウントがobj2の参照により増加し、unset()によってobjの参照が解除された後に、obj2の参照カウントが 1 に減少したことがわかります。これは、PHPのガベージコレクション機能が参照カウント法を使って、不要になったメモリ領域を解放する仕組みを持っていることを示しています。

もう少し深掘っていきましょう。

リファレンスカウントの原理

PHPの変数は「zval」と呼ばれる特別なコンテナに値と型と共に保管されており、変数の型と値の他に、情報の追加ビットを2つ含んでいます。

is_ref

「is_ref」は、変数が参照集合の一部かどうかを示すブール値の情報です。これにより、PHPエンジンは通常の変数と参照を区別することができます。PHPでは参照を使うことができるため、zvalコンテナは内部的なリファレンスカウント機構を持っており、メモリ使用状況を最適化します。

refcount

「refcount」は、この1つのzvalコンテナをどれだけ多くの変数名(シンボルとも呼ばれます)が指すかを含みます。変数名はシンボルテーブルに保管され、スコープごとにシンボルテーブルが存在します。スコープは関数やメソッドの中だけでなく、メインスクリプト用のスコープ(つまり、ブラウザによってリクエストされたスクリプト)も存在します。

以下は、zvalコンテナが作成される例です。

$a = "new string";
xdebug_debug_zval('a');

// 出力結果
// a: (refcount=1, is_ref=0)='new string'

 

$a = “new string”;というコードが実行されると、新しい変数名$aが現在のスコープで作成され、この$a変数には値が “new string”であり、型がstringである新しい変数コンテナが作成されます。

・この変数コンテナには、参照集合の一部かどうかを示すis_refという情報のビットが含まれていますが、この時点ではユーザーランド参照が作成されていないため、is_refデフォルトでfalseにセットされます。また、この変数コンテナを利用するシンボル(変数名)が1つだけあるため、refcountは1に設定されます。

・注意点として、refcountを持つ参照である場合(つまり、is_refビットがtrueの場合)であっても、refcount1の場合は参照されていないかのように扱われます(つまり、is_refが常にfalseであったかのように)。これには Xdebugというツールを使うと情報を表示できます。

つまり、xdebug_debug_zval(‘a’);を実行すると、変数名$aの変数コンテナについての情報が表示され、その情報にはrefcountis_refの値が含まれています。この情報を確認することで、PHPの変数の参照やリファレンスの管理状況を理解することができます。

refcountの減少でも同様に、以下のようにリンクが解除されることでrefcountの値は変化します。

$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
$b = 42;
xdebug_debug_zval( 'a' );
unset( $c );
xdebug_debug_zval( 'a' );

// 出力結果
// a: (refcount=3, is_ref=0)='new string'
// a: (refcount=2, is_ref=0)='new string'
// a: (refcount=1, is_ref=0)='new string'

 

配列型の場合は少し複雑で、それらのプロパティをそれら自身のシンボルテーブルに保管します。

$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );

/**    出力結果
*    a: (refcount=1, is_ref=0)=array (
*       'meaning' => (refcount=1, is_ref=0)='life',
*       'number' => (refcount=1, is_ref=0)=42
*    )
**/

イメージは以下のようになります。

引用元:PHP公式リファレンス

よって、この選択肢は合っています。

 

選択肢②

 

PHPの変数で、参照された数は、Xdebug がインストール済みであれば  xdebug_debug_zval() 関数によって得る事ができる。
しかしintやstringなどの型の変数は、=(代入演算子)によって「元の変数を新しい変数にコピーする(値による代入)」ために、通常の代入ではrefcountは増えない。
一方で参照による代入(&)をすると、refcountが増える。
そのため、以下のコード

declare(strict_types=1);
error_reporting(-1);

$i = 1;
xdebug_debug_zval('i');
$i2 = $i;
xdebug_debug_zval('i');
$i3 = &$i;
xdebug_debug_zval('i');

を実行すると

i: (refcount=0, is_ref=0)=1
i: (refcount=0, is_ref=0)=1
i: (refcount=2, is_ref=1)=1

となる。

 

選択肢①でも説明したように、「refcount」は、この1つのzvalコンテナをどれだけ多くの変数名(シンボルとも呼ばれます)が指すかを含みます。

また、「is_ref」は、変数が参照集合の一部かどうかを示すブール値の情報です。

そのため、参照による代入を行うと増えるのは「is_ref」であり、出力結果は以下のようになるべきです。

i: (refcount=1, is_ref=0)=1
i: (refcount=1, is_ref=0)=1
i: (refcount=2, is_ref=1)=1

 

 

選択肢③

 

PHPの変数で、参照された数は、Xdebug がインストール済みであれば  xdebug_debug_zval() 関数によって得る事ができる。
オブジェクトをcloneした場合は「オブジェクトのプロパティを 全てシャローコピーする」が、コピーオンライトによって内部的には参照が用いられているために、値を変更するまでの間は一時的にrefcountが増える。
そのため、以下のコード

declare(strict_types=1);
error_reporting(-1);

$obj = new stdClass();
xdebug_debug_zval('obj');
$obj2 = clone $obj;
xdebug_debug_zval('obj');
$obj2->tset = 'value';
xdebug_debug_zval('obj');

を実行すると

obj: (refcount=1, is_ref=0)=class stdClass {  }
obj: (refcount=2, is_ref=0)=class stdClass {  }
obj: (refcount=1, is_ref=0)=class stdClass {  }

となる。

 

まずはシャローコピーディープコピーについて深掘っていきます。

シャローコピーとは

見ている元データは同じですが、データ自体をコピーしているわけではありません。

そのため、同じデータを指す新しい変数を作成しているだけのコピーとなります。

引用元:ただ屋ぁのブログ

ディープコピーとは

シャローコピーとは異なり、新しい変数を作り、元データの実体そのものもコピーします。

いわゆる、一般的にイメージされるコピーはディープコピーのことを指します。

引用元:ただ屋ぁのブログ

これらの説明から、

オブジェクトをcloneした場合は「オブジェクトのプロパティを 全てシャローコピーする」が、コピーオンライトによって内部的には参照が用いられているために、値を変更するまでの間は一時的にrefcountが増える。」は間違いで、オブジェクトをcloneした場合、そのプロパティはディープコピーされます。つまり、新しいオブジェクトには元のオブジェクトとは別のプロパティが作成されるため参照が用いられることはありません

そのため、cloneした後のオブジェクトは元オブジェクトは独立しているため、refcountは1となります。

正しい出力は以下のようになると考えられます。

obj: (refcount=1, is_ref=0)=class stdClass {  }
obj: (refcount=1, is_ref=0)=class stdClass {  }
obj: (refcount=1, is_ref=0)=class stdClass {  tset => (refcount=1, is_ref=0)string(5) "value" }

よって、この選択肢は誤りです。

 

選択肢④

 

PHPの変数はコピーオンライトが使われているため、参照ではないコピーであっても、コピーのタイミングではメモリはほとんど消費される事がない。
ただしコピー先の値に変更が加わると、そのタイミングで「実態がコピーされる」ために、一気にメモリが消費される。
そのため、以下のコード

declare(strict_types=1);
error_reporting(-1);

$awk = range(0, 1000000);
var_dump(memory_get_usage(true));
$awk2 = $awk;
var_dump(memory_get_usage(true));
$awk2[] = 'v';
var_dump(memory_get_usage(true));

を実行すると

int(35655680)
int(35655680)
int(69214208)

となる(値は環境によって変わる)。

 

まずは、コピーオンライトとはどのような仕組みなのかみていきます。

コピーオンライトとは

コンピュータ内部で、ある程度大きなデータを複製する必要が生じたとき、愚直な設計では、直ちに新たな空き領域を探して割り当て、コピーを実行する。 ところが、もし複製したデータに対する書き換えがなければその複製は無駄だったことになる。

そこで、複製を要求されても、コピーをした振りをして、とりあえず原本をそのまま参照させるが、ただし、そのままで本当に書き換えてはまずい。原本またはコピーのどちらかを書き換えようとしたときに、それを検出し、その時点ではじめて新たな空き領域を探して割り当て、コピーを実行する。これが「書き換え時にコピーする」、すなわちコピーオンライト (Copy-On-Write) の基本的な形態である。

引用元:Wikipedia

memory_get_peak_usage()関数は、PHP によって割り当てられたメモリの最大値を返す関数です。

PHP スクリプトに割り当てられたメモリの最大値を、バイト単位で返します。

引数にtrueに設定すると、システムが割り当てた実際のメモリの大きさを取得します。

memory_get_peak_usage(bool $real_usage = false): int

 

選択肢では、$awk2に$awkをコピーしていますが、コピーオンライトの仕組みにより、実際には元データを参照しています。そのため、メモリの使用量は同じ値を示しています。

しかし、その次の処理では$awk2の値を書き換えようとしているため、その時点で初めて値(実体)がコピーされて新たな空き領域に割り当てられるため、一気にメモリが消費されています

よって、この選択肢は合っています。

ABOUT ME
ヒロ
社会人4年目/25歳/食品商社で2年間営業した後、IT業界にシステムエンジニアとして転職/Java,PHP言語を扱う開発エンジニア