PHPのuniqid()を連続で呼んでも重複しないのは何故か

Hiroshi Ohtake
paronym
Published in
4 min readDec 1, 2020

--

この記事はPARONYM Advent Calendar 2020の1日目です。

とあるコードレビュー中にループ内で uniqid() を連続で呼んでいるのを見かけて、表題の件についてふと疑問に思ったので軽く調べました。

uniqid ([ string $prefix = "" [, bool $more_entropy = FALSE ]] ) : string 

“マイクロ秒単位の現在時刻にもとづいて”一意なIDを生成する uniqid()

つまりは、完全にマイクロ秒単位で同一時刻の呼び出しでは同じ値が返ってくるはずです。(prefixもmore_entropyも使用しない場合)

最近の数GHzで動作するCPUならば1マイクロ秒の間に数千クロックの命令が実行出来ます。
いくらPHPが速度面で優れている訳ではないとは言え、uniqid()ぐらい1マイクロ秒もかからないはず。
連続で呼んだときに重複しないの?

という事で確認してみましょう。
1000万回 uniqid() を呼んで経過時間と重複が無いかをチェックします。

uniqid()確認用コード
time: 11.015179 (sec)
count: 10000000 / unique: 10000000

※ MacBookPro 2017 — Core i7 2.9GHz (4コア) で数回実行した際の中央値 (以下同様)

1000万回実行してみても重複する値は無いようです。
1マイクロ秒 * 10000000回 = 10秒 なので1回の呼び出しあたり1マイクロ秒以上かかるのが保証されているように見えます。

uniqid()の内部実装を確認してみましょう。
折角なので先日正式リリースされたばかりのPHP 8.0のソースを見てみます。

part of php-8.0.0/ext/standard/uniqid.c

コード上にコメントされている通りですが、前回の呼び出し時からマイクロ秒単位で変化が無い場合はポーリングで待っています。
スリープ中に他プロセスに実行権が渡ってしまい1マイクロ秒を大幅に超えるのを回避するためにusleep(1)は使っていないようです。

なお、過去のPHPバージョンでは more_entropyフラグが指定されていなければ usleep(1) するという実装でした。(PHP 7.0.22で確認)

ともかく、実行に1マイクロ秒以上の経過を保証することで値の重複を防いでいることが確認出来ました。

※ あくまでシングルスレッドで実行する場合にその範囲においての話です。マルチスレッド/プロセス/ホスト間ではそのまま使うと普通に重複します。prefixとmore_entropyを指定しましょう。

おまけでuniqid.cの do-while ループのポーリングを削ってビルドしたPHPで先程のコードを再度実行してみると…

time: 2.998325 (sec)
count: 10000000 / unique: 1909486

uniqid() 1回あたりの呼び出しが1マイクロ秒未満となり、値の重複が発生する結果となっています。

ちなみに、このビルドでuniqid()のmore_entropyフラグをtrueにして実行してみた結果がこちらです。

time: 5.401482 (sec)
count: 10000000 / unique: 10000000

1回あたりの呼び出しは1マイクロ秒未満ですが、重複が無くなりmore_entropyの効果についても理解できると思います。(同一マイクロ秒でも重複する確率が下がるだけで重複することはありえます。)

uniqie()使う時は基本prefixとmore_entropyフラグは付けておけ、で済む話なのですが、実装の中身を確認してみるのも面白いよという話でした。

※ 公式ドキュメントに書いてあることですが、そもそもuniqid()は戻値の一意性を保証するものではないので用途は良く考えましょう。

※ uniqid()を暗号用途としては使うなよ!絶対だぞ!

それではまた。

--

--