Archive for the ‘php’ Category.

phpフレームワークとの付き合い方

フレームワークを使うならメモリ消費について諦めなければいけない点かもしれません。

よくあるフレームワークで考えると、indexというメインコントローラがアクションクラスのインスタンスを作成します。
このメモリが解放されるタイミングは、どこになるでしょうか?
恐ろしい事に(ほぼ)全ての処理が終わってindexコントローラーからインスタンスが解放されるタイミングです。

なので、アクションクラスではprotectedやpublicのメンバ変数(プロパティ)はなるべく使用しない方が良いです。
インスタンスが解放される=メインコントローラーの最後までメモリ解放されません。

アクションメソッドの中でマスターデータなどを取得した場合もunsetしない限りはスコープから外れるまで解放されません。
スコープから外れる=アクション終了=(ほぼ)全ての処理が終わっている

オブジェクトを多用するのは、zend_class_entryに設計図が登録されるので、クラス数だけメモリを消費します。
クラスを一杯作成するのはメモリ消費には繋がります。
ですがstaticメソッドを持ったクラスを作るのは別に問題だとは思いません。
これはfunctionを作ってもfunctionが登録されるのと同じだからです。

どうせ数msで終わるのだから、頑張ってメモリ解放しなくても良いというアプローチこそ
lightweight languageに相応しい気がしますので、あまり神経質にならなくて良い気がします。
アクションクラスのメンバ変数に大きなマスターデータを持たせるとかは、やめた方が良いかもしれませんけども。

久しぶりに連日技術的なネタを投稿して充実していましたが、明日ソードアートオンラインのゲームが出るので若干更新ペースが落ちるかもしれませんw

php extensionのススメ3(メモリ消費)

同時アクセス数が多いという事はweb(ap)サーバーのメモリ消費に注意しなくてはいけません。
webサーバーレイヤーではnginxが有利だと言われる部分はアプリレイヤーでも考慮する必要があります。

phalconフレームワークとCakePHPで単純にHelloWorld的なプログラムを実行してみます。
テンプレートでmemory_get_usage()をechoするだけのプログラムです。

phalconフレームワークは400Kくらいです。
phalcon

cakephpは10倍の4Mくらいです。
cakephp

DB接続すらせずに、Actionも空っぽでテンプレートでecho memory_get_usage()しているだけのプログラムで10倍のメモリ消費となります。
phalconフレームワークは早いだけではなくメモリ消費という最重要項目に関しても優秀です。
これは、先日説明したphpのzvalを思い出して下さい。
c言語のネイティブレイヤーでスタック変数を定義するのと、phpレイヤーで変数を定義するのとではメモリ消費が違います。
同じ処理をするにしても、ネイティブ側で例えばint型(私のPCは32bitなので4バイト)で良い値があるとします。
phpレイヤーでしか定義出来ない場合は、まず$aというポインタ変数として(私のPCは32ビットなので)4バイト
ポインタの参照先の実態として、zval構造体+共用体のメモリを消費します。
ネイティブで済ませる部分が多ければ多いほど、zvalの呪縛から解放されメモリ消費を抑える事も出来ると言うことです。

Actionやテンンプレートが空っぽでこれなのですから、CakePHPをそのまま使ってしまうと1アクセス当たり平均10MBは覚悟しておいた方が良いかもしれません。
対象サーバーでの同時アクセス数を100に設定したと仮定すると軽く見て1GBは覚悟しておくべき数値かと思われます。
1000アクセスだとすると10GBですね。
ゲーム開発の場合はマスターデータをKVSから全件取得したりもしますので、多めに考えておいた方が良さげです。
あくまでアプリレイヤーのメモリ消費ですので、webサーバーは別途メモリを消費します。
そしてAPCにキャッシュさせたデータもメモリを消費します。
APCのコンパイルキャッシュもメモリを消費しますので、プログラムの本数分消費します。

その他サーバー監視プロセスやログ収集プロセスなど最低限動いているプロセスもメモリを消費します。
これから考えてもApacheとnginxの項でも書いた、C10Kという単位は普通に馬鹿かと言いたくなる単位なのは、ご理解いただけるかと思いますw

プロジェクトスケルトンの作成

プロジェクトの設定とかは全くしていませんが、とりあえず動きました。
これでチュートリアルに進めます。
phalcon

Phalcon Developer Tool

Phalconフレームワークの使い方を勉強していきたいと思います。

ここからPhalcon Developer Toolsをダウンロードしてコマンドラインからプロジェクトスケルトンが作れるようです。
http://docs.phalconphp.com/en/latest/reference/tools.html

好きな所にインストールすれば良いのですが、私は/usr/local/binにインストールします。

配列(ハッシュ)よりオブジェクトのメモリ消費が少ないという嘘?

前回のエントリーの続きです。

参考サイト①に記載されている内容は別に間違いではありません。
前提条件としてオブジェクトに有利な形になる形の説明をしているだけですね。
参考サイト①の参考リンクにある参考サイト②の説明をされているだけです。

まず参考サイト①にも記載されていますが、PHPの言語仕様の説明をします。
確かPHP5から変わったハズなんですけど

①は値のコピーでは無く、$aの実態への参照となります。
①のタイミングでコピーしてしまうとメモリの消費が激しいので、値に変化がない間はメモリを節約する為に言語仕様が変わりました。
なので初めて値が変わった②のタイミングで新しいzvalがメモリに確保されます。
xdebugをインストールすると簡単に確認できます。

①は$aしかないので$aの実態の参照カウントは1です。
②は$bからも参照されたので$aの実態の参照カウントは2になりました。
③は$aの実態を参照しているので②と同じ結果になります。
④は$bは値変更により新しい実態を持ったので参照カウントが$aの1つに戻りました。
⑤は$bの新しい実態なので参照カウントは$bの1つです

という基本的な言語仕様を前提条件として理解して下さい。

昨日説明したzend_class_entryというのがクラスの設計図だと思います。

この先はphpのユーザークラスをコンパイルした後の世界なので、ソースコードもよく分かりません。
public $member0 = 0;
例えば上記のようなプロパティに初期値が設定されている時は、
①0を表すzval
②default_properties_tableが①を参照
③new Classした時にnew_class.member0はdefault_properties_table[member0]を参照
という動きになるので参考サイト①のように参照カウントが3から始まるのだと思います。

この動きと配列を比較するのであればこういう形で初期化するのが平等な計測になります。

結果は当然配列の方が早くてメモリ消費も少ないです。

メモリ消費が少ないのは、昨日説明した階層を思い出して下さい。
内部的にはzval->obj->HashTableでありプロパティの実値部分のHashTable部分のメモリ消費が同じならobjの部分が配列よりも多くなります。
参考サイト②は間違いと言っている訳ではありません、あえて配列が不利になる動きで比較をしているという事です。
逆に言うと私があえて配列が有利になるように初期化しただけですw
同条件なら配列の方が早くて軽量!
これはphpの内部データの持ち方から間違いありませんから、私にとっては予想通りの結果になったつー事ですね。

如何だろうか?
ネットには単純に鵜呑みにしてしまうと嘘になってしまう情報がゴロゴロ転がっているのである。
真実はいつもひとつ!嘘じゃない情報も嘘になる事がある。

オブジェクトより配列(ハッシュ)の方が早い理由

このエントリーは別に配列を使っての高速化を推奨する訳ではありません。
オブジェクトなんて使うより配列の方が早いからオブジェクトと配列の選択肢がある時の参考にして貰えればという感じですね。
例えば散々例に上げているPDOも結果行の形式をオブジェクト・配列から選択するパラメーターがありますよね。

これを説明するに当たって、とても素晴らしいサイトを見つけました。
2009年と古い内容ですが、図解してくれているのでとても分かりやすいです。
参考サイト:http://d.hatena.ne.jp/yokkuns/20090614/1244994082

所謂何でも型というのはc言語の共用体の拡張だというのは、どの言語でも同じだと思います。
私が業務系でVBをやり始めた頃はC言語出身者もゴロゴロ転がってましたので、VBのVariant型は共用体拡張だと聞いていたので想像はついていました。
phpの何でも型の正体?呼び名?はzvalと言います。

zval_val共用体とzval構造体

参考サイトの図を合わせて見ながら読んで貰うと分かりやすいと思います。
zval構造体の中にtypeという項目があります。
これが何でも型だけど内部的にstring型だとか持ってる奴ですね。
図の右に飛ぶと共用体に移ります。
共用体っつーのは同じメモリ領域を複数の型で共用できるそのまんまの奴なんですが、c言語を知らない人は分かりやすい図入りのサイトを探して下さい。
大事なのは、配列はHashtableで、オブジェクトはzend_object_valueで型が違う=データの持ち方が違うという事です。

脱線してzval構造体のrefcount_gcがガベージコレクタの説明によく出てくる参照カウンタつー奴ですね。
GCはこのヒープ変数がどこからも参照されていない時(=0)に解放する仕組みです。
面白いのでついでに覚えておくと自己満足できます!
オブジェクト側にもrefcountとか出てくるので、GC側のソースも何かあれば見てみたいと思います。

zend_object_value構造体

ここからの説明の仕方に迷うというか文章力を試されてしまいますね。
まずは、表題通りの何故オブジェクトの方が遅いのか?について説明したいと思います。
参考サイトで図解してくれていますので私が簡単に書くのと合わせて見て下さい。
ハッシュの場合は
zval->value->ht->zval->value
オブジェクトの場合は
zval->value->obj->(Hashtable)get_properties->zval->value
単純に矢印が多い=目的の値に辿り着くまでの経路が多いと思って下さい。
参考サイトにも書かれているように、最終的にオブジェクトのプロパティは内部的にはハッシュとして扱われるんです。

で、オブジェクトはクラスの型を管理する情報と実際にヒープメモリに展開したインスタンスの情報を管理してるぽいですね。
やたらとceという変数が出てきますが、これがzend_class_entryという奴でクラスの設計図の情報ぽいですね。
さっきのzend_object_valueのhandleとhandlersというのがインスタンス側の管理情報で、グローバル変数objects_storeで管理してるぽいです。

その中のバケットという中にオブジェクトを詰め込んでいってるポジションがhandle的な感じぽいです。

糞長すぎて混乱してしまいますねw
実際に使用されている部分を抜粋するとこんな感じです。
EG(objects_store).object_buckets[handle].bucket.obj;

さらに最終的にこのバケットに入ってる_store_bucket->obj->objectがこれになるんだと思うんですが、ちょっとそろそろ眠たいのでソース見て確証するのは後日で・・・。

なので、さっき書いた
zval->value->obj->(Hashtable)get_properties->zval->value
を詳細に書くと
properties_hash = zval->value->obj->handlers->get_properties(EG(objects_store).object_buckets[zval->value->obj->handle].bucket.obj);
多分こんな感じの糞長い動きでようやくプロパティ配列が取れる感じだと思います。

さて配列の方が早い理由はご理解頂けたでしょうか?
分かり難い人は

より

の方が早いのと同じだと思って下さい。
$hoge->0->1->$iより1->$iの方が目的の場所に辿り着く経路が短い=早いです。

次に、参考サイトに書かれているオブジェクトのメモリ消費量について書きたいと思いますが、やっぱり長くなったのでエントリーを分けたいと思います。
to be continued

クエリ発行回数の削減(SELECT)

MySQLのプロシージャは複数結果セットに対応しています。
これを利用しない手はありません。
昨日、関連テーブル一括削除のエントリーで1:nの親子関係テーブルを複数JOINするとねずみ算式に結果セットの行数が膨らむ話をしました。
そのようなケースでも利用できますし、JOIN出来ない関係ないデータを1クエリで同時に取得するという用途にも利用出来ます。

1:nの子テーブルがあるパターンです。
取得するカラム数も結果セット毎に違います。

スクリプトを準備します。
PDOには複数結果セットの為のPDOStatement::nextRowsetというメソッドが準備されています。

3回クエリを発行するのと同じ結果が1クエリで取得出来ます。

比較の為に普通に3回SELECTするテストスクリプトも用意します。

今回はループじゃなくてabでベンチマークを取ってみます。
ネットワークを介するデメリットが対象なのでDBサーバーとWEBサーバーはちゃんと別のVMになっています。
私のPCはシングルコアの糞PCにLubuntu入れて延命してる環境なので負荷は軽めにしています。

秒間38.35リクエストが64.57リクエストまで跳ね上がっています。
ネットワークを介するデメリットは想像以上ではないですか?
スレーブから取得するデータであれば、IF文等の制御ロジックを含むプロシージャも許容範囲となるかもしれません。

php extensionのススメ2(phalconフレームワーク)

結局phpロジック側で稼げるのは数msの積み重ねなので、諸悪の根源であるフレームワークをネイティブ化すれば最も効率が良いという結論にたどり着きます。
最速PHPフレームワークphalcon
早速インストールしてみました。
phalcon

公式サイトに各フレームワークとの速度比較も乗っています。
ライセンス問題がどうなったのかよく分からないCodeIgniterがPHPフレームワークとしては最速ですが、それと比較しても半分どころじゃない圧倒的ですね!
Hello World Benchmark

ドキュメントも充実していますし、これからこのフレームワークについて勉強してみたいと思います。
ソーシャルゲーム開発ではデファクトスタンダードになっても不思議ではありませんね。
キャッシュもMemcachedとAPCに標準で対応していますね。
https://github.com/phalcon/incubator
これを使えばRedisも対応出来るっぽいですけど、phpクラスになるので悩みどころですね。

クエリ発行回数の削減(複数テーブルinsert)

getLastInsertIdのエントリーで説明したシチュエーションの続きです。
親・子テーブルを一括で登録するシーンが前提となります。
mysqlのプロシージャを使うと一括で登録出来ます。
勿論レプリケーション構成でも問題ありません。
2014.4.15追記:私のテスト環境だと問題なく動きますしPDO::rollbackも効くのですが、procedureが別トランザクションになる環境もあるようです。
どこの設定の違いなのかは掴めていません。

登録された主テーブルのlast_insert_idは、プログラム側で必要になるという前提で考えています。
プロシージャの場合は、最後にSELECT文を投げることで結果も返せるというのがポイントです。

普通のinsertのテスト関数を用意します。

プロシージャ利用のinsertのテスト関数を用意します。

複数クエリ一括投げinsertのテスト関数を用意します。
これはlast_insert_idを取るためにクエリを2回投げてしまっています。

ローカルPC内のVMとは言え、ネットワークを介した通信誤差の影響を受けるので、ある程度の回数で試します。
1000回くらいループさせてみます。
batch_insert2はlast_idを取得しないデータ作成の1クエリのみです。

当然の結果ですが、通信回数の差が顕著に出ていますね。
1000回ループなので1回当たり約12.5msがプロシージャ化で約6.8msになる感じですね。
勿論insertするサブテーブル数が増えるともっと差が出ます。
php-extensionの項でも書きましたが、数msの積み重ねが処理速度の向上に繋がりますから馬鹿には出来ません。

last_insert_idが必要ない前提で考えた場合、バッチクエリのやり方でフレームワークとして処理の最後に1クエリだけで更新系を済ませるというのもアリかもしれないですね。

【朗報】PDOのgetLastInsertIdメソッドは通信なし

ソーシャルゲーム開発においてクエリの発行回数を削減するのは非常に有用です。
DBサーバーへのネットワークを介する回数自体が変わるのですから。
ネットワークを経由する事の問題点はApacheとnginxの項でも書きました、ハードウェアの物理構成まで関係してきます。
単純にローカルネットワークだったら早いとかの問題でもありませんので、そこら辺はインフラ屋さんが考えるんでしょうね。

ソーシャルゲーム開発は、テーブル設計にしてもあえて正規化しないといった手法が取られます。
なのでユーザー情報に紐づくサブテーブルがたくさんあったりすると思います。
user_table
user_hoge1
user_hoge2
みたいな感じですね。
ユーザー登録時に限らず、サブテーブルを含めて一括でデータを作成するシチュエーションを想定しています。

普通にphpで作成すると、3テーブル作成するなら3回もinsert文を発行する事になります。

ここでPDOのlastInsertIdメソッドが、SELECT LAST_INSERT_ID()を発行してたら4回になるなぁと気になって調べました。

mysqlのmysql_insert_idというAPIを利用しているだけなんですけど、通信が発生するかどうかはAPI側のソースを見ないと分からないですね。

なので手っ取り早くtcpdumpしてみました。

hogeのinsert後にgetLastInsertIdをしていますが、直後にhoge2のinsertが来ていますので通信は発生していないようです。
driver_data->serverしか引数に渡してないから通信してそうだなぁと思ったのですが、これは予想がハズれてラッキーでした。
何らかの理由でSELECT LAST_INSERT_ID()クエリを投げている人はPDOのメソッドを使ったほうが良さげです。