PHP usleepより高い精度でスリープしたい

###TEST1###

まず、普通に`usleep`で1秒スリープしてみます。

`usleep(1e6)`で1秒スリープ。
(1e6は1000000、つまり100万マイクロ秒=1秒です)
開始、終了はできるだけ正確に計りたいのでここだけ`hrtime`(PHP7.3以降で使用可)を使いました。

~~~php:test1.php
<?php
sleep(1);
for($i = 0; $i < 1000; $i++) {
    $start = hrtime();
    usleep(1e6);
    $end = hrtime();
    printf("%.6f\n", $end[0] - $start[0] + ($end[1] - $start[1]) / 1e9);
}
~~~

|Windows|Linux|
|:-:|:-:|
|1.000058<br>1.007640<br>0.999958<br>1.000146<br>1.000745<br>0.999949<br>1.000058<br>1.000078<br>0.999953<br>1.000141|1.000105<br>1.000149<br>1.000147<br>1.000155<br>1.000174<br>1.000152<br>1.000152<br>1.000149<br>1.000150<br>1.000154|
1000回ループのうちの最終10回分。
以降のテストも同様。
最後に各テストごとに1000回分の平均をまとめてあります。

Windows版PHPでは`usleep`が指定した値より早く終了してしまう場合があります。
これについては、こちらの方が詳しく調べていらっしゃいます。
[PHPのsleep関数とusleep関数の挙動を調べてみた - hnwの日記](https://hnw.hatenablog.com/entry/20170717)

同様に、私が調べた限りでは`time_sleep_until`や`time_nanosleep`もWindowsでは指定した値よりも早く終了してしまう場合がありました。

###TEST2###

次に、`microtime`関数を監視して目標時間までループさせる処理にしてみました。

~~~php:test2.php
<?php
sleep(1);
for($i = 0; $i < 1000; $i++) {
    $start = hrtime();
    usleep_(1e6);
    $end = hrtime();
    printf("%.6f\n", $end[0] - $start[0] + ($end[1] - $start[1]) / 1e9);
}

// 目標時間までループ
function usleep_($microSec) {
    $targetTime = microtime(true) + $microSec / 1e6;
    while($targetTime > microtime(true));
}
~~~

|Windows|Linux|
|:-:|:-:|
|1.000002<br>1.000003<br>1.000002<br>1.000002<br>1.000002<br>1.000002<br>1.000002<br>1.000002<br>1.000002<br>1.000002|1.000002<br>1.000003<br>1.000002<br>1.000003<br>1.000002<br>1.000002<br>1.000002<br>1.000003<br>1.000002<br>1.000003|

Windows、Linuxともに単純に`usleep`を使った時よりもかなり精度は上がりました。
但しこれは全力でループが回って無駄にCPU負荷が上がってしまう、良くない処理です。

###TEST3###

次に、目標までの時間を`usleep`でスリープさせてCPU負荷が無駄に上がらないようにしました。
残り時間が少なくなるにつれ誤差が少なくなるよう、`usleep`にはループごとの残り時間の半分を指定します。
全体で1秒スリープの場合なら、最初のループで0.5秒スリープ、次のループではその半分の0.25秒スリープ… となり、更に細かくなる頃には`while`の条件が`false`になりループを抜けます。

~~~php:test3.php
<?php
sleep(1);
for($i = 0; $i < 1000; $i++) {
    $start = hrtime();
    usleep_(1e6);
    $end = hrtime();
    printf("%.6f\n", $end[0] - $start[0] + ($end[1] - $start[1]) / 1e9);
}

// ループごとに目標時間までの半分をusleepで待機
function usleep_($microSec) {
    $targetTime = microtime(true) + $microSec / 1e6;
    while(($diffSec = $targetTime - microtime(true)) > 0) {
        usleep($diffSec * 5e5);
    }
}
~~~

|Windows|Linux|
|:-:|:-:|
|1.015564<br>1.000082<br>1.015602<br>1.001342<br>1.000101<br>1.015589<br>1.000046<br>1.015584<br>1.000133<br>1.000064|1.000042<br>1.000127<br>1.000039<br>1.000046<br>1.000030<br>1.000032<br>1.000033<br>1.000029<br>1.000044<br>1.000034|

CPU負荷はほぼ問題なくなりますが、Windowsでの結果でのばらつきが大きく精度がよくありません。
Windowsでは`usleep`に指定する値がある程度小さくなると結果が変わらなくなってくるので、その影響が出ているのだと思います。

###TEST4###

次に`usleep`を使うのはそのままに、`while`ループ最後の1/1000秒だけは`usleep`無しにしてみます。

~~~php:test4.php
<?php
sleep(1);
for($i = 0; $i < 1000; $i++) {
    $start = hrtime();
    usleep_(1e6);
    $end = hrtime();
    printf("%.6f\n", $end[0] - $start[0] + ($end[1] - $start[1]) / 1e9);
}

// ループごとに目標時間までの半分をusleepで待機させつつ
// 残り時間の最後1/1000秒だけはusleepなし
function usleep_($microSec) {
    $targetTime = microtime(true) + $microSec / 1e6;
    while(($diffSec = $targetTime - microtime(true)) > 0) {
        if($diffSec > 0.001) usleep($diffSec * 5e5);
    }
}
~~~

|Windows|Linux|
|:-:|:-:|
|1.000117<br>1.005522<br>1.000104<br>1.000095<br>1.001568<br>1.000001<br>1.000106<br>1.000075<br>1.000066<br>1.000001|1.000001<br>1.000002<br>1.000001<br>1.000001<br>1.000001<br>1.000001<br>1.000002<br>1.000001<br>1.000001<br>1.000001|

CPU負荷もほとんど上がらず、Windowsでの精度もそこそこ良い結果になりました。
少なくとも、`usleep`を素のまま使うよりは正確かと思います。
CPUパワーに余裕があれば、負担にならない範囲でusleepしない時間を多めにすれば更にばらつきも減るかと思います。

**Windows、Linuxそれぞれでtest1.php～test4.phpを1000回ループさせた平均**

||Windows|Linux|備　　考|
|:-:|:-:|:-:|:-:|
|test1|1.001014|1.000146|素のusleep()|
|test2|1.000003|1.000003|microtime()監視でwhileループ<br>(高負荷)|
|test3|1.002505|1.000042|test2+ usleep()で負荷軽減|
|test4|1.000225|1.000001|test3+ 最後1/1000秒のみ<br>usleep()なし|
|参考|1.001476|1.000147|time_nanosleep(1, 0)での<br>1000回ループの平均|

参考で`time_nanosleep`も同じ条件で計測してみましたが、ナノ秒単位で細かく指定はできるものの、精度自体は`usleep`とたいして変わらなかったのが意外でした。