がるの健忘録 このページをアンテナに追加 RSSフィード

2017-03-22

[][]CAS実装……の前提のお話

ふと、色々と紆余曲折があった末として「MagicWeaponのdata_clumpで、cas形式の楽観的ロック、実装してみようかしらん?」というような発想がありまして。

その前提として「cas tokenど〜やって実装しよう?」から、比較的現実的な可能性があるあたりを妄想して、その辺の実装の欠片を思いついたので、おおむね、メモ用途でw


端的には「bigint(8byte) unsigned相当の整数」をランダムで持たせたら、大体、トークン足りえるんじゃないかなぁ? と、妄想をしまして。

8byteなら、index切っても、メモリ空間の圧迫が「そんなもんだろう」程度、だろうし。

渡し方については……MySQLであれば……二種類あるなぁ。


mysql> SELECT CAST(0xffffffffffffffff AS UNSIGNED);

+--------------------------------------+

| CAST(0xffffffffffffffff AS UNSIGNED) |

+--------------------------------------+

| 18446744073709551615 |

+--------------------------------------+

mysql> SELECT CONV('ffffffffffffffff', 16, 10);

+----------------------------------+

| CONV('ffffffffffffffff', 16, 10) |

+----------------------------------+

| 18446744073709551615 |

+----------------------------------+


………ん?

mysql> SELECT CAST('0xffffffffffffffff' AS UNSIGNED);

+----------------------------------------+

| CAST('0xffffffffffffffff' AS UNSIGNED) |

+----------------------------------------+

| 0 |

+----------------------------------------+

あぁ文字列にすると駄目なのか。

って事は、CONV関数だな。こっちなら「文字列」として渡せる。

まぁ、MySQLに方法があるんだ、ほかのRDBでもあるだろうw(ざつ)


んで。

だとするとあとは「8byte整数相当の値をランダムにとってくる」と、なんとなく行けるっぽくて。

後は「重複しねぇよなぁ」を、雑にテストしてみるとよいんじゃなかろうか、って思う。


Linux的に手っ取り早いのは /dev/urandom デバイスでしょ、ってんで、確認かねて、以下のコードを動かしてみた。

<?php

$data = [];
for($i = 0; $i < 100000; ++$i) {
    $h = bin2hex(file_get_contents('/dev/urandom', false, NULL, 0, 8));
    if (false === isset($data[$h])) {
        $data[$h] = 1;
    } else {
        echo "ng... \n";
    }
}

重複が発生したら教えてくれる、的な子。

とりあえず10万回。

……ま〜ま〜時間かかるのな。ほんのわずかにびっくりしたおw


何度か動かしてみましたが、(おおよそ予想通り)一度もぶつからなかったので。

多分、いけるんじゃないかなぁ、的な。


どこかで、気が向いたり体力が向いたりしたら、CASを実際に実装してみませう。

って事で、とりあえず「実装するために一番重要な部分」を、軽くテストしてみました。


突っ込みなどあったら歓迎いたしますので、是非。

2016-08-10

[][]before_filter的なほにゃらら、の想定

元ネタは、友人の

会員登録に住所が必要な場合の「数字は全角で」「数字は半角で」もそろそろどうにかならんのか

ってあたりの発言。


まぁ実際「住所入力全角のみ」で、番地数半角ではねられていらついたとかいうケースは割と後を絶たないわけでして。

その辺りを考えると、入力値受け取る時に

・半角になぁれ

・全角になぁれ

・カンマ(,)はいらねぇっつってんだろ!!(数値入力)

など、いくつか定型的な「事前に置換したり除去したりしたいパターン」ってのはあるような気がするんですね。

記憶をほっくり返しても確かに組んでた記憶があるし。


んで「なら、set_valueあたりでフックして、自動で置換したり除去したりすればいいんぢゃね?」って思いましたのが事の発端。

実装イメージ的にはvalidateと似ている感じかなぁ。


設定は、set_element() 辺りで

  $this->push_before_filter('hoge', 定数 | 定数); // 
  $this->push_before_filter('foo',  定数 | 定数); // 
  $this->push_before_filter('bar',  定数 | 定数); // 

的な感じで。


後は、普通にset_valueするタイミングで逐次、filterが適用される的な。

微妙に悩んでるのは、以下のあたり。


本来的には「手で入力した際」のフック用、なのだよねぇ。

それを考えると、なんとなし「set_from_cgi*()」がフック場所なのだけど。

「手動で普通にごりごりとset_valueしてるコード」の存在もあり得るので、そうすると「フック場所はset_value」のほうが、性能を考えなければ、より確実。

ただ「すでにfilter済みのデータも毎回filter」ってのも些か処理的に重いのでは? ってのもあって。

そうすると「フック場所はset_valueで、引数フラグを追加、"明示的にfilterをすっ飛ばす"オプションがある」にするとよいのかなぁと思うんだけど、「手間」なのと「DBからの入力なんだけど信用できない」とかいう特殊ケースをどうすっかなぁ? 的な問題。


とりあえず今のところ何となく

・フック場所はset_value

引数に「明示的に指定すると"filterを通さない"意思表示が出来る」ように改造

・data_clumpがもっている、いくつかのメソッドは「filterを通さない」「filterを通さなくする事ができる」ように改造

って方向かなぁ、と思ってる。

あとは作ってみて使ってみて改良かなぁ?


filterは、候補としては

・数値を全て半角にする

・数値を全て全角にする

・英字を全て半角にする

・英字を全て全角にする

・英字を全て「大文字」にする(最終データが半角データのみ)

・英字を全て「小文字」にする(最終データが半角データのみ)

・全角スペースを「半角」に変換

・半角スペースを「全角」に変換

・全角カタカナを「全角ひらがな」に変換

・全角ひらがなを「全角カタカナ」に変換

・半角カタカナを「全角ひらがな」に変換

・半角カタカナを「全角カタカナ」に変換

・全角の長音の類い(調べないと…)をハイフン(半角)に変換

・全角の長音の類い(調べないと…)を長音(ー)に変換:揃える、とも言う

・ハイフン(半角)を長音(ー)に変換

・カンマを除去する(半角、全角とも)

・スペースを除去する(半角全角とも)

・改行を除去する(\r\nとも)

くらいかなぁ?

まぁ必要に応じてこの辺も追加。


単純に「必要要素」だけだと面倒な気もするので。

セットメニュー的なのも作っておくと良さそうかも。

・「数値を全て半角」+「英字を全て半角にする」+「全角スペースを「半角」に変換」+「全角の長音の類い(調べないと…)をハイフン(半角)に変換」の、「半角セット」

とかとか。

セット的には

・半角セット

・全角セット

・半角数値入力セット(半角数値系+カンマ除去)

あたりがとりあえず有力かなぁ。


あと、もっと大前提として「filter」でいいのかしらん? 的な、命名の問題。

「before_filter」って名前も、微妙におよそと混ざりそうで、幾分躊躇してる感じw

なんかいい命名ないかしらん?

意味合いとしては「入力されたデータを、置換したり一部除去したりして整えたり揃えたりする」感じ。


…なんてことを、もわもわ。

意見コメント突っ込みその他、ありましたらお気軽に是非!!

2016-06-19

[][]phpのsession関数を使ってのギミック

PHPのセッションは、便利な反面色々と気になる事もあるのですが。

そのうち

・「セッション有効時間」をきっちりしたい(有効時間が切れたら問答無用で切断したい)

・アカウントロックの実装をかけて、ロックしたら「ログイン出来ない」だけじゃなくて「現在ログイン中のアカウントもシャット」したい

のあたりを解決しようかなぁ、と思いました。


細かい所をかくと

・「自分のアカウントに紐付いているセッションIDのうち、現在のセッション以外を全部遮断」(gmailの「アカウント アクティビティ」の「他のすべてのWebセッションからログアウト」)

も実装したいのだけど…ここに手を入れるのは些か難儀なので、一旦見送り。


で、実装方法を簡単に。

基本は、いわゆる「認可(authorization):ログイン中のユーザかどうか」の判断ロジックのところで、少しギミックを入れました。

その手前として、ログイン(認証:authentication)のタイミングでも、ギミック追加しています。


寿命については、大まかには

・ログイン時に、$_SESSIONに「有効時間」を入れる

・認可の度に

・・有効時間を超えてたら問答無用でログアウトさせる

・・有効時間範囲内なら「有効時間を更新して」認可する

ってロジック。


また合わせて、認可のタイミングのラスト付近で「ロックテーブル」なるテーブルを1枚用意して。最低限としては「ユーザID」だけがカラムにあればよくて、判断としては

・ロックテーブルに「認可を試みてるユーザのユーザIDが存在してたら、認可しない」

って感じで「問答無用でロック」が出来るようにしました。

DBアクセスが1つ増えるのが微妙っちゃぁ微妙なんだけど、まぁその辺はトレードオフかなぁ、と。

基本「KVS的なアクセス」でよいので、その辺で色々と小細工をしてみてもよいだろうし。


たいした実装でもないのですが、色々と「ちょっともにょってた」ところが実装できたので、割と満足でございます。


実装サンプルはこちらからどんぞ。

https://github.com/gallu/MagicWeaponTest

の「lib/common/base_model_admin_base.inc」と、

https://github.com/gallu/MagicWeapon

の「authorization_session.inc」

あたりを追いかけていくと、大体、処理が点在しています(笑


あとは…微妙っちゃぁ微妙なのですが

・そもそも「Anonymouse セッション(ログインしてなくても始まるセッション)」ってどうなんだろう?(セッションアダプションはやっぱりちょっと気になる)

・「他のすべてのWebセッションからログアウト」入れたいよねぇ

あたりになると、セッション周りを「自作しなおすかねぇ( http://d.hatena.ne.jp/gallu/20160402/p1 )」という話になって。

ただ、この辺になると上述で言うところの「authorization_session.inc」の処理にも手を加える(ってか多分、別クラスにする)ってのもあって…ってのが、「セッションIDを作ったり設定したりする」辺りを全部自前実装にする気ぃ満々なのでw

興味はあるんだけど少し時間がかかりそうで……次回短期入院の時の課題かなぁ、とw


まぁ「なければ作る」はUNIX系列の伝統的作法だと思うので。

便利なPHPの機能は最大限「そのまま使いつつ」、不便な所をラップしていくのが最近のおいちゃんの好みかなぁ、という辺りでの邁進を想定しておりまするる。

2016-06-10

[][]data_clumpの使い方(ざっくり版)

ほぼ私信のようなもんですがw

細かくはまたマニュアル( https://github.com/gallu/MagicWeaponManual/blob/master/table_of_contents.md )のほうに書きますが、一端、メモ程度に。


本質的なところ

data_clumpは「データの塊」です。

例えば「1つのmail form」は一塊のデータだと思われますし、DBの1テーブルなんてのも、一塊のデータだと思われます。

そんな「一塊」ごとに1クラスをアサインしているのがdata_clumpです。


下ごしらえ

とりあえず「data_clump継承クラス」を作成してください。

場所は、MagicWeaponの流儀をそのまま持ち込むのであれば、libディレクトリにclumpってディレクトリを切って、そこに置くとよいです。

また必須ではないのですが、clumpディレクトリの中にbaseってディレクトリを切っておくと、「Generation Gapパターン」的な意味でよりよいです。


例えば。

http://furu.mwtest.gjmj.net/

で動きを見ていただけて、ソースコードは

https://github.com/gallu/MagicWeaponTest

にあるのですが。

「1つのform」を構成するクラスは、

https://github.com/gallu/MagicWeaponTest/blob/master/lib/clump/base/form_test_clump_base.inc

https://github.com/gallu/MagicWeaponTest/blob/master/lib/clump/form_test_clump.inc

の2つのクラスに分解すると、色々とはかどります。


「clump/base/*_clump_base.inc」は、大まかには「このデータの塊は具体的にどんな要素をもっているのか」が記述してあります。

「clump/*_clump_base.inc」は、data_clumpの本体になります。

formだと難しいのですが、「DBのテーブル」単位でclumpを作る場合は、MagicWeaponをインストールしたディレクトリのtool/soak_up_information.php( https://github.com/gallu/MagicWeapon/blob/master/tools/soak_up_information.php )ってのを叩くと、自動でコードを生成してくれます。

もうちょっと正確に書くと「baseは常に上書きで生成(するからこのファイルは追記とかしないようにしたほうが無難)」「clumpは"なければ作る"」って動きをします。


以下、上述のファイルを作ってる前提で話をすすめます。


formからのデータの取得とかvalidateとか出力とかその辺

ものすごく大まかには、以下のようなコードで大体処理をします。

https://github.com/gallu/MagicWeaponTest/blob/master/lib/form_confirm.inc

(ちょっとコメント変えてます)

  // data_clumpインスタンスを取得
  $form_test_c = $this->get_clump('form_test_clump');

  // 「cgi requestから」データを取得
  $form_test_c->set_from_cgi($this->get_cgi_request());

  // validate(定型のみ)
  if (false === $form_test_c->is_valid($this->get_conv())) {
    $this->forward('form_input');
    return ;
  }

  // セッションに保存
  $_SESSION['form_test'] = $form_test_c->get_all_data_to_hash();

  // 表示
  $this->get_conv()->set('form', $form_test_c->get_all_view_values());

インスタンスは「newで作る」でもよいのですが、MagicWeaponのMVCを使っている場合は「model(ほかのフレームワークのcontroller/action)から取得可能」なので、そこで取得してます。

base_model#get_clump()で取得すると「newした後でDBハンドルぶち込んどいてくれる」程度に便利です…が、上述のような「formからの取得だけ」なら、あんまり意味はないです(笑

まぁ「他とそろえた方が見やすい」程度かなぁ。


data_clumpは「一塊のデータ」なので「cgi requestから、一塊を一式取得しといて」は、1メソッド set_from_cgi() で片付けます。

同様に、validateも「どう? valid?」なので、1メソッド

ちなみにvalidateについては、「定型で片付く」範疇までなら、protected function set_element()ん中のpush_validateで定数とか設定しておけば、後は自動。

「もうちょっとややこしいvalidateが絡む」場合は、is_valid_insert()またはis_valid_update()を上書きします。先にparent::で「定型処理のvalidate」呼んでもらって、残りの「ややこい」のは自力で適宜追記実装してください。


validaだった場合。

data_clumpは、内部的に「data」と「view_values」を微妙に使い分けているので、使い分けながら「セッションに情報を保存しつつ」「表示しつつ」します。

dataは「内部データ構造」。なので、DBとのやりとりとか、いわゆる「生データ」がここになります。

一方でview_valuesは「表示用のデータ」。

dataと等しいケースもあるけど「表示用にちょっと色々小細工してみたい」なんてケースもあるので、その場合はview_valuesメソッドを上書きして、parent::で情報取得した後に「小細工いれてreturn」とかやると、色々と便利です。


で、データを最終的に受けとるのはこっち。

https://github.com/gallu/MagicWeaponTest/blob/master/lib/form_fin.inc

  // clumpに一度通してからviewへ
  $form_test_c = $this->get_clump('form_test_clump');
  $form_test_c->set_all_data_from_hash($_SESSION['form_test']);

  // 表示
  $this->get_conv()->set('form', $form_test_c->get_all_view_values());

表示するので、言い方を変えると「view_valuesの処理を通したい」ので。

set_all_data_from_hash()で「生データをぶち込んで」から、get_all_view_values()でviewに変数を一式セット。

これを定型でやっておくと「なんか表示上の変更があっても、get_all_view_values()とテンプレート修正すればOK」なので、DRYに近い感じでよいのです。


上述コードは「ほかはなんもやってない」のですが、実際には、mailを作って送ったり、とかするんだと思う。

get_all_view_values()があるからあんまりこの状態だと使わない気がするんだけど、例えば上述で「text_dataのデータ単体が欲しい」場合は、「get_value('text_data')」で取得可能。


formからのデータの取得をDBにぶち込む系

大体いっしょ。

サンプルコードがないのでざっくり概念だけ書くと、finのタイミングで

  // clumpに一度通してからviewへ
  $form_test_c = $this->get_clump('form_test_clump');
  $form_test_c->set_all_data_from_hash($_SESSION['form_test']);

  // DBにinsert
  $r = $form_test_c = $this->insert();
  if (false === $r) {
    // insertできなかったお orz
  }

  // 表示
  $this->get_conv()->set('form', $form_test_c->get_all_view_values());

これくらい。

set()ってメソッドもあって、いわゆるupsertなんだけど、最近あんまり使わないようにしてるので多分そのうち非推奨になる予定w

ちなみにinsertの場合、「set_insert_date_name()」ってのが設定されていると、設定されたカラム名に「現在日付」が入る感じ。


で…DBにinsertする場合。

set_from_cgi(正確にはset_from_cgi_insert)でデータを取得するときに、例えば「idはauto incrementなのに、パラメタインジェクションで外部から指定されちゃってショック!!」ってのが、起きないとも限らない。

なので、そーゆー時はset_from_cgi_insert()を上書きしてくださいませ。

set_from_cgi_insertは内部的に「set_from_cgi_detailをcallしている」だけで、set_from_cgi_detailは

 * @param cgi_request $req cgi_requestクラスのインスタンス
 * @param vector $target 対象とするcgi name attribute値
 * @param boolean $empty_overwrite_flg 空文字の上書きフラグ trueにすると空なら空文字を上書きする

なので。

第二引数の「$target」から、例えば「自動で設定したいPKのカラム名」を抜いたりすればOK。

$this->get_all_names() で「全パラメタ名」が配列で取得できるので、そこから「抜きたいのを削除する」ってロジックを書くと楽。

おいちゃんは

・array_flipで値とkeyを反転

・unsetでkeyを指定して削除したいカラム名を削除

・array_flipで戻す

ってやり方をよくやるかな。

これなら「後でカラム名が増えても」気にせずやっていけるからw


update系

処理としては似てるんだけど。

やり方的には

・keyに対応する情報をDBから取得

・cgi requestで「修正すべき値」が投げられてくる

DBをupdate

って手順になると思う。


雑にソースコードを書くと

  // clumpインスタンス作成
  $hoge_c = $this->get_clump('hoge_clump');

  // keyを設定
  // XXX IDが空かどうかのチェックは省略
  $hoge_c->set_value('hoge_id', $this->get_cgi_request()->find('hoge_id'));

  // DBから取得
  $r = $hoge_c->get();
  if (false === $r) {
    // データないってよ!!
    適宜エラー処理各種
    return ;
  }

  // 表示
  $this->get_conv()->set('hoge', $hoge_c->get_all_view_values());

で、まず表示。

修正内容取得&(確認画面抜きにして)修正なら


  // clumpインスタンス作成
  $hoge_c = $this->get_clump('hoge_clump');

  // 「cgi requestから」データを取得
  $hoge_c->set_from_cgi_update($this->get_cgi_request());

  // validate(定型のみ)
  if (false === $hoge_c->is_valid_update($this->get_conv())) {
    適宜エラー処理各種
    return ;
  }

  // DBの内容編集
  $hoge_c->update();

  // 表示
  $this->get_conv()->set('hoge', $hoge_c->get_all_view_values());

こんな感じ。

set_insert_date_name()とほぼ一緒な感じで、set_update_date_name()ってメソッドがあるので。

「修正日」とかいうカラムがある系なら、ご利用くださいませ、的な。


あと「set_from_cgi_updateでデータを取得するときに、変更させたくないパラメタなのにパラメタインジェクションで外部から指定されちゃってショック!!」ってのが、起きないとも限らないので、的な、insertと同じお話。

set_from_cgi_updateは内部的に「set_from_cgi_detailをcallしている」だけなのと、 $this->get_all_no_key_names() で「pk以外のカラム名一式」が取得できるので、insertん時と同じように「適宜、抜くべきカラム名は抜いて」あげてくださいませ。


「PK以外のデータを指定して」情報を引っ張ってきたい場合

例えばユーザデータなんかで

・IDはint

・emailがユニーク

・emailからユーザを引っ張ってきたい

なんてケース。

  // clumpインスタンス作成
  $users_c = $this->get_clump('users_clump');

  // keyを設定
  // XXX IDが空かどうかのチェックは省略
  $users_c->set_value('email', $this->get_cgi_request()->find('email'));

  // DBから取得
  $r = $hoge_c->get_nopk();
  if (false === $r) {
    // データないってよ!!
    適宜エラー処理各種
    return ;
  }

ようは、get()がget_nopk()に変わるだけ。

ただこれ「複数引っかかる場合、なにが引っかかるかは保障されない」ので、注意してね。


「一覧」とかを処理する系

MagicWeaponの基本の一つは「SQLは書いて」なのでw

その辺を前提に、一覧系を、やっぱり雑なコードで簡単に解説。


  // なんか「特定のstatus」を持ってるユーザの一覧とか検索
  $mw_sql = new mw_sql();
  $mw_sql->set_sql('SELECT * FROM users WHERE status=:status;'); // プリペアドステートメントを設定
  $mw_sql->bind(':status', $this->get_cgi_request()->find('status')); // 値をbind
  $res = $this->get_db()->query($mw_sql); // SQLの発行
  $res->set_fetch_type_hash(); // fetchのタイプをhash(カラム名)に変更

  // data_clumpのインスタンスを再利用してちょっとだけメモリ節約用:昔は結構重要だった。今はどうかなぁ?
  $users_c = null;

  // データがなくなるまでぶん回す
  $users_list = [];
  while($res->fetch()) {
    $users_c = $this->get_clump('users_clump', $users_c); // 第二引数がnullならnew、nullでなければiniti叩いてインスタンス初期化して再利用

    // データを「db_dataインスタンスから」取得
    $users_c->set_from_dbdata($res);

    // view用データを蓄積
    $users_list[] = $users_c->get_all_view_values();
  }

  // viewに設定
  $this->get_conv()->set('users_list', $users_list);

大体こんな感じ。

その他雑多で「まぁまぁ使う」子たち

del()

データの削除。単純にdeleteなんだけど、

「もし、テーブル名 + '_delete'っていう名前のテーブルが存在する」場合、そちらへのinsertを同時にやってくれる、ってあたりがちょっとだけ小細工。

テーブル名_deleteのテーブルには、テーブル名のカラム+「delete_date」ってカラム、が必須。


set_value_nowdate()

set_valueとほぼ等価なんだけど、値は「日付が自動で入る」ので、稀に便利。


set_value_token()

set_valueとほぼ等価なんだけど、値は「tokenが自動で入る」ので、稀に便利。

ちなみにtokenは、tokenizerクラス( https://github.com/gallu/MagicWeapon/blob/master/tokenizer.inc )の値。


set_value_token_with_ip()

set_value_token()とほぼ一緒なんだけど「IPアドレス付き」になるので、複数サーバでも無問題。


update_calculation()

「1つのカラム」の数値を加減算できるメソッド


set_insert_id()

auto_increment時に、insertの後でこのメソッドをcallすると「IDがclumpの中に入ってくる」ので、後の取り回しが楽、かも。


あとは、data_clumpは「実はデータをmemcachedに入れられる」とか「実はデータをAPCん中に入れられる」とか、細かいギミックがいくつか。


…うん思った以上に長くなった(苦笑

まぁなんだかんだ、ある程度「MagicWeaponに特徴的なクラス」なので、細かくは色々な機能がありますw

でもまぁベースにあるのは「データを一塊で扱う」以上終了、なので。


ソースコードは、またどっかのタイミングで整理しないとねぇ(苦笑


PS

この文章は本気で「見直しをしていない」ので、突っ込みは大歓迎w

多分、定期的に修正いれますw

2016-04-08

[][]おおざっぱに「がちゃ用の選択プログラム」を組んでみた

以前にも estab_table というクラスを組むには組んでいたのですが。

ほんのりと思うところがありまして、新規に組んでみました。


https://github.com/gallu/MagicWeapon/blob/master/probability.inc

https://github.com/gallu/MagicWeapon/blob/master/probability_T.php (超大まかな使い方)


基本的には

・確率はすべて整数で入れる

・別に「トータルが100であるかどうか」は気にしない(のでデータ作るときにでもチェックしてくれぃ)

という感じ。

pushメソッドの第二引数「確率」の合計は、100でもいいし、1000でもいいし、って感じ。まぁ昨今の「0.002%」とかってのを受け入れると合計の数値が100,000とかになっちゃうんだろうけど、まぁそれはそれで。


使い方としては

・pushで一通り突っ込んで

・choiceで取り出す

だけなのですが。ロジック上、pushは「確率が高いものから順番にpushする」と、幾分効率がよくなると思われたりします。

大体がちゃって「マスターがテーブル上にある」ケースが多いので。引っ張ってくる時にorder byでもしていただければ、って感じかねぇ。


で、以下備忘録的に少し。


まずは確率の検証。モンテカルロ法的な手法をとってみます。

<?php

require_once('probability.inc');

$obj = new probability();
//
$obj->push('item_1', 50);
$obj->push('item_2', 30);
$obj->push('item_3', 20);
//var_dump($obj);

//
$data = array();

//
for($i = 0; $i < 100000; $i ++) {
  @$data[ $obj->choice() ] += 1;
}
//
printf("item_1 is %.4f\n", $data['item_1'] / 100000 * 100);
printf("item_2 is %.4f\n", $data['item_2'] / 100000 * 100);
printf("item_3 is %.4f\n", $data['item_3'] / 100000 * 100);

あるタイミングにおける結果は、こんな感じでした。

[gallu@localhost wk]$ php probability_T2.php

item_1 is 50.1870

item_2 is 29.7750

item_3 is 20.0380

[gallu@localhost wk]$ php probability_T2.php

item_1 is 49.5910

item_2 is 30.3550

item_3 is 20.0540

[gallu@localhost wk]$ php probability_T2.php

item_1 is 49.9310

item_2 is 29.9220

item_3 is 20.1470

[gallu@localhost wk]$ php probability_T2.php

item_1 is 50.0470

item_2 is 29.9730

item_3 is 19.9800

[gallu@localhost wk]$ php probability_T2.php

item_1 is 50.1910

item_2 is 29.7650

item_3 is 20.0440

まぁ大体意図通りだねぇ、っと。


次に性能。

<?php

require_once('probability.inc');

$t = microtime(true);

for($j = 0; $j < 1000; $j ++) {
  $obj = new probability();
  //
  for($i = 0; $i < 200; $i ++) {
    $obj->push("item_{$i}", 1);
  }
  $obj->choice();
}
$t = microtime(true) - $t;
var_dump($t);

これで「1000回」ぶん回してみませう。1000回だとちょうど「1秒が実際には1ミリ秒」になるからわかりやすいのよ、的な(笑

実際に「1つのがちゃに200種類」は些か盛りすぎな気がせんでもないのですが、まぁそれくらいが確認にはよろしかろう、的な。


ノーマル

[gallu@localhost wk]$ php probability_TTT.php

float(1.2731449604034)

[gallu@localhost wk]$ php probability_TTT.php

float(1.272558927536)

[gallu@localhost wk]$ php probability_TTT.php

float(1.2700932025909)

[gallu@localhost wk]$ php probability_TTT.php

float(1.2739930152893)

[gallu@localhost wk]$ php probability_TTT.php

float(1.2681269645691)

実質的には大体1.27ミリ秒。気にならないで済む程度の速度なのではないかなぁ、と。


念の為に「配列の一番最後」を意図的にチョイスするように小細工した時の速度。

[gallu@localhost wk]$ php probability_TTT.php

float(1.3437149524689)

[gallu@localhost wk]$ php probability_TTT.php

float(1.3538720607758)

[gallu@localhost wk]$ php probability_TTT.php

float(1.3482151031494)

[gallu@localhost wk]$ php probability_TTT.php

float(1.343001127243)

[gallu@localhost wk]$ php probability_TTT.php

float(1.3433270454407)

それでも1.34ミリ秒。


せっかくなんで「配列の先頭」を意図的にチョイスするように小細工した時の速度。

[gallu@localhost wk]$ php probability_TTT.php

float(1.2022018432617)

[gallu@localhost wk]$ php probability_TTT.php

float(1.2001230716705)

[gallu@localhost wk]$ php probability_TTT.php

float(1.1996169090271)

[gallu@localhost wk]$ php probability_TTT.php

float(1.2015750408173)

[gallu@localhost wk]$ php probability_TTT.php

float(1.1977519989014)

1.2ミリ秒。

まぁまぁ。


もうちょっと速度を気にするんなら、もしかするとだけど「あらかじめpushしたインスタンス」をシリアライズしておいてDBにぶち込んで、実際に使う時には「アンシリアライズして使う」とかやると、もっと早いかも…と思ったのでさっそく検証。

<?php

require_once('probability.inc');


$obj = new probability();
//
for($i = 0; $i < 200; $i ++) {
  $obj->push("item_{$i}", 1);
}
$s = serialize($obj);
$obj = null;

$t = microtime(true);
for($j = 0; $j < 1000; $j ++) {
  $obj = unserialize($s);
  $obj->choice();
}
$t = microtime(true) - $t;
var_dump($t);

結果

[gallu@localhost wk]$ php probability_TTT2.php

float(0.24353098869324)

[gallu@localhost wk]$ php probability_TTT2.php

float(0.23857712745667)

[gallu@localhost wk]$ php probability_TTT2.php

float(0.24080300331116)

[gallu@localhost wk]$ php probability_TTT2.php

float(0.24097180366516)

[gallu@localhost wk]$ php probability_TTT2.php

float(0.24446702003479)

アベレージ、0.24ミリ秒。

む、思った以上に早いでやんの。

「多少の運用コストかけてでもマシンコストを下げたい」ってニーズであれば、こっちのほうがいいかもしんまい…けどまぁ「微々たるもん」って気もするので、その辺はお好みかなぁ。


「ユーザ毎に確率を変える」とかその手の小細工が必要になるとこのコードは使えないのですが*1、そうでなければ、割と使えるんじゃないかなぁ、という感触。

まぁ以前も似たようなコードを組んでたのですが、なんかふと「降りてきた」ので、一気に書き上げてみましたとさ。


感想その他ありましたらお気軽に。

*1:それをやっていいのか悪いのかはまた別問題 B-p