このサイトには検索ワードの『PHP unset』で訪れる人も多い。
PHPでunsetの挙動を調べたいと考える人のパターンは大抵次の通りだろう。
・変数の初期化をしたいからunsetを使おうと思う
・メモリー不足でスクリプトが止まったから明示的に開放したい
朝から晩まで、猛烈にPHPでコーディングし続けている私の経験に基づき、これらの要求の是非を検討してみよう。
○変数の初期化をしたいからunsetを使おうと思う
PHPでは値未入力の変数をis_nullで判定できる。unsetした変数はnull状態になり、is_nullで判定できるようになるため、C言語的にローカル変数の使用前に初期化するというコーディングスタイルの場合に、その初期化関数として用いる事になる。
C言語などの開発経験があると、いきなり変数を使用するというのには違和感があるし、PHPではいきなり値を代入できるわりには、いきなり返り値用変数として関数に渡したりするとワーニングになるという仕様がある。そこでnull初期化のためにunsetを使うわけだ。ところが、このunset。本来の意味は『変数への割り当てを開放』であり、nullになるのはその副作用に過ぎない点に注意する必要があるのだ。
実はunset($a)と$a=nullの結果は、nullを設定されてis_nullがtrueになるのだけど、unsetをクラスメンバー変数に使用した場合に悲劇が起こる。それは『クラスメンバー変数の割り当てを解放』という動作になり、なんとメンバー変数が消えてなくなるのだ。
この動作はJavaScriptと同様に、オブジェクトを連想配列の一種として実装しているからではないかと想像するのだけど、この結果、$obj-≫aというようなアクセスが以降できなくなってしまうのだ。
C++的なやり方でコンストラクタで、全てのメンバー変数を明示的にnullにするというような処理をunsetで行なうと、new時には問題はなにもなく、エラーさえでないが、実際にメンバー関数を使うタイミングで、オブジェクトに存在しないプロパティへのアクセスというエラーがでて悩まされる原因となる。クラスメンバーとしてバッファ的なものを用意している場合などでも、自然とそのバッファよりもオブジェクトの存在期間が長くなるので、処理の最後や開始時にバッファを開放するという事をする必要があるので、そこでunsetを使うとよく分からないタイミングでエラーになったりするわけだ。
従ってunsetは配列から要素を取り除くような本来の機能の範囲で使用し、nullにするのは代入で行なうというのが良いと考えている。
○メモリー不足でスクリプトが止まったから明示的に開放したい
PHPでは参照が全てスコープ外になった時点で、参照しているデータやオブジェクトが開放されるという事になっている。この事になっているというのは残念な言い方なのだけど、そうは思えない動作をする場合があるのだ。
例えば、このスコープというのが非常に怪しい。特に怪しいのは、オブジェクトのメンバー変数が他から参照された場合にどうしているのかという点だ。実例を挙げると、PEAR::XML_RSSにおいて、
for ($i = 0; $i ≪ 10; ++$i)
{
$obj = new XML_RSS(URL[$i]);
unset($obj);
}
等とした場合、きちんとunsetで開放されるので、延々とメモリが増え続けるという事がないのだけど、
for ($i = 0; $i ≪ 10; ++$i)
{
$obj = new XML_RSS(URL[$i]);
$obj-≫parse();
unset($obj);
}
とやると、延々と使用メモリーが増えていくのだ。
恐らく、検索ワードで『PHP unset』として検索していた人は、このような問題が発生したからこの問題の解決方法を探していたのではないだろうか。
このコードの問題点は、なぜparseを呼び出しただけで$objの参照が開放されないのかという点である。これが、
XML_RSS::parse($obj);
とかならまだ分からなくもない。あとは、どこかグローバルに配列があって、parse内で$thisを格納しているとかでもないと起こりえないはずなのだが、実際にそんなコードはない事は確認済みである。
原因はなんであれ、一応これに関しては解決法は分かっている。それは、オブジェクトの全メンバー変数をunsetしてオブジェクトをunsetすると開放されるのだ。
この挙動からみても、どこかに$objが登録されているとか、メンバー変数の参照が保持されているとかそういったものではなくて、メンバー変数の参照が使われた時に、そのスコープから外れても参照カウンタが適切に処理されないと思える。
少なくともunsetは必要な参照を強制的に消すものではないので、上記手法で開放されるという事は問題ないという事だと思うし、実際に問題は生じていない。
○余談:メモリの使用量に関して
C言語系のメモリー管理を自分でやる必要がある言語以外からPHPに入ってきた人がメモリーオーバーに遭遇した時に、恐らく悩みの種になる事がらについてもアドバイスしておきたいと思う。
それは、処理前と処理後でmemory_get_usageしたときに、適切にメモリ開放させているつもりなのにメモリーが増えていくという現象についてである。これは、メモリーリークではなくて、C言語系では『メモリープール』という実装テクニックが引き起こしている現象だと私は考えている。
メモリープールとは、メモリーを解放したときに毎回正直に開放するのではなく、メモリープールと呼ばれる場所にメモリーを置いておき、次に同じ(場合によってはそれより小さい)メモリーが要求された時にその場所からメモリを取り出洲すことでメモリ確保を高速化するという手法である。
従って、上記の現象は文字列やクラスといった不定サイズのメモリ確保が行なわれる物を使用した場合は頻繁に発生する現象である。
これ以外にも、システム系の処理で初めて関数が呼び出された時にメモリが確保され、以降終了まで確保し続けるという系統のメモリ確保もあるようなので、
≪?php
echo memory_get_usage() . PHP_EOL;
…
echo memory_get_usage() . PHP_EOL;
exit(0);
として、開始直後のメモリーと最後のメモリーを確認する方法はあまり意味がない。
この関数の使いどころは、newとunset、mysqli_result::mysqli_free_result等といった明示的にメモリの取得、開放を行なうループの前後で『だんだん増えていかないかどうか』を調べる事が最も有効である。
増えたり減ったりの減ったりが発生したら問題ないとみなしてよいだろう。
PHPでunsetの挙動を調べたいと考える人のパターンは大抵次の通りだろう。
・変数の初期化をしたいからunsetを使おうと思う
・メモリー不足でスクリプトが止まったから明示的に開放したい
朝から晩まで、猛烈にPHPでコーディングし続けている私の経験に基づき、これらの要求の是非を検討してみよう。
○変数の初期化をしたいからunsetを使おうと思う
PHPでは値未入力の変数をis_nullで判定できる。unsetした変数はnull状態になり、is_nullで判定できるようになるため、C言語的にローカル変数の使用前に初期化するというコーディングスタイルの場合に、その初期化関数として用いる事になる。
C言語などの開発経験があると、いきなり変数を使用するというのには違和感があるし、PHPではいきなり値を代入できるわりには、いきなり返り値用変数として関数に渡したりするとワーニングになるという仕様がある。そこでnull初期化のためにunsetを使うわけだ。ところが、このunset。本来の意味は『変数への割り当てを開放』であり、nullになるのはその副作用に過ぎない点に注意する必要があるのだ。
実はunset($a)と$a=nullの結果は、nullを設定されてis_nullがtrueになるのだけど、unsetをクラスメンバー変数に使用した場合に悲劇が起こる。それは『クラスメンバー変数の割り当てを解放』という動作になり、なんとメンバー変数が消えてなくなるのだ。
この動作はJavaScriptと同様に、オブジェクトを連想配列の一種として実装しているからではないかと想像するのだけど、この結果、$obj-≫aというようなアクセスが以降できなくなってしまうのだ。
C++的なやり方でコンストラクタで、全てのメンバー変数を明示的にnullにするというような処理をunsetで行なうと、new時には問題はなにもなく、エラーさえでないが、実際にメンバー関数を使うタイミングで、オブジェクトに存在しないプロパティへのアクセスというエラーがでて悩まされる原因となる。クラスメンバーとしてバッファ的なものを用意している場合などでも、自然とそのバッファよりもオブジェクトの存在期間が長くなるので、処理の最後や開始時にバッファを開放するという事をする必要があるので、そこでunsetを使うとよく分からないタイミングでエラーになったりするわけだ。
従ってunsetは配列から要素を取り除くような本来の機能の範囲で使用し、nullにするのは代入で行なうというのが良いと考えている。
○メモリー不足でスクリプトが止まったから明示的に開放したい
PHPでは参照が全てスコープ外になった時点で、参照しているデータやオブジェクトが開放されるという事になっている。この事になっているというのは残念な言い方なのだけど、そうは思えない動作をする場合があるのだ。
例えば、このスコープというのが非常に怪しい。特に怪しいのは、オブジェクトのメンバー変数が他から参照された場合にどうしているのかという点だ。実例を挙げると、PEAR::XML_RSSにおいて、
for ($i = 0; $i ≪ 10; ++$i)
{
$obj = new XML_RSS(URL[$i]);
unset($obj);
}
等とした場合、きちんとunsetで開放されるので、延々とメモリが増え続けるという事がないのだけど、
for ($i = 0; $i ≪ 10; ++$i)
{
$obj = new XML_RSS(URL[$i]);
$obj-≫parse();
unset($obj);
}
とやると、延々と使用メモリーが増えていくのだ。
恐らく、検索ワードで『PHP unset』として検索していた人は、このような問題が発生したからこの問題の解決方法を探していたのではないだろうか。
このコードの問題点は、なぜparseを呼び出しただけで$objの参照が開放されないのかという点である。これが、
XML_RSS::parse($obj);
とかならまだ分からなくもない。あとは、どこかグローバルに配列があって、parse内で$thisを格納しているとかでもないと起こりえないはずなのだが、実際にそんなコードはない事は確認済みである。
原因はなんであれ、一応これに関しては解決法は分かっている。それは、オブジェクトの全メンバー変数をunsetしてオブジェクトをunsetすると開放されるのだ。
この挙動からみても、どこかに$objが登録されているとか、メンバー変数の参照が保持されているとかそういったものではなくて、メンバー変数の参照が使われた時に、そのスコープから外れても参照カウンタが適切に処理されないと思える。
少なくともunsetは必要な参照を強制的に消すものではないので、上記手法で開放されるという事は問題ないという事だと思うし、実際に問題は生じていない。
○余談:メモリの使用量に関して
C言語系のメモリー管理を自分でやる必要がある言語以外からPHPに入ってきた人がメモリーオーバーに遭遇した時に、恐らく悩みの種になる事がらについてもアドバイスしておきたいと思う。
それは、処理前と処理後でmemory_get_usageしたときに、適切にメモリ開放させているつもりなのにメモリーが増えていくという現象についてである。これは、メモリーリークではなくて、C言語系では『メモリープール』という実装テクニックが引き起こしている現象だと私は考えている。
メモリープールとは、メモリーを解放したときに毎回正直に開放するのではなく、メモリープールと呼ばれる場所にメモリーを置いておき、次に同じ(場合によってはそれより小さい)メモリーが要求された時にその場所からメモリを取り出洲すことでメモリ確保を高速化するという手法である。
従って、上記の現象は文字列やクラスといった不定サイズのメモリ確保が行なわれる物を使用した場合は頻繁に発生する現象である。
これ以外にも、システム系の処理で初めて関数が呼び出された時にメモリが確保され、以降終了まで確保し続けるという系統のメモリ確保もあるようなので、
≪?php
echo memory_get_usage() . PHP_EOL;
…
echo memory_get_usage() . PHP_EOL;
exit(0);
として、開始直後のメモリーと最後のメモリーを確認する方法はあまり意味がない。
この関数の使いどころは、newとunset、mysqli_result::mysqli_free_result等といった明示的にメモリの取得、開放を行なうループの前後で『だんだん増えていかないかどうか』を調べる事が最も有効である。
増えたり減ったりの減ったりが発生したら問題ないとみなしてよいだろう。


