てきとうなさいと べぇたばん

Allowed memory size ofというエラーはどういうエラーなのか - PHP Zend Engineのメモリ管理

該当のエラー部分。heap構造体が鍵を握ってそうだと調べた。

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を調べていきたい。