PHPの配列(および値)はどれくらいの大きさですか? (ヒント:非常に大きい)

この記事では、次のスクリプトを例として使用して、PHPでの配列(および一般的な値)のメモリ消費量を調べます。



これは翻訳です(私によく気づかない人のために)。



最初は、メモリ消費の隠れた場所を見つける手助けをしてくれたヨハネスティラエルに感謝します。



<?php $startMemory = memory_get_usage(); $array = range(1, 100000); echo memory_get_usage() - $startMemory, ' bytes';
      
      







どうなると思いますか? 整数が8バイト(64アーキテクチャでlong型を使用)で、100,000個の整数がある場合、明らかに、800,000バイトが必要になります。 これは約0.76 MBです。



次に、コードを実行してみてください。 これはオンラインで実行できます。 結果は14,649,024バイトです。 はい、あなたは正しいと聞きました、これは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をコンパイルすると、異なる値が得られます。 しかし、64ビットLinux上のPHP 5.3の通常のアセンブリでは、指定されたサイズが表示されると思います。



これらの144バイトに100,000の数値を掛けると、14,400,000バイト、つまり13.73 MBになります。 実際の結果にかなり近く、残りはほとんど初期化されていないブロック(バケット)へのポインターですが、これについては後で説明します。



上記の値のより詳細な分析が必要な場合は、次をお読みください:)。



Union zvalue_value





まず、PHPが値を保存する方法を見てみましょう。 ご存じのように、PHPは型指定の弱い言語であるため、値をすばやく切り替える方法が必要です。 PHPは共用体を使用します。これは、次のように定義されています。zend.h#307 (私のコメント):



 typedef union _zvalue_value { long lval; //     double dval; //      struct { //   char *val; //      int len; //    } str; HashTable *ht; //   (-) zend_object_value obj; //   } zvalue_value;
      
      







Cがわからない場合、これは問題ではありません。コードは非常に単純です。結合とは、値が異なる型として機能できることを意味します。 たとえば、 zvalue_value-> lvalを使用すると、値は整数として解釈されます。 一方、 zvalue_value-> htを使用すると、値はハッシュテーブル(別名配列)へのポインターとして解釈されます。



これにとどまりません。 私たちにとって重要なことは、ユニオンのサイズがその最大コンポーネントのサイズに等しいことです。 最大のコンポーネントは文字列です(実際、 zend_object_value構造体もサイズ変更されますが、簡単にするためにこの点は省略します)。 構造は、ポインター(8バイト)と整数(4バイト)で構成されます。 合計12バイト。 メモリのアライメント(12バイトの構造は64ビット/ 8バイトの積ではないため、クールではありません)により、構造の最終サイズは16バイトになり、それに応じて全体の関連付け全体になります。



したがって、PHPの動的型付けのために、各値に8バイトではなく16バイトが必要であることがわかりました。 100,000を掛けると、1,600,000バイトになります。 1.53 Mb。 しかし、実際のボリュームは13.97 MBなので、まだ目標に到達していません。



Zval構造





ユニオンは値のみを保存するのが論理的であり、PHPは明らかに、その型とガベージコレクションの情報も保存する必要があります。 この情報を含む構造はzvalと呼ばれ、おそらく既に聞いたことがあるでしょう。 これがPHPである理由の詳細については、 Sara Golemonの記事を読むことをお勧めします 。 なるほど、この構造は次のように定義されています



 struct _zval_struct { zvalue_value value; //  zend_uint refcount__gc; //     ( GC) zend_uchar type; //  zend_uchar is_ref__gc; //      (&) };
      
      







構造体のサイズは、そのすべてのコンポーネントのサイズの合計によって決まります: zvalue_value -16バイト(上記の計算)、 zend_uint -4バイト、 zend_uchar-各1バイト。 合計22バイト。 繰り返しますが、メモリのアライメントにより、実際のサイズは24バイトになります。



したがって、24バイトの値をそれぞれ100,000個保存すると、2,400,000バイト、つまり2.29 MBになります。 ギャップは狭くなっていますが、実際の値は6倍以上です。



循環参照のガベージコレクター(PHP 5.3)





PHP 5.3では、 循環参照用の新しいガベージコレクターが導入されました。 このため、PHPはいくつかの追加情報を保存します。 ここではその仕組みを説明しません。マニュアルから必要な情報を入手できます。 サイズの計算では、各zvalzval_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バイトのポインターです。 したがって、ユニオンのサイズも8バイトです。



上記の受信した24バイトを追加すると、32バイトになります。 これに100,000を掛けると、3.05 MBになります。



ZENDメモリマネージャー





Cは、PHPとは異なり、メモリを管理しません。 メモリ割り当てを個別に監視する必要があります。 これを行うために、PHPはニーズに合わせて最適化された独自のメモリマネージャー、Zend Memory Managerを使用します。 MM Zendは、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_t _prev; //   (   ) } zend_mm_block_info;
      
      







ご覧のとおり、定義には多くのコンパイルオプションチェックが含まれています。 これらのオプションの少なくとも1つが有効になっている場合、割り当てられたメモリのヘッダーは大きくなり、ヒープ保護、スレッドセーフ、デバッグ、MM Cookieを使用してPHPをコンパイルすると最大になります。



たとえば、これらのオプションはすべて無効になっていると仮定します。 この場合、2つのコンポーネントのみ size_t _size_prevのままです。 size_tは8バイト(64ビット)を占有するため、ヘッダーのサイズは16バイトです。このヘッダーはメモリ割り当てごとに追加されます。



そのため、 zvalサイズを再度調整する必要があります。 実際には、このヘッダーのために32バイトではなく48バイトになります。 100,000個の要素を掛けると、4,58 Mbが得られます。 実際のサイズは13.97 MBなので、すでに約3分の1をカバーしています。



ブロック





これまで、値を個別に見てきました。 しかし、PHPの配列構造は多くのスペースを占有します。 実際、ここでは「配列」という用語の選択が不十分です。 PHPでは、配列は実際にはテーブル/辞書のハッシュです。 それでは、ハッシュテーブルはどのように機能しますか? 基本的に、各キーに対してハッシュが生成され、このハッシュを使用して「実際の」C配列に入ります。 ハッシュは競合する可能性があり、同じハッシュを持つすべての要素がリンクリストに保存されます。 要素にアクセスする場合、PHPは最初にハッシュを計算し、目的のブロック(バケット)を探し、要素ごとに完全に一致する要素を検索してリストを調べます。 ブロックは次のように定義されます( zend_hash.h#54 ):



 typedef struct bucket { ulong h; //  (    ) uint nKeyLength; //   (  ) void *pData; //  void *pDataPtr; // ???   ??? struct bucket *pListNext; // PHP  .     struct bucket *pListLast; //    struct bucket *pNext; //     ()   struct bucket *pLast; //     ()   const char *arKey; //  (  ) } Bucket;
      
      







ご覧のとおり、PHPで使用されるような抽象的なデータ配列を取得するには、データの「ロード」を保存する必要があります(PHP配列は、同時に配列、辞書、およびリンクリストであり、もちろん多くのデータが必要です)。 個々のコンポーネントのサイズは、 ulong型の場合は8バイト、 uint型の場合は4バイト、ポインターの場合は7バイトの8バイトです。 結果は68です。アライメントを追加し、72バイトを取得します。



ブロックおよびzvalの場合、16バイトのヘッダーを追加する必要があり、88バイトになります。 また、これらのブロックへのポインターを「実際の」C配列(Bucket ** arBuckets;)に格納する必要があります。前述したように、要素にさらに8バイトを追加します。 したがって、一般的に、各ブロックは96バイトのメモリを消費します。



したがって、各値にブロックが必要な場合、 バケットの場合は96バイト、 zvalの場合は48バイトになり 、合計で144バイトになります。 100,000要素の場合、これは14,400,000バイト、つまり13.73 MBになります。



謎は解決されました。



待ってください、まだ0.24 Mbです!





これらの最後の0.24 Mbは、初期化されていないブロックが原因です。「実際の」配列Cのサイズは、理想的には要素の数に等しくなければなりません。 このようにして、衝突を最小限に抑えます(大量のメモリを無駄にしたくない場合)。 しかし、PHPは明らかに、新しい要素が追加されるたびに配列全体を再配布することはできません-それは非常に遅いでしょう。 代わりに、PHPは、内部ブロック配列が制限内に収まる場合、常に内部ブロック配列のサイズを2倍にします。 したがって、配列のサイズは常に2のべき乗です。



私たちの場合、これは2 ^ 17 = 131 072です。しかし、これらのブロックのうち100,000だけが必要なので、31 072ブロックは未使用のままにします。 これらは、これらのブロックのメモリは割り当てられません(したがって、96バイト全体を使用する必要はありません)が、ポインタのメモリ(ブロックの内部配列に格納されている)はブロックで使用する必要があります。 したがって、8バイト(ポインターごと)* 31,072要素を追加で使用します。 これは248 576バイトまたは0.23 MBです。 不足しているメモリに対応します。 (もちろん、さらにいくつかのバイトが欠落していますが、すべてを完全にカバーするわけではありません。これらは、ハッシュテーブル自体の構造、変数などのようなものです)



謎は本当に解決されています。



これは何を教えてくれますか?





PHPはCではありません。それはそれだけを示しています。 Cのように、超動的PHP言語がメモリを効率的に使用することは期待できません。それはできません。



ただし、メモリを節約したい場合は、大規模な静的配列にSplFixedArrayを使用することを検討できます。



変更されたスクリプトを見てみましょう。



 <?php $startMemory = memory_get_usage(); $array = new SplFixedArray(100000); for ($i = 0; $i < 100000; ++$i) { $array[$i] = $i; } echo memory_get_usage() - $startMemory, ' bytes';
      
      







基本的に同じことを行いますが、実行すると、5,600,640バイトのみを使用することに気付くでしょう。 これは要素ごとに56バイトであり、通常の配列の要素ごとに144バイトよりはるかに少ないです。 これは、固定配列がバケット構造を必要としないためです。したがって、要素ごとに1つのzval (48バイト)と1つのポインター(8バイト)のみが必要であり、観測可能な56バイトが得られます。



PS翻訳に関するコメントをすべてLANに書いてください。すぐに修正するようにします。



All Articles