メモリを操作します(それでも)

「通常の」PHP開発者は実際にメモリ管理について心配する必要はないと広く信じられていますが、「ケア」と「知識」はまだ少し異なる概念です。 変数と配列を操作するときのメモリ管理のいくつかの側面と、PHP内部最適化の興味深い「落とし穴」を強調してみます。 ご覧のとおり、最適化は優れていますが、最適化の正確性がわからない場合は、「明白でないレーキ」が発生する可能性があり、非常に緊張します。





一般的な情報



小さな教育プログラム


PHPの変数は、いわば、2つの部分で構成されています。hash_tablesymbol_tableに格納されている「 name 」と、zvalコンテナーに格納されている「 value 」です。

このメカニズムにより、同じ値を参照する複数の変数を作成でき、場合によってはメモリ消費を最適化できます。 実際にどのように見えるかは、後で説明します。



多かれ少なかれ機能的なスクリプトを想像するのが難しい最も一般的なコード要素は、次の点です。

-変数(番号、行など)の作成、割り当て、削除

-配列の作成とそのトラバース(foreach関数を例として使用します)、

-関数/メソッドの値の転送と戻り。



以下の説明は、メモリを操作するこれらの側面についてです。 それは非常に膨大であることが判明しましたが、巨大な複雑さはなく、すべてが非常にシンプルで、明らかに例があります。



メモリを操作する最初の例


手始めに、メモリ消費分析の実行方法の基本的な例。

これを行うには、いくつかの単純な関数( func.phpファイル)が必要です。

<?php

function memoryUsage $ usage $ base_memory_usage {

printf "Bytes diff: %d \ n" $ usage-$ base_memory_usage ;

}

関数 someBigValue {

return str_repeat 'SOME BIG STRING' 1024 ;

}

?>




そして、文字列のメモリ消費テストの簡単な最初の例:

<?php

include 'func.php' ;

echo "文字列メモリ使用量テスト。\ n \ n" ;

$ base_memory_usage = memory_get_usage ;

$ base_memory_usage = memory_get_usage ;



echo "開始\ n" ;

memoryUsage memory_get_usage $ base_memory_usage ;



$ a = someBigValue ;



echo "文字列値が設定されました\ n" ;

memoryUsage memory_get_usage $ base_memory_usage ;



設定解除 $ a ;



echo "文字列値が設定されていません\ n" ;

memoryUsage memory_get_usage $ base_memory_usage ;

?>


注: 間違いなく、コードは操作性の観点から最適化されていませんが、この場合、このビューが実装されているメモリ消費の可視性は非常に重要です。



コードの結果は非常に明白です。

文字列メモリ使用量テスト。



開始する

バイトdiff:0

設定された文字列値

バイトの差分:15448

設定されていない文字列値

バイトdiff:0




同じ例ですが、 unset($ a)の代わりに$ a = null; 使用します。

開始する

バイトdiff:0

設定された文字列値

バイトの差分:15448

nullに設定された文字列値

バイトの差分:76



ご覧のとおり、変数は完全には破壊されていません。 その下には、さらに76バイトが割り当てられたままです。

boolean、integer、floatなどの変数にまったく同じ量が割り当てられることを考えると、かなりまともです。 これは、変数値に割り当てられたメモリの量ではなく、割り当てられた変数(値と変数名自体を含むzvalコンテナ)に関する情報を保存するための総メモリ消費量です。

したがって、割り当てを使用してメモリを解放する場合、正確にnull値を割り当てる必要はありません。 式$ a = 10000; メモリ消費についても同じ結果が得られます。



PHPのドキュメントでは、 nullにキャストすると変数とその値破壊されると書かれていますが 、このスクリプトはそうではなく、実際にはバグ(ドキュメント)であることを示しています。



unset()が可能な場合に、 null割り当てを使用するのはなぜですか?

割り当ては(KOのおかげで)割り当てです。つまり、変数の値が変化します。したがって、新しい値が必要とするメモリが少ない場合、すぐに解放されますが、計算リソースが必要になります(比較的少ないですが)。

unset()は、変数名とその値に割り当てられたメモリを解放します。

それとは別に、 unset()nullの割り当ては変数参照では非常に異なる動作をするという事実に言及する価値がありますUnset()はリンクのみ破棄し、 nullを割り当てると変数名が参照する値が変更されるため、すべての変数はnull値を参照します。



注:

unset()は関数であるという誤解がありますが、これは正しくありません。 unset()、ドキュメントに明示的に記載されている言語構成体( ifなど)であるため、変数の値を介したアクセスには使用できません。

$ unset_func_name = 'unset' ;

$ unset_func_name $ some_var ;




怠idleな思考に関する追加情報(上記の例を変更する場合):

$ a = array();

164バイトを割り当て、未設定($ a)はすべてを返します。



クラスA {}

$ a = new A();

184バイトを割り当て、未設定($ a)はすべてを返します。



$ a =新しいstdClass();

272バイトを割り当てますが、未設定($ a)の後に88バイトが「リーク」します(どこで、なぜリークしたかを正確に見つけることができませんでした)。



これまでのところ、文字列と数値は非常に明確に保存および処理されるため、上記の例はメモリ消費の観点から重要ではありません。 配列を使用すると、すべてがさらに悪化します(オブジェクトにも多くの機能がありますが、これには別の記事が必要です)。



配列



PHPの配列は十分なメモリを「使い果たし」ます。通常、処理中に大量のデータを格納するのはそのためです。そのため、それらの操作には十分注意する必要があります。 ただし、PHPでの配列の操作には「最適化の魅力」があり、メモリ消費に関連する問題の1つに言及する価値があります。



陰湿な例1

< php

include 'func.php' ;

echo "アレイメモリの使用例。" ;

$ base_memory_usage = memory_get_usage ;

$ base_memory_usage = memory_get_usage ;



echo 「ベース使用量」。PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;



$ a = array someBigValue 、someBigValue 、someBigValue 、someBigValue ;



echo '配列が設定されています。'PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;



foreach $ a as $ k => $ v {

$ a [ $ k ] = someBigValue ;

設定解除 $ k、$ v ;

echo 「FOREACHサイクル」。PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;

}



「FOREACHの直後の使用法」をエコーします。PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;



設定解除 $ a ;

echo '配列未設定'PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;

>


一見、$ a配列のメモリ消費量は変わらないように見えるかもしれません(変数$ kおよび$ vの設定を除く)が、この場合配列を操作する場合、PHPには特別なアプローチがあります。



出力を見てください:

アレイのメモリ使用量の例。ベースの使用量。

バイトdiff:0

配列が設定されています。

バイトの差分:61940

FOREACHサイクル。

バイトの差分:77632

FOREACHサイクル。

バイトの差分:93032

FOREACHサイクル。

バイトの差分:108432

FOREACHサイクル。

バイトの差分:123832

FOREACHの直後の使用。

バイトの差分:61940

配列が設定されていません。

バイトdiff:0



この場合、foreachループの最後の反復で、配列によるメモリ消費が2倍になりましたが、これはコード自体からは明らかではありません。 しかし、サイクルの直後に、メモリ消費量は以前の値に戻りました。 奇跡など。

これは、ループ内の配列の使用を最適化するためです。 ループの実行中、元の配列を変更しようとすると、配列構造の暗黙的なコピー(値のコピーではない)が作成され、サイクルの終わりに使用可能になり、元の構造が破棄されます。 したがって、上記の例では、新しい値を元の配列に割り当てた場合、それらはすぐには置き換えられませんが、個別のメモリが割り当てられ、ループの終了時に返されます。

この瞬間は非常に見逃しやすいため、たとえばデータベースからフェッチする場合など、大規模なデータ配列を使用しているサイクル中に大量のメモリが消費される可能性があります。



注:

ループ自体の内部では、$ a [$ k]の値を変更した後、$ vの値を保存しなかった場合、元の配列にまだ保存されている値を取得できません。 $ a [$ k]を繰り返し呼び出すと、新しい値が生成されます。



ユーザー zibada からの追加 (要するに):

変更の場合の新しい「一時配列」へのメモリの割り当ては、配列の構造全体に対して一度に行われますが、変更される各要素に対して個別に行われることを考慮することが重要です。 したがって、多数の要素を持つ配列(ただし、必ずしも大きい値である必要はありません)がある場合、そのようなコピー中の1回限りのメモリ消費が大きくなります。



陰湿な例2

コードを少し変更しましょう。

echo '配列が設定されています。'PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;

$ b = $ a ; //これを追加します

foreach $ a as $ k => $ v {

$ a [ $ k ] = someBigValue ;

設定解除 $ k、$ v ;

echo 「FOREACHサイクル」。PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;

}

設定解除 $ b ; //そしてこれ

「FOREACHの直後の使用」をエコーします。PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;




ループコード自体は変更しませんでした。変更したのは、ソース配列への参照カウントを増やすことだけでしたが、これによりループ操作が根本的に変更されました。

バイトdiff:0

配列が設定されています。

バイトの差分:61940

FOREACHサイクル。

バイトの差分:61988

FOREACHサイクル。

バイトの差分:61988

FOREACHサイクル。

バイトの差分:61988

FOREACHサイクル。

バイトの差分:61988

FOREACHの直後の使用。

バイトの差分:61940

配列が設定されていません。

バイトdiff:0



小さな変更:(61988-61940 =参照変数$ bを格納するための48バイト)。

そうでなければ、ループに使用される配列がそれ自体への複数の参照を持つ場合、例1の最適化は適用されないことがわかります。 元の配列が割り当てに使用されます。

ループに$ b配列を使用する場合、またはループ内で参照による値の転送を使用する場合、まったく同じ結果が得られます。

echo '配列が設定されています。'PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;



foreach $ a as $ k => $ v {

$ a [ $ k ] = someBigValue ; //または$ v = someBigValue();

設定解除 $ k、$ v ;

echo 「FOREACHサイクル」。PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;

}



「FOREACHの直後の使用法」をエコーします。PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;




結果:

バイトdiff:0

配列が設定されています。

バイトの差分:61940

FOREACHサイクル。

バイトの差分:61940

FOREACHサイクル。

バイトの差分:61940

FOREACHサイクル。

バイトの差分:61940

FOREACHサイクル。

バイトの差分:61940

FOREACHの直後の使用。

バイトの差分:61940

配列が設定されていません。

バイトdiff:0



ここで、参照による$ vの転送を追加すると、ソース配列の参照カウントは増加しませんが、「最適化」が無効になることに注意してください。



参照による転送またはコピーによる転送





メソッドまたは関数に非常に大きな値を転送する(またはそれらから返す)場合は、「何をすべきか」の場合を検討してください。 最初の明白な解決策は、通常、参照によるパス/リターンの使用を検討することです。

ただし、PHPのドキュメントには、 パフォーマンスを向上させるために参照渡しを使用しないでください。 PHPの中核は、最適化自体を行うことです。

それがどのような「最適化」であるかを理解してみましょう。



まず、最も単純な例(これまでのところ引数を渡さずに):

...

$ a = someBigValue ;

$ b = $ a ;



echo "文字列値が設定されました" ;

memoryUsage memory_get_usage 、$ base_memory_usage ;



設定解除 $ a、$ b ;

...


「ダイレクトロジック」では、変数の値用に2つのブロックをメモリに割り当てる必要があります。 ただし、PHPはこの点を最適化します。

開始する

バイトdiff:0

設定された文字列値

バイトの差分:15496

設定されていない文字列値

バイトdiff:0



この場合、変数$ aは15448バイトで占められ、残りの48バイトは変数$ bに割り当てられますが、それらの間にリンクはありません。 このメモリ消費は、これらの変数のいずれかを何らかの形で変更するか、実際には変更しなくても、その値で何かを実行するまで保持されます。

$ a = someBigValue ;

$ b = $ a ;

$ b = strval $ b ;



echo "文字列値が設定されました" ;

memoryUsage memory_get_usage 、$ base_memory_usage ;



設定解除 $ a、$ b ;




その結果、結論が得られます。

バイトdiff:0

設定された文字列値

バイトの差分:30896

設定されていない文字列値

バイトdiff:0



ご覧のとおり、変数$ bの値を「タッチ」しようとすると、スクリプトがそのストレージに別のメモリ領域を割り当てるようになります。 $ aの値を「タッチ」しようとすると、同じことが起こります。



この最適化は、配列の個々の値でもある特定の値に対して有効です。

これをよりよく理解するには、以下の例を見てください。

$ a = array someBigValue 、someBigValue ; // 31052バイト

$ b = $ a ; // + 48バイト= 31100バイト

$ b [ 0 ] = someBigValue ;



echo "文字列値が設定されました" ;

memoryUsage memory_get_usage 、$ base_memory_usage ;



設定解除 $ a、$ b ;




この例では以下が得られます。

バイトdiff:0

設定された文字列値

バイトの差分:46704

設定されていない文字列値

バイトdiff:0



つまり、結果として、$ b配列全体ではなく、配列のゼロ要素の値のコピーのみを作成するために、新しいメモリ(15k +バイト)が割り当てられました。 $ b [1]の値は、依然として$ a [1]と「最適に関連」しています。



上記のすべては、関数とメソッドの内外で「最適化されたコピー」を通じて値を転送/返すために同様に機能します。 メソッド内で渡された値を「タッチ」しない場合、別のメモリ領域は割り当てられません(メモリは変数名の下にのみ割り当てられ、値に関連付けられます)。 「コピー」を渡してメソッド内の値を変更した場合、変更を行う前に、値の実際の完全なコピーが既に作成されます。



このようにして、PHPはリンクの受け渡しを使用してメモリ使用量を最適化する必要性を実際に排除します。 参照渡しは、メソッドの外部からこれらの変更を表示するために元の値を変更する必要がある場合にのみ、実用的に重要です。



例のコード:

< php

include 'func.php' ;



関数testUsageInside $ big_value、$ base_memory_usage {

echo 「関数内での使用、その後$ big_valueは変更されません。」PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;



$ big_value [ 0 ] = someBigValue ;

echo '関数内での使用、その後$ big_value [0]が変更されました。PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;



$ big_value [ 1 ] = someBigValue ;

echo 「関数内の使用法は、その後$ big_value [1]も変更されました。」PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;



}



echo "アレイメモリの使用例。" ;

$ base_memory_usage = memory_get_usage ;

$ base_memory_usage = memory_get_usage ;



echo 「ベース使用量」。PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;



$ a = array someBigValue 、someBigValue 、someBigValue 、someBigValue ;



echo '配列が設定されています。'PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;



testUsageInside $ a、$ base_memory_usage ;



echo '関数呼び出し直後の使用法'PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;



設定解除 $ a ;

echo '配列未設定'PHP_EOL ;

memoryUsage memory_get_usage 、$ base_memory_usage ;

>




結論:

アレイメモリの使用例。

基本的な使用法。

バイトdiff:0

配列が設定されています。

バイトの差分:61940

関数内での使用は、$ big_valueは変更されません。

バイトの差分:61940

関数内での使用は、その後$ big_value [0]が変更されました。

バイトの差分:77632

関数内での使用も変更され、$ big_value [1]も変更されました。

バイトの差分:93032

関数呼び出し直後の使用。

バイトの差分:61940

配列が設定されていません。

バイトdiff:0



例からわかるように、値が実際にコピーによって転送されているという事実にもかかわらず、関数の配列のコピーは作成されませんでした。 また、転送された配列を部分的に変更しても、完全なコピーは作成されず、新しい値にのみメモリが割り当てられました。



情報提供のみを目的として、次の2つの値に注意する価値があります。

配列が設定されています。

バイトの差分:61940

関数内での使用は、$ big_valueは変更されません。

バイトの差分:61940



実際に新しい変数$ big_valueが出現しましたが、制御が関数に転送されたときにメモリ消費は増加しませんでした。 これは、スクリプトテキストの解析段階でも、インタープリターがこの関数をコードで使用するかどうかを決定し、入力パラメーターの名前にメモリ内の場所を事前に割り当てたという事実によるものです(関数が使用されない場合、インタープリターはそれを無視し、メモリを割り当てません)。 また、「コピーによる最適化された転送」が行われるため、既存の変数名$ big_valueは単純に暗黙的に大きな配列$ aに「接続」されました。 その結果、追加の1バイトを費やすことなく、値は「コピーを介して」関数に転送されました。



注:

PHP5では(PHP4とは異なり)、デフォルトですべてのオブジェクトが参照によって渡されますが、実際にはこれは劣ったリンクです。 こちらの記事をご覧ください。



簡単な結論



間違いなく、PHPでのメモリ使用量の最適化の例は「ドロップインザバケット」にすぎませんが、メモリ消費を最適化し、不要な頭痛からあなたを救うために選択するコードを考えることが理にかなっている最も一般的なケースについて説明します。



それとは別に、オブジェクトを使用するときにメモリを消費して最適化するメカニズムに触れることは価値がありますが、可能な例が豊富であるため、この点には別の記事が必要です。 たぶんいつか。



PS:これをいくつかの記事に分解することは可能ですが、そのような情報を「一緒に」保存する方がよいので、ポイントはわかりません。 この情報が実用的な意味を持つ人はより便利になると思います。 PHP 5.3.2(Ubuntu 32bit)でテストされているため、割り当てられたバイトの値は異なる場合があります。



より多くの便利なものが、英語で:

nikic.github.com/2011/12/12/How-big-are-PHP-arrays-really-Hint-BIG.html

nikic.github.com/2011/11/11/PHP-Internals-When-does-foreach-copy.html

blog.golemon.com/2007/01/youre-being-lied-to.html

hengrui-li.blogspot.com/2011/08/php-copy-on-write-how-php-manages.html

sldn.softlayer.com/blog/dmcaloon/PHP-Memory-Management-Foreach

blog.preinheimer.com/index.php?/archives/354-Memory-usage-in-PHP.html

derickrethans.nl/talks/phparch-php-variables-article.pdf



UPD

記事の主要部分は重要なポイントをカバーしていませんでした。

リンクが作成される変数がある場合、その変数が引数として関数に渡されると、すぐにコピーされます。つまり、コピーオンライト最適化は適用されません。

例:

< php

include 'func.php' ;

関数testFunc $ a、$ base_memory_usage {

memoryUsage memory_get_usage 、$ base_memory_usage ;

}

$ base_memory_usage = 0 ;

$ base_memory_usage = memory_get_usage ;

memoryUsage memory_get_usage 、$ base_memory_usage ; // 0バイト

$ a = someBigValue ;

$ b = $ a ;

memoryUsage memory_get_usage 、$ base_memory_usage ; // 15496バイト

testFunc $ a、$ base_memory_usage ; // 30896バイト

memoryUsage memory_get_usage 、$ base_memory_usage ; // 15496バイト

設定解除 $ a、$ b ;

memoryUsage memory_get_usage 、$ base_memory_usage ; // 0バイト

>




All Articles