Hatena::ブログ(Diary)

do_akiの徒然想記 RSSフィード

2017-09-03

再考:列挙型

はじめに

php で列挙型と言えば、 @hiraku さんの http://qiita.com/Hiraku/items/71e385b56dcaa37629fe みたいな実装がほとんどかと思います。

自分もだいたいこんな感じでいいかなと思っているのですが、2点ほどどうしても実現したいことがありました。

1. IDE で補完したい

2. 同じ定数値であっても異なる列挙型ならば別物としたい


これについて、第117回 PHP勉強会@東京 でLT したスライドがこちらです。

下記は、話した内容をもう少し詳しく書き起こしたものになります。


1. IDE で補完したい

そのままの話で、 __callStatic を利用した場合、補完できません。

補完できないということは、IDE のコード支援機能(例えばリファクタリング)が利用できないということです。

これについては、DocComment を入れれば済む話ではあります。

例えば、ゲームの難易度を表す列挙型があったとして、こんな感じにクラスコメントを追加することで対応可能です。

<?php
/**
 * @method static GameDifficulty HARD()
 * @method static GameDifficulty NORMAL()
 * @method static GameDifficulty EASY()
 */
final class GameDifficulty extends Enum
{
    const HARD = 'hard';
    const NORMAL = 'normal';
    const EASY = 'easy';
}

2. 同じ定数値であっても異なる列挙型ならば別物としたい

例えば、チーズの種別でハードを表すのに CheeseType::HARD としたとして、これはゲーム難易度のハード GameDifficulty::HARD とは別物です。

ところが、現状の列挙型実装だと

<?php
$hard = CheeseType::HARD();

var_dump($hard->valueOf() === GameDifficulty::HARD); // bool(true)

みたいなことが起きてしまいかねません。

では、下記のような比較用のメソッドEnum クラスに生やしてはどうでしょう。

<?php
public function is(Enum $lhs)
{
    return get_class($this) === ($lhs)
        && $this->value() === $lhs->value();
}

これでも先の例よりはましです。

ただ、下記のようにミスした時に、(実行時ではなく)記述した時に気づけたほうが嬉しいですね。

<?php
$hard = CheeseType::HARD();

var_dump($hard->is(GameDifficulty::HARD())); // bool(false)

trait を利用した 列挙型

そんなわけで、以前、trait を使って getter や setter を IDE で補完できるようにしたことがあった *1 ので、その仕組みを利用して列挙型を実装してみました。

以下のように利用します。

同じようなことを2度(const と constant as)書かないとならないのが少々面倒ではありますが、クラスコメントで補完が効くようにする手間とそう変わらないのでいいかなと。

(後から気づいたけど、 コンストラクタを private にするなら、 コンストラクタでのチェックは不要かも)

EnumTrait の特徴

use … as でメソッドを再定義することで、IDE からそれぞれのメソッドが存在すると認識されます (少なくとも PhpStorm では認識されます)

trait を利用することで、`self` タイプヒンティング(型宣言)が、use されたクラスを表すことになるため、`is` メソッドが他のクラスを受け付けません。

また、シングルトンにすることで、オブジェクト自身の比較が定数値の比較と同じことになり、厳密な比較が可能になります

constant は、 `debug_backtrace` を使っているので、一見遅そうですが、 一度呼ばれた後はキャッシュ (static 変数) が効くため、呼び出し回数によっては `__callStatic` を呼ぶよりも早かったりします。

PhpStorm のバグ

ただ、残念ながら問題がありました。

PhpStorm では、 定数を生成するメソッドの利用箇所を参照する機能が利用できないのです。

バグレぽ出すも、なかなか修正されず。 


仕方ないので、 CommentDoc も併用しています。


また、

<?php
var_dump($difficulty->is(CheeseType::HARD())); // throw TypeError`

のコードは、静的にエラー出せるはずなのですが、

今の PhpStorm では trait 上の self は trait 自身 (つまり EnumTrait) と認識してしまうため、警告されません。

今後の進化に期待したいところ。

(あれ? ということは、結局現状の列挙型と大して変わらなくない?)

まとめ

従来の列挙型の欠点を trait を利用することで解消する試みを紹介しました。

残念ながら 今の PhpStorm による静的解析では、trait を適切に解釈しないためちょっとイマイチな部分もあります。

2016-12-19

闇PHP勉強会 #yamiphp でASTとsignalについて話してきたこと

時間が開いてしまいましたが、先週 第七回闇PHP勉強化に参加し、トークしてきました。

タイトルは「PHP AST 徹底解説(補遺)」および、「signal の話 或いは Zend Signals とは何か」です。

前者は、過日 PHPカンファレンス2016 で発表した「PHP AST 徹底解説」の補足集で、PHPカンファレンス2016 では時間の都合上省略したり、発表後に指摘された内容について調べたものになります。

後者は PHP における signal 処理についての概要と、7.1 における変化を解説しました。

PHP AST 徹底解説(補遺) の、ASTに関する部分については 「PHP AST 徹底解説」にマージしてありますので、興味があればこちらを参照してください。


signal については、調べていくと OS の挙動を確認する必要があったりして、なかなか複雑だったりします。

また、signal は max_execution_time の実装においても利用されているのですが、時間の計測についてもなかなか奥深いものがあって、詳しく説明すると長くなりすぎると思ったので、あくまで signal にかかわる部分のみを解説しました。

そのため、想定よりもトーク時間が短くなってしまいました。(時間足りない詐欺してすみません。)

どっかで調べたことをもう一度整理してまとめたいところ。



自分以外のトークについて


「realpathキャッシュとOPcacheの面倒すぎる関係」

realpath キャッシュの実装が(phpのほかの機構を利用せず、)独自に実装されてるあたりが面白いなと。

ただこのキャッシュの実装。衝突時に単方向リンクドリスト*1となる固定長(1024)のハッシュマップ*2なので、コリジョンが多く発生するとパフォーマンスがだいぶ落ちそう。

単純に考えても、1024 file 以上なら必ずコリジョンは発生しますし、ハッシュキーの生成法も固定*3なので、もしかするとコリジョンが発生しやすいファイルパスもあるのではないかと思いました。

php スクリプトは、利用するテンプレートエンジンによっては大量に生成されますし、ある程度の規模であれば、realpathキャッシュを切るというのは正しい戦略なのかもしれません。


JITのコードを読んでみた

JIT についてはほかの言語での実装についても全く知らず、 php での実装の仕方を知ることができたのは大きかったです。

LuaJIT のコード生成エンジンを利用しているとは。

最適化という点で考えると、やはり(ネイティブな)型を意識していく方向に向かうのは当然ではありますが、今のところの実装は良くも悪くも php らしさがあるなと思いました。

このPHP拡張がすごい!2017

php は、他のLLと比べて拡張が作られることが少ないと感じていましたが、それでもこれだけの拡張が書かれていたのだと圧倒されました。

ゃ、もっとみんなカジュアルに拡張書けばいいのに。

XPathソースコード検索

ASTを XML に変換すれば、構造の一部を XPath で抽出できるという発想は斬新だなぁと。

聞き手からは XPathより JQuery セレクタ という声も聞こえましたけど、XPath、強力なんですよね。

jQuery セレクタは CSSセレクタをベースにしていて、あまり柔軟な指定って出来なくて、長期的なメンテナンス性を考えなくて良いこういう時の XPath はほんと強力。


ストリームフィルタで暗号化しよう!

7.1 でmcrypt が非推奨になって、OpenSSL 使え (http://php.net/manual/ja/migration71.deprecated.php) とあるけど、OpenSSL だと暗号/複合処理が全体をまとめてでしかできないよね、と。

その時は、ブロック単位での暗号化インタフェースを (php拡張作って)実装すれば、それを利用してユーザ定義ストリームフィルタを登録してやればいいんじゃないかななんて言ってたけど、よくよく考えれば、openssl_encrypt をつかっても、IV を更新していくことでブロック単位で暗号化可能なんじゃないかなー?(未検証)

最後に

主催していただいた @hnw さん、会場提供の pixiv さん、ありがとうございました。

2016-11-09

PHPカンファレンス2016 #phpcon2016 で PHPのASTについて話してきたこと

概要

PHP7 で導入された AST(Abstract Syntax Tree) について、その概要と、導入によるPHPの変化を解説しました。

おまけでASTの利用法についても少し。

AST の可視化は https://dooakitestapp.herokuapp.com/phpast/webapp/ にて試せます(動いてなかったらごめんなさい)。


動機

以前、まだphp7 がリリースされる前に闇PHP勉強会でASTについて発表したことがありました。

ASTの導入は、それ単体でのインパクトは小さく、php7の他の新機能に隠れがちではありますが、可能性という点においては他の機能に勝るとも劣らない仕組みです。

しかし、発表以来気になりつつもあまり追っていなかったのでした。

久しぶりにAST周りについて何か新しいことは起きてないかとググってみましたが、新しいことはおろか、ASTそのものについての話題すらほとんど見受けられないという状況でした。

この状況であれば、そろそろphp7リリースから1年経ちそうな現在であってもASTの話題はまだ古くはないかと思い、発表に至りました。

7.1でどう変化するのかも気になってましたし。

スライドについて

当初は、全AST Node について解説するつもりでいたのですが、全て解説したところであまり面白くないので、止めました。

スライドの中にはAST Node を解説するあたりに、全Node紹介をぶっこみました(が、最後の方はやる気無くなってるのが丸わかり)

また、いくつか講演時には利用しなかったスライドがありまして、これは30分に収める泣く泣く削ったものです。

トークについて

練習中はだいぶ早口でやってぎりぎり30分に収まるかなといった感じだったのですが、実際にやってみると思ったよりも早く進んでしまい、最後は時間が余って焦ってしまいました。

たぶん、練習中には話すつもりだった内容の一部が抜け落ちてる気がする。

最後に質問を受けたことについてですが、質問を受けたとき php-parser について(php-ast を利用して parse した結果を辿る別のライブラリと)勘違いしてまして、とんちんかんな答えをしてしまって申し訳ないです。

質問は、php のクラスを読み込み、(php-parser を使って) 分解、再構築することで (なにかしらの付加機能を加えた) クラス(コード)を作るようなライブラリを書いているが、(ASTを使って)もっと効率化することは出来ないかということでした。

今冷静になって答えると以下のようになります。

質問者の行っていることを、コンパイル時に行う(zend_ast_process を利用して AST を改変する) ことで多少パフォーマンスを改善することは出来ます。

しかしながら、おそらく質問者の書かれたライブラリでは毎回パースしているわけではないと思われるので、実行に与えるインパクトは小さいでしょう。

また、php-parser (内部では token_get_all を利用) を使ったほうが速いか、php-ast を使った方が速いか という話では、これはおそらく後者に分があるでしょう(ベンチマークを取ったわけではないので推測ですが)。

php-parse では 分解された token を php スクリプト構文木を作り上げているわけですが、実は token_get_all は 内部では字句解析と構文解析を行い、生成されたASTを捨てて、その過程のトークンだけを返しています。

これに対して、 php-ast は 字句解析と構文解析を行い、生成されたASTを php スクリプトから扱えるように変換しているだけです。

もちろん、php5 までは php-parser 以外に php スクリプトから構文木を扱う方法がなかったわけですからこれはこれで正しいのですが、ASTを構築するようになった php7 移行について言えば php-parser はだいぶ無駄なことをしていることになるのです。

補足

講演時には解説できなかったスライドや話題について、ここで補足しておきます。

Parse Error と Compiler Error

構文解析時に構文として受け入れられなかった場合は Parse Error で、これは php7 から例外となりました。つまり、回復可能(catch 可能) です。

ところが、Opcode 生成時に受け入れられなかったコードについては Compiler Error (Fatal) となります。

php スクリプトとして成り立ってないようなコードを eval した場合は 例外として catch 可能なのに、一見 php スクリプトっぽいのに NGなコードは Fatal です。

ちょっとどっかで嵌ることがありそうな気がしますね。


その他のコンパイル時最適化

最適化の例については半分ほど省略しました。以下に省略した最適化例を挙げます

静的関数展開(Opcode 変換)

http://www.slideshare.net/do_aki/php-ast#25

静的関数展開の一環で、関数呼び出しではなく Opcode に変換してしまいます。

以前の php であれば、これを実現するためには、関数ではなく言語構造にする(これらの関数を構文ルールに加えてしまう) しかなかったのではないかと思います。

しかしそれではさらに構文が増えることになり煩雑になるため、導入したくても出来なかったものではないのかなという憶測。

静的関数展開の無効化

http://www.slideshare.net/do_aki/php-ast#26

実は 静的関数展開 は無効にすることができる(ようなコードが用意されています)

どう使うのか、何の目的で用意されているのかは不明。

静的zval構築

http://www.slideshare.net/do_aki/php-ast#27

いままでの配列リテラルは実は結構コストかかっていたことを示す例となっています。

Opcache ではやっていそうな最適化ではありますが。

静的ショートサーキット

http://www.slideshare.net/do_aki/php-ast#28

これは、最適化と言うには少々微妙な例。

本来なら if (false) となったら、その内部のブロックを含め無くしてしまえば良いはずなのですが、そうはなっていません。

また、もともと実行される予定のないコードの部分について Opcode が生成されなくなるだけですので、実行速度への影響はほぼないでしょう。



コンパイルタイミングによって Opcode が変換する例

http://www.slideshare.net/do_aki/php-ast#31

http://www.slideshare.net/do_aki/php-ast#32

このスライドで説明したかったことは、autoload よりも あらかじめ include (require) しておくほうが最適化されるケースがあるよ ということです。

`php echo.php` は autoload を模しており、 autoload によってクラスが読み込まれる時に参照した定数は畳み込まれず、定数参照として Opcode が生成されます。

これがどの程度実際のパフォーマンスに影響してくるかは計測していませんが、もしかすると、Opcache は最初に作った Opcode をキャッシュするのではなく、2回目読み込んだ際に再度 Opcode を構築してキャッシュすることで速度向上が見込めるんじゃないか。という妄想も出来るわけですね。

HHVM における AST

http://www.slideshare.net/do_aki/php-ast#53

実は php 本家だけではなく、 hhvm についても少し調べていました。

hhvm は php5 用のパーサと php7 用のパーサを持っていて、(おそらくコンパイル時に)切り替えて使っているようですね。

本家の広い環境でコンパイルされることを前提としたコードとは異なり、 (C++ の)比較的新しい機能も利用しているところが特徴的だと感じました。

AST については、構成がほぼ同じなため、C で書く事と C++ で書く事の違いがはっきりと表れています。

hhvm の場合は1つのASTノード種を 1つの class で表現しています。

AST から phpスクリプトへの逆変換

トークでは php に実装された 関数を紹介しましたが、実は、php-ast-reverter (https://packagist.org/packages/tpunt/php-ast-reverter) というライブラリもあります。これは php-ast によって得た AST (のコピー) を元に php スクリプトを再構築するものです。

これを参考にすれば、AST から異なるコードへの変換は比較的容易ではないでしょうか。

最後に

phpカンファレンススタッフのみなさまありがとうございました。

また、他にもおもしろい講演がある中、だいぶニッチな箇所に焦点を当てた講演を聞いてくださった方ありがとうございます。

とりあえず、今後は今書いてる拡張をまともに動かせるようにしようとおもいます。

2015-12-10

生成される 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 になってしまうと再計算が行われるためにメモリを書き換えようとしてクラッシュするようです。


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


参照

Hash algorithm and collisions — PHP Internals Book

SipHashについてのメモ - Qiita

2015-12-09

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 開発陣には頭の下がる思いですね。

2015-12-05

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) をフラグとして利用

2015-12-04

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 関数で表されています。