
みなさんこんにちは、ガノー(Twitter:Ganohr)です。
1年に1回くらいの割合で「先頭0埋めの固定長乱数値」を生成したいと思うことがあります。
こういった用途はDBなどの定義において、以前は「Auto Increment(AI)」で値を生成したが、
- 複数のDBをマージする上でAIだと都合が悪くなった
- 別システム上のDBとマージしないといけず、Unique Indexを効率的に生成する必要がある
- 件数が多いが、実際はそこまで厳密に識別可能な一意性は必要ない※
といった条件で利用されるように思います。
※ 数千万分の一程度の確率で同値が生成されて挿入/更新に失敗しても、その値は無視して良いといった状況が想定されます
このときDBサーバーで処理するのではなく、PHPプログラムでバッチ処理を行う場合などに「先頭0埋めの固定長乱数値」という実装が必要になります。
ただ、こうした用途の場合は「先頭1埋めの固定長乱数値」の方が適切かもしれませんけどね(本記事で実装を理由含め解説しています)。
更新履歴
2023/01/20 公開
PHP Tips!:sprintfよりも速く、先頭0埋めの固定長乱数値を生成するコード
ひとまずコード例は以下の通り。
function ganohrs_get_random_number_string(int $len = 9):string {
$max = pow(10, $len);
// echo "max = $max\n";
$rnd = mt_rand(0, $max - 1);
// echo "rnd = $rnd\n";
return substr($rnd + $max, 1);
}
echo ganohrs_get_random_number_string() . "\n";
※ PHP7.4にて実装。タイプヒンティングが無効なバージョンではintやstring等を除去してください。
このコードはganohrs_get_random_number_string(int):string
という関数が定義されています。
ganohrs_get_random_number_string(4)
とやれば4桁の先頭0埋めの数値の文字列が返却されます。
この定義は先頭0埋めを加算とsubstr
のみで実現しているため、理論上最速です。
ネットを検索すると「0埋め如きにsprintf
なんて使う無駄なゴミコード」が溢れてるんで、一応記事にしました。
ただし、mt_rand
は精度が比較的悪いので暗号化の用途で使ってはダメです。
加えて、理論上最速ではあっても実測上最速ではないので、その意味でも次に示す先頭1埋めを推奨します。
また、PHPは、
32ビット環境ではint
の範囲は「-2147483648
~ 2147483647
」なので、この関数では最大9桁までしか対応できません。
64ビット環境ならint
の範囲は「-9223372036854775808
~ 9223372036854775807
」なので、この関数では最大18桁までしか対応できません。
念のため桁数は意識して使ってください。
そして「DB等へ格納する用途では、先頭1埋めを推奨」します。
PHP Tips!:最速で先頭1埋めの固定長乱数値を生成するコード(先頭0埋めよりこっちを推奨)
function ganohrs_get_random_number_string(int $len = 9):string {
$min = pow(10, $len - 1);
$max = pow(10, $len);
// echo "max = $max\n";
$rnd = mt_rand($min, $max - 1);
// echo "rnd = $rnd\n";
return $rnd;
}
こちらもsprintf
を使わないので最速です。
先頭0埋めと1埋めはどちらも計算だけで実現できますね。
ちなみに先頭9埋めは計算ではできませんが、こちらもsprintfを使わずに実装できます。
function ganohrs_get_random_number_string(int $len = 9):string {
$max = pow(10, $len);
// echo "max = $max\n";
$rnd = mt_rand(0, $max - 1);
// echo "rnd = $rnd\n";
return str_pad($rnd, $len, '9', STR_PAD_LEFT);
}
echo ganohrs_get_random_number_string() . "\n";
こちらはstr_pad
を使う必要はありますが、ほとんど速度的な負荷がありません。
なぜ先頭0埋めよりも1埋めの方が良いのか?
DBへ値を入れた際に、数値型の場合先頭0は無視されるため、固定長になりません
あくまでも仕様上の注意点というか、結合試験等で案外ミスに繋がりやすいのが先頭0埋めの仕様です。
DBのテーブルへ固定長の整数値型へ値をインサートしたりアップデートした場合に、先頭の0を欠落させるか否かはエンジンごとにまちまちです(基本的に欠落します)。
場合によってはせっかく固定長にして入れ込んでも、結局DBから取得した値からは先頭の0が欠落しているため、いちいちプログラム側でそれを補填するという無意味な作業が発生することがあります。
そして、先頭を0で埋めるか1で埋めるかの違いは些末な問題であり、それによってデータの精度が落ちるといったことはありません。
したがって、「先頭0埋めは不具合を発生させうるが、先頭1埋めは不具合を発生させないため、1埋めを推奨する」というわけです。
速度検証
最後にどのくらい速度差があるのか確認しておきましょう。
検証コードは以下の通りです。各関数を100万回実行して検証しました。
$time_s = microtime(true);
for($i = 0; $i < 1000000; $i++) {
ganohrs_get_random_number_string();
}
$time_e = microtime(true);
$time_p = $time_e - $time_s;
echo "start $time_s\n";
echo "end $time_e\n";
echo "past $time_p\n";
【先頭0埋めの固定長乱数値】
start 1674159471.9139
end 1674159472.047
past 0.13305521011353
【先頭1埋めの固定長乱数値】
start 1675277675.3244
end 1675277675.4586
past 0.13426899909973
【先頭9埋めの固定長乱数値】
start 1674159509.8315
end 1674159509.9626
past 0.13107109069824
結果は9埋め版の方が早いですね。
また、よくあるsprintf
による実装も計測しましょう。
function ganohrs_get_random_number_string(int $len = 9):string {
$max = pow(10, $len);
// echo "max = $max\n";
$rnd = mt_rand(0, $max - 1);
// echo "rnd = $rnd\n";
return sprintf('%0' . $len . 'd', $rnd);
}
echo ganohrs_get_random_number_string() . "\n";
【sprintf
版先頭0埋めの固定長乱数値】
start 1674158811.7433
end 1674158812.9987
past 1.255362033844
…遅すぎて絶句。
約9.435倍も遅いってことが判明しました。
こんなのを堂々と公開しているネット記事ばかりなんでうんざりです。
弱い型付けと厳密な型付けによる速度検証
PHP7以降ではdeclare
構文にて厳密な型付けをファイル単位で有効化できます。
弱い型付けというのは、PHPの特徴の一つであり、文字列型で表現された値や数値型や浮動小数点数などの型を自動的に相互変換してくれる機能です。
JavaやC#などのオブジェクト指向プログラミングに慣れている方なら「オートボクシング」と言ったほうがわかりやすいかもしれません。
この弱い型付けはいうなれば毎回自動的に型変換が必要か確認するため、速度的に多少不利になります。
とはいえほぼ誤差レベルの差しか出ませんが、誤差を除いていくとやはり若干厳密な型付けを利用したほうが早くなります。
function ganohrs_get_random_number_string(int $len = 9, string $pad = '1'):string {
$max = pow(10, $len);
// echo "max = $max\n";
$rnd = mt_rand(0, $max - 1);
// echo "rnd = $rnd\n";
return str_pad($rnd, $len, $pad, STR_PAD_LEFT);
}
echo ganohrs_get_random_number_string() . "\n";
【弱い型付けによる速度検証】
start 1674159796.0418
end 1674159796.174
past 0.13219690322876
【厳密な型付けによる速度検証】
start 1675315804.3144
end 1675315804.439
past 0.1246018409729
※ 厳密な型付けのコードは次の結論を参照。
結論:phpでsprintfに替わり、先頭0/1/9埋めの乱数値を生成する最速コード
declare(strict_types=1);
function ganohrs_get_random_number_string(int $len = 9, string $pad = '1'):string {
$max = pow(10, $len);
// echo "max = $max\n";
$rnd = mt_rand(0, $max - 1);
// echo "rnd = $rnd\n";
return str_pad((string)$rnd, $len, $pad, STR_PAD_LEFT);
}
echo ganohrs_get_random_number_string() . "\n";
これは、省略可能な第二引数に先頭パディング文字として’0’や’1’や’9’を指定可能で、省略時は本記事おすすめの’1’が設定されるコードです。
速度はなとsprintf
の10倍以上も高速です。
ということでDB等で利用することを想定している場合はこちらのコードをご利用ください。
決してネットの有象無象のsprintf
を恥ずかしげもなく使っているサンプルコードに騙されないように!
コメントを書く