Allowed memory size ofというエラーはどういうエラーなのか - PHP Zend Engineのメモリ管理
TOP > てきとうにこらむ > ゲーム作りとプログラミング日記 > Allowed memory size ofというエラーはどういうエラーなのか - PHP Zend Engineのメモリ管理
Allowed memory size of ~
このエラーは、php.iniにおいて、指定したmemory_limitを超えて更にメモリを確保しようとするときに起こるエラーである。
Allowed memory size of xxx bytes exhausted (tried to allocate xxx bytes) in /path/to/dir/hoge.php on line xxxx
単純に、php.iniとかini_setとかで変更できるので、それでお茶を濁すことももちろん可能であるが、大体は以下のような根本的なコードの問題を抱えていることが多い。
- データベースなどから大量のデータを取得してしまっていて、それが大幅に圧迫している
- フレームワークがやたらと大量のデータを確保したがる
しかも、フレームワークのデバッグツール(CakePHPで言うところのdebugkit)を華麗にスルーしてエラーのみを出してくることが多い。
そうなるとボトルネックを追いかけるのも一苦労である。
どこで確保しているのか
以下のコードのopcodeを見てみよう。
<?php
$hoge = array();
while(1) {
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
$hoge[] = 1;
}
実行には、以下のようにmemory_limitを指定して実行。
$ php -dvld.active=1 -dvld.execute=0 -d'memory_limit="32M"' infinite_loop.php
さて、opcodeを実行してみよう。
$ php -dvld.active=1 -dvld.execute=0 -d'memory_limit="32M"' infinite_loop.php
Finding entry points
Branch analysis from position: 0
Jump found. Position 1 = 5, Position 2 = 102
Branch analysis from position: 5
Jump found. Position 1 = 4
Branch analysis from position: 4
Branch analysis from position: 102
Jump found. Position 1 = -2
filename: /home/tekitoh/docker/infinite_loop.php
function name: (null)
number of ops: 103
compiled vars: !0 = $hoge
line #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
3 0 E > EXT_STMT
1 INIT_ARRAY ~0
2 ASSIGN !0, ~0
5 3 EXT_STMT
4 > > JMPZ 1, ->102
6 5 > EXT_STMT
6 ASSIGN_DIM !0
7 OP_DATA 1, $3
7 8 EXT_STMT
9 ASSIGN_DIM !0
10 OP_DATA 1, $5
8 11 EXT_STMT
12 ASSIGN_DIM !0
13 OP_DATA 1, $7
9 14 EXT_STMT
15 ASSIGN_DIM !0
16 OP_DATA 1, $9
10 17 EXT_STMT
18 ASSIGN_DIM !0
19 OP_DATA 1, $11
11 20 EXT_STMT
21 ASSIGN_DIM !0
22 OP_DATA 1, $13
12 23 EXT_STMT
24 ASSIGN_DIM !0
25 OP_DATA 1, $15
13 26 EXT_STMT
27 ASSIGN_DIM !0
28 OP_DATA 1, $17
14 29 EXT_STMT
30 ASSIGN_DIM !0
31 OP_DATA 1, $19
15 32 EXT_STMT
33 ASSIGN_DIM !0
34 OP_DATA 1, $21
16 35 EXT_STMT
36 ASSIGN_DIM !0
37 OP_DATA 1, $23
17 38 EXT_STMT
39 ASSIGN_DIM !0
40 OP_DATA 1, $25
18 41 EXT_STMT
42 ASSIGN_DIM !0
43 OP_DATA 1, $27
19 44 EXT_STMT
45 ASSIGN_DIM !0
46 OP_DATA 1, $29
20 47 EXT_STMT
48 ASSIGN_DIM !0
49 OP_DATA 1, $31
21 50 EXT_STMT
51 ASSIGN_DIM !0
52 OP_DATA 1, $33
22 53 EXT_STMT
54 ASSIGN_DIM !0
55 OP_DATA 1, $35
23 56 EXT_STMT
57 ASSIGN_DIM !0
58 OP_DATA 1, $37
24 59 EXT_STMT
60 ASSIGN_DIM !0
61 OP_DATA 1, $39
25 62 EXT_STMT
63 ASSIGN_DIM !0
64 OP_DATA 1, $41
26 65 EXT_STMT
66 ASSIGN_DIM !0
67 OP_DATA 1, $43
27 68 EXT_STMT
69 ASSIGN_DIM !0
70 OP_DATA 1, $45
28 71 EXT_STMT
72 ASSIGN_DIM !0
73 OP_DATA 1, $47
29 74 EXT_STMT
75 ASSIGN_DIM !0
76 OP_DATA 1, $49
30 77 EXT_STMT
78 ASSIGN_DIM !0
79 OP_DATA 1, $51
31 80 EXT_STMT
81 ASSIGN_DIM !0
82 OP_DATA 1, $53
32 83 EXT_STMT
84 ASSIGN_DIM !0
85 OP_DATA 1, $55
33 86 EXT_STMT
87 ASSIGN_DIM !0
88 OP_DATA 1, $57
34 89 EXT_STMT
90 ASSIGN_DIM !0
91 OP_DATA 1, $59
35 92 EXT_STMT
93 ASSIGN_DIM !0
94 OP_DATA 1, $61
36 95 EXT_STMT
96 ASSIGN_DIM !0
97 OP_DATA 1, $63
37 98 EXT_STMT
99 ASSIGN_DIM !0
100 OP_DATA 1, $65
38 101 > JMP ->4
39 102 > > RETURN 1
branch: # 0; line: 3- 5; sop: 0; eop: 3; out1: 4
branch: # 4; line: 5- 5; sop: 4; eop: 4; out1: 5; out2: 102
branch: # 5; line: 6- 38; sop: 5; eop: 101; out1: 4
branch: #102; line: 39- 39; sop: 102; eop: 102; out1: -2
path #1: 0, 4, 5, 4, 102,
path #2: 0, 4, 102,
もちろん、単純に複数のopcodeが実行されていくのみになっている。
- EXT_STMT
- ASSIGN_DIM
- OP_DATA
これが順々に繰り返されている。この内、変数に格納するASSIGN_DIMをみてみよう。
ASSIGN_DIMのなかみ
- zend_assign_to_string_offset
- zend_string_extend or zend_string_init
- pemalloc
- _erealloc
- _zend_mm_realloc_int
どうやら何通りかある模様。_zend_mm_realloc_intで確保しようとして失敗するとFatal Errorを発行するようになる。
興味深いのはZend/zend_alloc.cの1724行目、984行目にある「メモリを確保する量が、制限された量を超えるとエラーの箇所。
このエラーを発行する際に、確保したヒープを開放せざるを得ないため、zend_mm_safe_errorへ送られる。
Allowed memory size of xxx bytes exhausted (tried to allocate xxx bytes) in /path/to/dir/hoge.php on line xxxx
このようなエラーが送られているが、これはZend/zend.cのzend_error関数によってそういう処理が作成されている。
結局、このエラーが発生するときに処理をすべて終了させてexit(1)させているので、デバッグトレースなどは難しいのではないか、と思わされた。
emalloc
http://php.net/manual/ja/internals2.memory.management.phpによれば、Zend Engineのメモリ管理にsizeバイトのメモリを確保するもので、Zend Engineはこれを介してメモリを確保する。
このときに、メモリの確保に失敗すれば、その部分からエラーが発行されるという仕組みで、これによって一括管理されているようだ。
zend_mm_heap
メモリの確保した量、最も多く確保した量などを管理しているのは、zend_mm_heap構造体である。Zend/zend_alloc.c:415にある。
_zend_mm_heap
メモリ使用量が見たければ、単純にPHPの関数、memory_get_usageを使ってみることができる。
第二引数の$real_usageは、trueにすると、zend_mm_heapのreal_sizeを参照するが、デフォルトのfalseではzend_mm_heapのsizeを参照する。
このうち、Allowed memory size ofのエラーで判定するのはreal_sizeのほうなので
memory_get_usage(true);
とするとあとどのくらいでエラーするのかがわかるということになるか。試しに、さっきのコードにmemory_get_usageを突っ込んでtrueとfalseとで調べてみた。
int(33554432) // true
int(33541160) // false
PHP Fatal error: Allowed memory size of 33554432 bytes exhausted (tried to allocate 72 bytes) in /home/tekitoh/docker/infinite_loop.php on line 32
PHP Stack trace:
PHP 1. {main}() /home/tekitoh/docker/infinite_loop.php:0
確かに、trueの方が正確ということになりそう。
ちなみに、zend_mm_heapに関係なく、独自にmallocしたりするとそれを捕捉することができないので注意。とはいっても、ユーザーランドでどうにかできる問題ではなく、そうしたPHPのメモリマネージャーを利用しないC extensionを使わないという選択しかないけれども。
PHP は、emalloc() が割り当てたメモリ以外のメモリは追跡しません。
「memory_limitを無視したコードを書きたければ、mallocを使ってお茶を濁してしまえ」というのはそれは流石に禁じ手だと思った。
かだい
残っている疑問もあるにはある。zend_mm_heap構造体のsizeとreal_sizeとのちがいなどがそうで、ここらをさらに掘っていくことが必要なのかなと。
メモリ管理は重要な要素だと思うので引き続きZend Engineを調べていきたい。