生成される hash 値が php7 で少しだけ変わった件

この記事は 闇PHP Advent Calendar 2015 10日目 です

php連想配列で用いられているハッシュ関数は DJBX33A と呼ばれるものです。


実装は php5.6 までは Zend/zend_hash.h 、php7 からは Zend/zend_string.h に、どちらも zend_inline_hash_func という関数で実装されていますが、実装はわずかに異なります。

以下は、php-5.6.16 と php-7.0.0 の zend_inline_hash_func を抜粋したものの diff です。

一番多い修正は、引数名が、arKey から str、 nKeyLength から len に変わったことです。
これはおそらく key のハッシュ値生成目的だったものが、zend_string のハッシュ生成になったので、それに合わせてでしょう。


5381 という初期値が Z_UL(5381) になりました (20行目)
Z_UL は環境に応じて、値の後に U、UL、ULL を付与するマクロのようです。おそらくコンパイラの最適化のためでしょうが、具体的な理由はよく分かりません。


上記は、計算されるハッシュ値には影響しない変更です。
しかし、関数の最後にハッシュ値に変更が加わる修正が入りました。ハッシュ値の計算後、値が 0 だったら 最上位ビットを立てる という処理です。

コメントには、"ハッシュ値はゼロになってはいけない" とありますが、なぜゼロではいけないのかの理由は示されていません。

git blame して確認して見ると、 https://github.com/php/php-src/commit/bdf7fc67d8a146b4a5387c56268181c63047dfe6 のコミットで追加されており、そのコミットログに理由が書かれていました。

zend_string は読み取り専用メモリ領域に置かれることがあり、ハッシュ値が 0 になってしまうと再計算が行われるためにメモリを書き換えようとしてクラッシュするようです。


読み取り専用メモリ領域に置かれる状況や、ハッシュ値の再計算が行われるコードの箇所は調べきれませんでしたが、とりあえず、そういうことのようです。

ext_skel の 雛形ファイルは php7 でどう変わったか

この記事は 闇PHP Advent Calendar 2015 9日目 です



先日の 闇PHP勉強会で、 ext_skel スクリプトで生成される雛形って、ほとんど更新されてないのでは 的な話をしたので、気になって調べてみました。

上は php-5.6.16 と php-7.0.0 の skelton.c の diff です。

大きく分けて3種類の変更が入っています。


1. PHP_FUNCTION(confirm_extname_compiled) の処理
2. TLS キャッシュ の処理
3. コメント行の行末空白の除去


3は見ればわかるので、 1 と 2 について少し詳しく見ていきましょう。

PHP_FUNCTION(confirm_extname_compiled)

まず、ZTS 絡みではありますが zend_parse_parameters の引数から TSRMLS_CC が消えました。(13行目)
これは、雛形ファイルに限らず、 php-7.0.0 への変更において最も多くの diff を作った変更の一つでしょう。


文字列の結合処理が spprintf から strpprintf へ変更されています。(19行目)
ともに main/spprintf.c で定義されている関数ですが、 strpprintf は 7.0.0 で新たに追加されたものです。

spprintf が、 第一引数で渡した char* にヒープを割り当てて文字列を格納するのに対し、
strpprintf は 生成した zend_string へのポインタを返すようになっています。


従来は RETURN_STRINGL を duplicate 0 (第三引数) で呼ぶことで、zval.str.val に char* を zval.str.len にその長さを割り当てていたところが、RETURN_STR になって 生成された zend_string を zval.str に割り当てるようになりました。(21行目)

ちなみにこの RETURN_STRINGL マクロは php 5.6系までは 4つの引数を取る関数マクロでしたが、 php 7 では 3つの引数に変更されていて、この変更が拡張開発者の頭を悩ませる要因の一つになっています。

zend_string が char 配列 へのポインタを保持するのではなく、zend_string (可変長)構造体に文字列のメモリを確保するため、メモリコピーを発生させずに zval を作ることができないからではないかと推測しますが、それでも、同じ関数(マクロ)名でインターフェースを変えるのはさすがにどうかと思う。。。

TLS キャッシュ

TLS とは Thread Local Storage のことで、 ここでは ZEND_TSRMLS_CACHE_DEFINE() (57行目) と ZEND_TSRMLS_CACHE_UPDATE() (47行目) が新たに呼ばれるようになりました。

ZEND_TSRMLS_CACHE は ZEND の TSRM (Thread Safe Resource Manager) の LS (Local Storage) の キャッシュという意味で、php7 における TLS 関連の変更については https://wiki.php.net/rfc/native-tls を参照してください。

php 処理系に含まれるコードのほとんどから TSRMLS_* マクロが消えたのは この変更によるものです。


1. TLS を利用するためのキーとなる変数を、従来はすべての関数の引数で受け渡ししていたのを、キーを必要な時だけ取得するようにした。
2. ただしそれだどリソースにアクセスする頻度が上がるほどパフォーマンスが落ちるので、取得したキーをキャッシュするようにした


という認識ですけど、ちゃんとは追ってないので推測の域を出ません。(詳しい方いたら解説お願いしたいです)

まとめ

skelton.c の変更から php-7.0.0 における変更点を確認しました。

zend_string にしても、 TLS にしても、php 全体に関わる変更を行っていて、php 開発陣には頭の下がる思いですね。

pcntl 拡張と signal

この記事は 闇PHP Advent Calendar 2015 5日目 です

pcntl 拡張で signal を扱う

php で signal を扱うためには pcntl 拡張を利用します。

使い方は簡単で、pcntl_signal 関数で トラップしたいシグナル番号とコールバックされる関数(シグナルハンドラ)を登録するだけです。
ただ、現在の pcntl 拡張 では ZEND_TICKS と tick 関数 で解説した tick を用いて実装されているため、declare を用いて ticks を 1以上に設定する必要があります。


code.4 を実行して Ctrl+C を押すと、SIGINT をトラップし、"interrupted" をecho 後に終了します。

code.4
<?php
declare(ticks=1);
pcntl_signal(
    SIGINT, 
    function() {
        echo "interrupted", PHP_EOL;
        exit();
    }
);

while(true) {
    echo 1, 2, 3;
}

tick を使って実装していると言うことは、シグナルハンドラはステートメントステートメントの間でのみ呼ばれる (ステートメント実行中に呼ばれることはない) ということです。
つまり前述の code.4 は 1 や 2 が出力されたタイミングで終了することはなく、必ず 1 と 2 と 3 が出力された後に終了します。


一般的に signal というのは、プログラムの実行中どのようなタイミングでもトラップされる(割り込みが発生する)ものです。
しかし、php の signal 実装においては、トラップされたタイミングでシグナルハンドラを呼び出すのではなく、一時的にトラップしたシグナルを貯めておき、tick のタイミングでシグナルハンドラを呼び出す形になっています。

この仕組みによりデッドロックやレースコンディションの発生を抑制し、phpプログラムの誤動作を防いでいます。

php の signal 実装

では、php の signal はどのように実装されているのでしょうか。

pcntl_signal PHP関数 は、第2引数で渡されたシグナルハンドラをシグナル番号をキーとしたシグナルテーブル ( PCNTL_G(php_signal_table) ) に登録します。
また、OS に対して sigaction システムコールを発行し、第1引数で指定したシグナルが発生した際に pcntl_signal_handler C関数(pcntl 拡張内で実装されている) を呼び出すように登録します。
pcntl_signal_handler が行っているのは、ペンディングシグナルキュー という名の queue に、受信したシグナル番号を格納するだけです。

前述の通り、シグナルを受け取った際に直接シグナルハンドラがコールバックされるわけではないところが重要です。
ちなみにこの ペンディングシグナルキュー、最初に pcntl_signal を呼び出した際にメモリを確保するのですが、その数は32固定。空きがない状態でシグナルを受け取ると、そのシグナルは格納されず無視されてしまいます。


貯まる一方では意味がないので、このキューから値を取り出す関数も当然存在します。それが pcntl_signal_dispatch 関数。これも pcntl 拡張内で実装されたCの関数です。pcntl_signal_dispatch はpcntl拡張が読み込まれたとき*1 にtick 関数として登録*2されています。

pcntl_signal_dispatch は、ペンディングシグナルキュー に貯まっているシグナル番号を取り出し、シグナルテーブルに登録されているシグナルシグナルハンドラを順次コールバックします。
これが php の signal 実装の全体像です。


なお、シグナルハンドラのコード中も tick は有効です。しかし、pcntl_signal_dispatch は再入を検知して抑止する*3ため、シグナルハンドラの中でシグナルハンドラが呼ばれることはありません。

補足

実は signal 実装はとてもシビアな処理であり、上記の説明は細かい処理(シグナルのマスク処理等)を省略しています。
このあたりはまたいずれ。

*1:MINIT時

*2:php_add_tick_function

*3:PCNTL_G(processing_signal_queue) をフラグとして利用

ZEND_TICKS と tick 関数

この記事は 闇PHP Advent Calendar 2015 4日目 です

tick について

ZEND_TICKS とは、tick 不可能行*1の後に発行されるオペコードで、多くは、セミコロンで終わるステートメントごとに発行されます。

ただし、デフォルトでは発行されず、利用するためには declare 命令で明示的に有効にする必要があります。
declare 命令の ticks で指定した回数の ZEND_TICKS が呼ばれるごとにregister_tick_function で登録した関数 (tick 関数) が実行されます。


code.1 と code.2 は(内部的には異なりますが)表面上同じ動きをします。

code.1
<?php
function tick() {
  echo "tick!" . PHP_EOL;
}

register_tick_function('tick');
declare(ticks=1);

$a = 1;
if ($a < 10) {
    echo $a . PHP_EOL;
}
code.2
<?php
function tick() {
  echo "tick!" . PHP_EOL;
}

tick();
$a = 1;
tick();
if ($a < 10) {
    tick();
    echo "{$a}" . PHP_EOL;
    tick();
    tick();
}
tick();

echo の後に 2回 tick が走りますが、これは誤りではなく、echo による tick と if の条件内ステートメントリスト(表面上は見えない) による tick です。

declare(tick=N) の有効範囲

declare による ticks の有効範囲は、少々特殊なので注意が必要です。

declare(ticks=1);
declare(ticks=1) {}

のように、単文あるいは空ブロックで指定された場合、ファイル内の declare 以降全てが範囲となり、

declare(ticks=1) sleep(1);

declare(ticks=1) {
	sleep(1);
	echo "hoge";
}

のように、他の文を伴った場合は、その文やブロックのみが範囲となります。
あまり使うことはないと思いますが、ブロックは入れ子にすることも出来ます。

また、ticks は別ファイルには影響しない事にも注意が必要です。
これは、 ZEND_TICKS の発行がコンパイル時に行われるためです。

ZEND_TICKS オペコードが発行されている部分(ticks > 0)のスクリプト実行は、発行されていない部分と比較して (オペコードや tick 関数を処理する分) オーバヘッドがあることに注意しましょう。

code.3
<?php
// ticks=0 (初期値 ZEND_TICKS 発行なし)

declare(ticks=1);
// tick=1

declare(ticks=2) {
	[any code]
	// ticks=2
}
// ticks=1

require 'hoge.php'; // hoge.php には ticks は引き継がれない (ticks=0 で開始される)

echo "any code"; // もちろんここでも tick=1 のまま

*1:php5.6 までは zend_language_parser.y における unticked_statement として定義され、パース時に発行していましたがが、 php7 においては AST 導入によりコンパイル時発行となったためその定義はなくなり、代わりに zend_is_unticked_stmt 関数で表されています。

Casualに闇とBLTしてきました

  • 2015/11/20 MySQL Casual Talks vol.8
  • 2015/11/22 第六回闇PHP勉強会
  • 2015/11/24 PHP BLT #1


意図せず、飛び石でトークすることになってました。

MySQL Casual Talks vol.8

いつものアレに対する近況報告と、ちょろっと Multi-source Replication の話。
それに加えて、 Optimizer Hint の話をしました。

実は Optimizer Hint の話はおまけで、
php で mysql の max_execution_time Optimizer Hint を模倣するサンプル - Qiita
こっちのほうがメインだったりする。

第二回闇PHP勉強会


PHP7 で導入された、コンパイル時に構築される抽象構文木についての話。
nikic/php-ast を知らずに、 ast 構造を確認するための拡張書いてしまったので、せっかくなので DEMO しました。

DEMO で使ったアプリケーションは、雑に書いたものなので実行できるものを公開するのは控えますが、コードは github にあります。

自分で動かすなら、 https://github.com/do-aki/phpast を手元に持ってきて以下の手順で動かせます。graphviz 必要だけど。

$ phpize
$ ./configure
$ make
$ php -d extension=`pwd`/modules/phpast.so -S localhost:3000 -t webapp/

PHP BLT#1

PHP拡張をGo言語で書いてみたよという話。
発表する時になって気づいたのですが、このトークphp色まったくないですね。
どなたか、c-shared で build する場合に go runtime 部分にデバッグ情報を付与する方法を教えてください。

まとめ

このペースでネタ作って資料作ってトークするの結構しんどかった。

PHPカンファレンス2014 で「mysqlnd 徹底解説」を話してきたこと #phpcon2014

去る 2014/10/11。大田区産業プラザPiOにて PHPカンファレンス2014が開催されました。

その中で、「mysqlnd 徹底解説」というタイトルでトークさせていただきました。

雑感

思い返せば PHP カンファレンスで初LT をしたのが 2011年。
いつかLTではなく本編で話そうと思いつつ、気づけば3年経っていました。
相変わらず行動力に欠けるようで。

応募したのは完全にノリ。


けど、採択されてからが大変だった。
採択されたのが9月25日、2週間ちょいしかない。資料どころか、mysqlnd についてまだ全然分かってない。という状況。

応募したときはちょうど仕事で mysqlnd の挙動について軽く追っていた頃だったので「これきっかけに詳細にコード追ってみよう」という軽い気持ちだったのですが、追えば追うほどよく分からないコードが目の前にどんどん詰まれていくという状況。
資料が形になったのは数日前で、前日になって時間が足りない事に気づいて一部削るという判断をしました。
いやー、ホントに発表できるのか心配ダッタヨ。

ただ、今回のセッションの為に歴史や周辺のこと(ライセンス問題とか)を深く調べたことで、「なぜ今のコードになっているのか」を、だいたい把握できたのはとても大きな収穫だった。

内容について

前半は 「mysqlnd とは何か」について、後半は「mysqlnd で何が変わるのか」についてを中心にトークしました。
前半はスライド見れば分かると思いますが、後半は別途解説がないとわかりにくいと思います。

最後のメモリ爆発のくだりは実際に自分が陥った経験です。サーバーサイドプリペアドステートメント で PDO という、最も地雷なところを踏み抜いてしまったことにより、このセッションが生まれたと言っても過言ではありません。


資料中にはないのですが、実際にはメモリ消費を抑える方法があります。

store_result ではなく、use_result を使うという手法です。
実は、 mysqli においては、プリペアドステートメントをつかうとデフォルトでは use_result となります。
ですので問題が表面しにくくはあるのですが、ただ、mysqli_stmt::store_result や mysqli_stmt::get_result を呼んだ場合は store_result が行われるので注意が必要です。

また PDO の場合は、デフォルトで store_result が使われます。
明示的に use_result を使うためには pdo::setAttribute で明示的に PDO::MYSQL_ATTR_USE_BUFFERED_QUERY を false にする必要があります。(これ、マニュアル読むだけだとデフォルト false っぽいけど、実際にはデフォルトは true)

ただし、use_result を利用すると、そのコネクションに於ける MySQL のリソースを占有するため、結果セットを解放するまで次のクエリを実行することが出来ません。また、フェッチの度に MySQL にアクセスするため、その分 MySQL 側の負担が増えます。
必要に応じて使い分けるのが良いでしょう。

とはいえ、それぞれをラップしたライブラリとかだと、なかなか難しいんだよねこれ。

レポート記事への補足

PHPカンファレンス2014 当日レポート[更新終了]:PHPカンファレンス2014 スペシャルレポート|gihyo.jp … 技術評論社 でレポート記事がでています。

結構詰め込んだセッションだったと思うのですが、要点要点しっかり抑えられていて素晴らしいです。

が、一部誤った部分があるので補足。


誤: libmysqlはGPLv2という若干特殊なライセンスであり,同梱して別のところに配布ができない
正: libmysqlはFOSS例外付きGPLv2という若干特殊なライセンスであり, ソースコードの開示なしに同梱して頒布出来ない


この辺は自分自身しっかりと把握している訳で確実ではないのですが、
libmysqlclient を (商用ライセンスではなく) GPLv2 として利用した場合、それを利用した php スクリプトプロプライエタリとすることは出来ない という認識です。

ライセンス ムズカシイネー

PHPコアから読み解くPHP5.5」を聴いて

yield 使ったコードと使わないコードどっちが速い? -> ほとんど変わらないから yield 使った方がコードがすっきりしていいよね
(bool)$value と boolval($value) どっちが速い? -> キャストの方が早いからキャスト使おうね
zend_execute -> 5.4系までと5.5からとで異なるから注意な

という内容。

コアから読み解く、というわりにコアの一部だけだった感は否めない。
オペコードの数では速度は決まらないのにやけに数を強調してたり、具体的な処理時間出す割にベンチの仕方全く出さないしでうーん。

PHPにおけるI/O多重化とyield」を聴いて

yield には多くの可能性を感じますね

co-httpclient は、http request に特化しているけれど、event loop っぽいところは汎用化できると思うので、
具体処理を差し込むかたちにできると面白いかな−と思った。

perl の Coro 思い出すよね。

「HHVM + Hack == PHP++」を聴いて

久々にわくわくするセッションだった。

静的型付けで強力な型推論あれば最高だよねーな人間としては、こういう方向性ってのはホント面白い。


プログラミング言語って人間が扱うモノだから、正しい方向って一つじゃないんだよねーと思ってる。
そういう意味でも、facebookphp script のほとんどを hack に置き換えた後、 hack に何を加えていこうとしていくのかは気になるところ。

LT 「よいことも悪いこともぜんぶPHPが教えてくれた」を聴いて

これはもう、じっくりと時間をかけて聴きたかった。
クロージングの前にこれを基調講演として枠取ってやって欲しかった。

http://d.hatena.ne.jp/moriyoshi/20110204/1296808771 と、この続きを含めてどっかで講演して欲しい。

「哲学がないという哲学」なんという哲学や……。

最後に

PHP カンファレンス実行委員の皆様、お疲れ様でした&ありがとうございました。

またセッションを聞きに来てくださった方、ありがとうござました。

リンク

PHPコアから読み解くPHP5.5
http://www.slideshare.net/techblogyahoo/phpphp55
PHPにおけるI/O多重化とyield
https://joind.in/talk/view/12038
Cooperative multitasking using coroutines (in PHP!)
http://nikic.github.io/2012/12/22/Cooperative-multitasking-using-coroutines-in-PHP.html
よいことも悪いこともぜんぶPHPが教えてくれた
http://www.slideshare.net/moriyoshi/php-40137945
今さらだけどMySQLとライセンス
http://www.slideshare.net/h141/mysql-22023284
漢(オトコ)のコンピュータ道
FOSS License Exception:http://nippondanji.blogspot.jp/2009/05/foss-license-exception.html

ISUCON4 予選に参加した話 #isucon

去る、9月27日(土)に ISUCON4 予選に参加しました。
チームは「ご注文は php ですか?」です。
利用言語は go でした。

名前に偽りありと言われそうですが、チーム名決定した後に利用言語を決めたのと、候補の中には「彼女募集中」とか、「ぺちぱー合コン部」とかあったのでその中ではまともなチーム名じゃないかと思う次第なわけで・・・・・・。

チームメンバーは、既にブログ書かれてますけど netmarkjpさん と matsuuさん。

matsuuさんのブログでは、光栄にも右手と書いていただけましたが、実際には

@netmarkjp: ファシリテータ
@matsuu: 両手両足
@do_aki: さっぱり妖精

といった状態で、netmarkjpさんの分析力とmatsuuさんの実装力に圧倒されて、終始何もできないままでした。*1

唯一貢献できたことと言えば、3人が集まる会場を提供できたことくらいでしょうか。

準備大事

9月8日にチームに誘われてから、いろんなテキストチャットでやり取りしていたのですが、ほんとに驚かされたのが matsuuさんの行動力。
当初は php を使う前提で動いていたため、xdebug や xhprof を使たプロファイリングの下準備をしっかりされていて、ISUCON3の予選 AMI を使って、実際にどう分析できるかを検証。

一度顔合わせしましょーということで開かれた、戦略会議*2が予選の1週間ちょい前で、そこで Go を使うことが決定してから数日中に Go言語使いこなしてて、ISUCON3 予選問題を Go でさくっとチューニングしてしまうというチートっぷり。

netmarkjpさんも、その傍ら適切なアドバイス入れつつ、アクセスログから時間がかかっているリクエストをさくっと見られるツール作ってて……

いやもう、自分ホント何もしてません。スミマセン。
(もちろん、A Tour of Go 一通り済ませて言語仕様の確認くらいはしてましたが!)

前日


数時間後

当日

当日の話は、お二人のブログに詳細があるのでそちらを参照してください。

自分がやったのは、ban user/ip の map 操作に対して lock を入れたことと、
トップへのアクセスに対してテンプレートを使わずに直接文字列 を返すようにした事くらい (これは結局 nginx 側で返すようにしたのであまり意味なかった)

あ、あと何故かデザイン崩れるね−って話になったときに mime おかしくね? ってことに気づいたくらい。

他はひたすら martini とその依存コード睨んでました(ホント役立ってない……)

まとめ

精進します……。

まずは go をもっと知ることからっ

Link

開催案内
ISUCON公式Blog:http://isucon.net/archives/cat_1024989.html
isucon4予選出場してきた #isucon
http://netmark.jp/2014/09/isucon4-qualifier.html
第4回ISUCON予選にチーム「ご注文はPHPですか?」で参戦して1日目暫定10位になりましたがPHP使ってません - Dマイナー志向
http://d.hatena.ne.jp/tmatsuu/20140928/1411897412

*1:お二人だけのチームでもきっと大差なかったのでは

*2:戦略会議という名の飲み会