戻る

PHP記事

May 16, 2020

PHPは面白い挙動が多く, 人口も多い人気言語であり最近PHP7になってさらにパワーアップしている. Nodejsが最近人気が出ているが, いきなりNodejsから始めるのはハードルが高くドキュメントも少ない. PHPは本当に丁寧に解説しているものが多くとても助かる.

パッケージマネージャー

PEARだとかパッケージマネージャーがいろいろあったがメンテナンスがされておらず非推奨となりPHP7.4あたりから標準でPHPに入らなくなった. その代わり最近ではComposerを使うのが流行っている. プロジェクトごとに使うライブラリのバージョンを管理でき, 古いバージョンに依存している場合でも別のプロジェクトでは最新バージョンで使う, といったことができる. そういう意味で, Composer is not a package manager in the same sense as Yum or Apt are. ということだ. しかしながらglobal指定をすることで全体にわたって同じバージョンを使うこともできる.

PHP5から大幅に進化したPHP7

PHP7は2016年ごろに10年ぶりにメジャーバージョンアップがなされたもので, 目覚ましい改良を遂げた. PHP7で追加された機能: PHP7で出来るようになったことを独断と偏見で紹介してみる

zvalの仕組みが大きく効率化された. 次の記事が詳しい:https://postd.cc/internal-value-representation-in-php-7-part-1/ これを読めば大体わかってしまうが, 概要はPHP5では変数がzvalを共有し参照カウントが対応し書き換えが起きたときにzvalを新たに生成したが, PHP7では変数は最初からその変数に対する一意的なzvalを持ち, int型では参照カウントを持たず最初からコピーしてしまいオブジェクトや配列などはポインタとして持ち参照先がzvalを管理するということになった. これだけで32byteのzvalが16byteだけになった. PHP5では変数がヒープ領域に積まれてヒープからzvalを参照していたが, 変数自体がzvalを持つためPHP5のzvalを丸々節約することができた. さらに間接参照が全て一つ減った, 一見zvalが生成される機会が増えたように見えるがヒープとは別にzvalを生成する必要がなくなっており, zval自体が小さくなったためこれだけでメモリが半分以下になる. 参照型の扱いもとても良くなった. is_refは廃止され代わりにref*がzvalとして追加された. refポインターは参照型zvalという特殊なzvalを参照し, 参照型zvalからさらにzvalを参照する. この間接参照を使うことで参照型で共有している二つの変数はrefポインターというzvalを持ちそれが一意的な参照型zvalを指すことになる. 参照型zvalは見えない変数として通常の変数と同じように解釈できるので, 例えばコピーオンライトは参照型でも通用するようになり参照型zvalが実体の値を書き換えない限りはzvalをコピーしなくて済む. これによって参照型の配列を値渡しするときにPHP5ではコピーする必要があったがPHP7ではコピーしなくてもよい. 参照型zvalを経由する間接参照が増えたが, このおかげで参照型の挙動がわかりやすくなった.

このPHP7の仕組みを知ってしまうと使わずにはいられない. 相変わらず参照型は参照される側も参照型に変換される, という挙動などPHP特有のものがある. 実際にコードで見てみよう. 参照型が参照先を別のものにするとき.


$a = 0;
$b = 1;
$c = &$a;
$c = &$b;
$c = 2;

この場合, $c = &$bではcの参照先が$aとは分離されて$bになるのかそれとも$aも同時に$bを参照するようになるのか. 結果は次になる.


$a = 0;
$b = 2;
$c = 2;

答えは前者であった. aはrefポインター型のままだろうか少し気になる. aが持つrefポインターが指す参照型zvalのref_countが1になるのだから, 参照型から元に戻すことができそうでその方が間接参照がへる. この挙動がどうなっているのか.

次の例を考える. 参照先が参照型だった場合は参照型への参照型となる訳ではなく, 同じ参照型zvalを参照するrefポインターとして参加するだけ.


$a = 0;
$b = &$a;
$c = &$b;
$c = 1;

この場合は$a=1となる. この点でPHP5と同様多重参照がされない構造となっている. しかしながら多重参照されないからと言って循環参照が起きない訳ではなく自己言及的(再帰的)な参照の場合どうするか. 一般に配列の要素として参照型を含んでいる場合に形式的には多重参照ではないが, ある要素を辿ると多重参照になっているという場合に問題が発生する.


$a = [1,2,3];
$a[] = $a[];
$a = 0;

[1,2,3]の実体のref_countは$a = 0;で2から1に減るが, 0にはならない. 変数ではアクセスできなくなったのにもかかわらず. この場合ちゃんとPHP7は処理してくれるのだろうか.

配列は複製されるがオブジェクト型は複製されないというのは, どちらもポインター型でアクセスするという点で変数の型としてはポインター型という形で違いはないが, 属性が配列ではcopyableでオブジェクト型はcopyableではないという違いから来ている. 型の違いで挙動を判別するだけでなく属性にも注意しなくてはならない. 属性としては他にもrefcounted, collectable, immutableなどがある. 属性はzval(16byte)のvalueメンバー(8byte)とは別のメンバー(8byte)にフラグとして管理されている.

関数の引数としてのスタンスは配列やオブジェクトを変更しないならば値渡しをするべきで参照型で渡してしまうと間接参照のアクセスコストが余計にかかる. 変更する場合は配列は参照型のままだが, オブジェクト型の場合はcopyableではないので値渡ししても変更が可能になっているので, 参照型を使って間接参照のコストをあげるべきではない.

このように考えていくとやはり参照型zvalのref_countが1になったときに参照型から値型に戻す挙動があった方が合理的に思える. 特に配列の場合は参照アクセスが必要になるが, 参照型のコピーの発生機会が大幅に減ったとはいえ間接参照のコストが多大だ.

サンプルケース2(foreachと参照) foreachに関しては癖があり展開してみるとわかりやすくforeach内の変数はスコープを外れても生き残っている. 再利用すると危険だったりそもそも再利用しなくても最後の要素を参照型のまま放置してしまったりと酷い. なおこの例をみることで上の疑問は解決し, 一度参照されても参照カウントが1になると値型に戻っている. と思いきやこの例の説明はPHP5でのzvalの仕組みを元にした説明となっている. あまり支障はなく, is_refはrefポインター型であるという解釈でOKだろう.

PHPの「参照」と配列の組み合わせに潜む罠 この例は配列と参照のトリッキーな挙動で有名な例だがPHPの仕組みを理解すれば納得できる. この例でも参照カウントが1になると値型に戻ることがわかる. この記事の最後のおさらいは難問で, 謎もある. この例を転載する.


$a = array(1, 2, 3); // 配列を準備
$c = &$a[1];         // 配列の要素への参照
$b = $a;             // 配列をコピー
$b[0] = 9;           // コピーした配列に変更を加える
unset($c);           // 「配列の要素への参照」をunset()
$b[1] = 0;
// $aの値は?
var_dump($a);

この結果は0となるが, $b[0] = 9;のときにb[1]はコピーが起きるのにもかかわらず, b[2]はref_countが2のままということが脚注2で謎として指摘されている.

すごく簡単なアルゴリズムがphpで書けなくてつらい PHPでは配列操作のアルゴリズムで良い関数が見つからないということがあるという話だがここでも参照を使うことのコストの悪さがうかがえるが, PHP7では参照型の値渡しではコピーがすぐに発生しなくなったため配列操作も改善されているだろう. この記事が言っていることは関数に配列を参照渡しするのでその関数内でcountを呼び出すと参照型を値渡しすることによるコピーによりコストがかかるため参照型を避けるために関数を使わずに関数の処理をベタ書きしてcountを呼び出すことで本来のパフォーマンスを出せるという. しかし関数が使えないというのは致命的であった, しかしながらPHP7では関数を使ってもコストが問題なくなった上, PHP7ではzvalの仕組み上大幅に効率化されたのでpythonよりも良いとさえ言えるかもしれない.

その他参考になる記事

PHP The Right Way

【PHP超入門】クラス~例外処理~PDOの基礎

【PHP超入門】Cookieとセッションについて

[PHP] リクエストパラメータ・セッションに関するまとめ

$_GET, $_POSTなどを受け取る際の処理

ファイルアップロードの例外処理はこれぐらいしないと気が済まない

[PHP] ファイルオープンモードに関するマニュアルの記述は間違っている

ぼくのかんがえたさいきょうのsession_start

セッションの有効期間とか設定とか挙動とかを調べました

https://tumblr.tokumaru.org/post/37676352092/session-adoption-and-session-fixation

PHPネイティブのDOMによるスクレイピング入門

PHP による HTTP 認証

namespaceとBOMに何の関係があるのさ

[シリアル版] ページング処理を採り入れた掲示板サンプル

ひと言掲示板を作る

PHP 【保存版!!】PHPからMySQLに接続する方法etc【データベース】