この記事は、PHP 5 のメモリ使用量について説明しています。この記事で説明されている状況では、PHP 7 ではメモリ使用量が約 3 倍低くなります。
この投稿では、100000 個の一意の整数配列を作成する次のスクリプトを例として使用して、PHP 配列 (および一般的な値) のメモリ使用量を調べたいと思います。要素を取得し、結果のメモリ使用量を測定しました:
$startMemory = memory_get_usage(); $array = range(1, 100000); echo memory_get_usage() - $startMemory, ' bytes';
What do you want it to be? 簡単に言えば、整数は 8 バイトです (64 ビット UNIX マシンでは long 型を使用)。 100,000 個の整数なので、明らかに 800,000 バイトが必要です。
次に、上記のコードを実行してみます。これにより、14649024 バイトが得られます。そうです。13.97 MB で、推定値の 18 倍です。
それでは、追加の因数 18 はどこから来たのでしょうか?
概要
ストーリー全体を知りたくない人のために、関係するものは次のとおりです。 さまざまなコンポーネントのメモリ使用量を簡単にまとめます。
| 64 bit | 32 bit --------------------------------------------------- zval | 24 bytes | 16 bytes + cyclic GC info | 8 bytes | 4 bytes + allocation header | 16 bytes | 8 bytes =================================================== zval (value) total | 48 bytes | 28 bytes =================================================== bucket | 72 bytes | 36 bytes + allocation header | 16 bytes | 8 bytes + pointer | 8 bytes | 4 bytes =================================================== bucket (array element) total | 96 bytes | 48 bytes =================================================== total total | 144 bytes | 76 bytes
上記の数値は、オペレーティング システム、コンパイラ、およびコンパイル オプションによって異なります。たとえば、デバッグ セーフまたはスレッド セーフを使用して PHP をコンパイルすると、異なる数値が得られます。ただし、上記のサイズは、Linux 上の PHP 5.3 の 64 ビット製品バージョンで表示されるものだと思います。
これらの 144 バイトに 100,000 要素を掛けると、14,400,000 バイト、つまり 13.73 MB が得られます。これは実際の数値に非常に近い値です。残りの大部分は初期化されていないバケット ポインターですが、これについては後で説明します。後で。
ここで、上記の値のより詳細な分析が必要な場合は、読み続けてください:)
zvalue_value Alliance
まず、PHP が値をどのように保存するかを見てください。ご存知のとおり、PHP は型指定が弱い言語であるため、型をすばやく切り替える何らかの方法が必要です。 PHP はこれに Union を使用します。これは zend で次のように定義されています。
typedef union _zvalue_value { long lval; // For integers and booleans double dval; // For floats (doubles) struct { // For strings char *val; // consisting of the string itself int len; // and its length } str; HashTable *ht; // For arrays (hash tables) zend_object_value obj; // For objects } zvalue_value;
C の知識がない場合でも、コードは非常に単純なので問題ありません。ユニオンは、特定の値をさまざまな型としてアクセスできるようにする方法です。たとえば、zvalue_value->lval を実行すると、整数として解釈される値が得られます。一方、zvalue_value->ht を使用すると、値はハッシュ テーブル (配列) へのポインターとして解釈されます。
しかし、ここではあまり話さないようにしましょう。私たちにとって重要なのは、結合のサイズがその最大のコンポーネントのサイズと等しいことだけです。ここでの最大のコンポーネントは文字列構造体です (zend_object_value 構造体は str 構造体と同じサイズですが、簡単にするために省略します)。文字列構造体には、ポインター (8 バイト) と整数 (4 バイト) の合計 12 バイトが格納されます。メモリの調整 (12 バイトの構造体は 64 ビット/8 バイトの倍数ではないため、クールではありません) により、構造体の合計サイズは 16 バイトになり、これは共用体全体のサイズでもあります。
PHP の動的型付けにより、各値には 8 バイトではなく 16 バイトが必要であることがわかりました。 100000 の値を掛けると 1600000 バイト、つまり 1.53 MB になりますが、実際の値は 13.97 MB であるため、まだ取得できません。
zval の構造
これは非常に論理的です。ユニオンは値自体を保存するだけですが、PHP には明らかにストレージ タイプといくつかのガベージ コレクション情報も必要です。この情報を保持する構造は zval と呼ばれ、聞いたことがあるかもしれません。 PHP にそれが必要な理由について詳しくは、Sara Golemon による記事を読むことをお勧めします。とにかく、構造体は次のように定義されます:
struct _zval_struct { zvalue_value value; // The value zend_uint refcount__gc; // The number of references to this value (for GC) zend_uchar type; // The type zend_uchar is_ref__gc; // Whether this value is a reference (&) };
構造体のサイズは、そのコンポーネントのサイズの合計によって決まります: zvalue_value (上記のように計算) に 16 バイト、zend_uint に 4 バイト、および 1 zend_uchars のバイト。合計は 22 バイトです。メモリの調整により、実際のサイズは 24 バイトになります。
したがって、100,000 個の要素を 24 バイトに格納すると、合計は 2,400,000、つまり 2.29 MB となり、差は縮まりつつありますが、実際の値は依然として元の 6 倍を超えています。
循環コレクター (PHP 5.3 以降)
PHP 5.3 では、新しい循環参照ガベージ コレクターが導入されました。これを行うには、PHP は追加データを保存する必要があります。このアルゴリズムがどのように機能するかについては、ここでは説明しません。マニュアルのリンク先のページで説明します。サイズ計算で重要なのは、PHP が各 zval を zval_gc_info:
typedef struct _zval_gc_info { zval z; union { gc_root_buffer *buffered; struct _zval_gc_info *next; } u; } zval_gc_info;
にラップすることです。ご覧のとおり、Zend は 2 つのポインター構成で構成される共用体をそれに追加するだけです。共用体のサイズはその最大のコンポーネントのサイズであることを覚えておいてください。両方の共用体コンポーネントはポインターであるため、サイズは両方とも 8 バイトです。したがって、union のサイズも 8 バイトになります。
これを 24 バイトに追加すると、すでに 32 バイトになります。 100000 要素を掛けると、メモリ使用量は 3.05 MB になります。
Zend MM Allocator
C PHP とは異なり、メモリを管理しません。自分の割り当てを自分で追跡する必要があります。これを行うために、PHP は、そのニーズに合わせて最適化されたカスタム メモリ マネージャー、Zend Memory Manager を使用します。 Zend MM は Doug Lea の malloc に基づいており、PHP 固有の最適化と機能 (メモリ制限、各リクエスト後のクリーンアップなど) が追加されています。
ここで重要なことは、MM はそれを通じて行われるすべての割り当てに対して割り当てヘッダーを追加するということです。定義は次のとおりです:
typedef struct _zend_mm_block { zend_mm_block_info info; #if ZEND_DEBUG unsigned int magic; # ifdef ZTS THREAD_T thread_id; # endif zend_mm_debug_info debug; #elif ZEND_MM_HEAP_PROTECTION zend_mm_debug_info debug; #endif } zend_mm_block; typedef struct _zend_mm_block_info { #if ZEND_MM_COOKIES size_t _cookie; #endif size_t _size; // size of the allocation size_t _prev; // previous block (not sure what exactly this is) } zend_mm_block_info;
如您所见,这些定义充斥着大量的编译选项检查。如果你用堆保护,多线程,调试和MM cookie来构建PHP,那么如果你用堆保护,多线程,调试和MM cookie来构建PHP,那么如果你用堆保护,多线程,调试和MM cookie来构建PHP,那么分配头文件会更大。
对于本例,我们假设所有这些选项都是禁用的。在这种情况下,只剩下两个size_ts _size和_prev。size_t有8个字节(在64位上),所以分配头的总大小是16个字节——并且在每个分配上都添加了这个头。
现在我们需要再次调整zval大小。实际上,它不是32字节,而是48字节,这是由分配头决定的。乘以100000个元素是4。58 MB,实际值是13。97 MB,所以我们已经得到了大约三分之一的面积。
Buckets
到目前为止,我们只考虑单个值。但是PHP中的数组结构也会占用大量空间:“数组”在这里实际上是一个不合适的术语。PHP数组实际上是散列表/字典。那么哈希表是如何工作的呢?基本上,对于每个键,都会生成一个散列,该散列用作“real”C数组的偏移量。由于哈希值可能会冲突,具有相同哈希值的所有元素都存储在链表中。当访问一个元素时,PHP首先计算散列,查找正确的bucket并遍历链接列表,逐个元素比较确切的键。bucket的定义如下:
typedef struct bucket { ulong h; // The hash (or for int keys the key) uint nKeyLength; // The length of the key (for string keys) void *pData; // The actual data void *pDataPtr; // ??? What's this ??? struct bucket *pListNext; // PHP arrays are ordered. This gives the next element in that order struct bucket *pListLast; // and this gives the previous element struct bucket *pNext; // The next element in this (doubly) linked list struct bucket *pLast; // The previous element in this (doubly) linked list const char *arKey; // The key (for string keys) } Bucket;
正如您所看到的,需要存储大量数据才能获得PHP使用的抽象数组数据结构(PHP数组同时是数组、字典和链表,这当然需要大量信息)。单个组件的大小为无符号long为8字节,无符号int为4字节,指针为7乘以8字节。总共是68。添加对齐,得到72字节。
像zvals这样的bucket需要在头部分配,因此我们需要再次为分配头添加16个字节,从而得到88个字节。我们还需要在“real”C数组中存储指向这些Bucket的指针(Bucket ** arbucket;)我上面提到过,每个元素增加8个字节。所以总的来说,每个bucket需要96字节的存储空间。
如果每个值都需要一个bucket,那么bucket是96字节,zval是48字节,总共144字节。对于100000个元素,也就是14400000字节,即13.73 MB。
神秘的解决。
等等,还有0.24 MB !
最后的0.24 MB是由于未初始化的存储bucket造成的:理想情况下,存储bucket的实际C数组的大小应该与存储的数组元素的数量大致相同。通过这种方式,冲突最少(除非希望浪费大量内存)。但是PHP显然不能在每次添加元素时重新分配整个数组——这将非常缓慢。相反,如果内部bucket数组达到限制,PHP总是将其大小加倍。所以数组的大小总是2的幂。
在我们的例子中是2 ^ 17 = 131072。但是我们只需要100000个bucket,所以我们留下31072个bucket没有使用。这些bucket不会被分配(因此我们不需要花费全部的96字节),但是bucket指针(存储在内部桶数组中的那个)的内存仍然需要分配。所以我们另外使用8字节(一个指针)* 31072个元素。这是248576字节或0.23 MB,与丢失的内存匹配。(当然,这里仍然缺少一些字节,但是我不想在这里介绍。比如哈希表结构本身,变量等等)
神秘真的解决了。
这告诉我们什么?
PHP不是c,这就是所有这些告诉我们的。您不能期望像PHP这样的超级动态语言具有与C语言相同的高效内存使用。你不能。
但是,如果您确实想节省内存,可以考虑使用SplFixedArray处理大型静态数组。
看看这个修改后的脚本:
$startMemory = memory_get_usage(); $array = new SplFixedArray(100000); for ($i = 0; $i < 100000; ++$i) { $array[$i] = $i; } echo memory_get_usage() - $startMemory, ' bytes';
它基本上做的是相同的事情,但是如果运行它,您会注意到它只使用了“5600640字节”。这是每个元素56字节,因此比普通数组使用的每个元素144字节要少得多。这是因为一个固定的数组不需要bucket结构:所以它只需要每个元素一个zval(48字节)和一个指针(8字节),从而得到观察到的56字节。
以上がPHP の配列と値はどれくらいの大きさですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。