2
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-06

TEST1

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

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

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
1.007640
0.999958
1.000146
1.000745
0.999949
1.000058
1.000078
0.999953
1.000141
1.000105
1.000149
1.000147
1.000155
1.000174
1.000152
1.000152
1.000149
1.000150
1.000154

1000回ループのうちの最終10回分。
以降のテストも同様。
最後に各テストごとに1000回分の平均をまとめてあります。

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

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

TEST2

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

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
1.000003
1.000002
1.000002
1.000002
1.000002
1.000002
1.000002
1.000002
1.000002
1.000002
1.000003
1.000002
1.000003
1.000002
1.000002
1.000002
1.000003
1.000002
1.000003

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

TEST3

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

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
1.000082
1.015602
1.001342
1.000101
1.015589
1.000046
1.015584
1.000133
1.000064
1.000042
1.000127
1.000039
1.000046
1.000030
1.000032
1.000033
1.000029
1.000044
1.000034

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

TEST4

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

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
1.005522
1.000104
1.000095
1.001568
1.000001
1.000106
1.000075
1.000066
1.000001
1.000001
1.000002
1.000001
1.000001
1.000001
1.000001
1.000002
1.000001
1.000001
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ループ
(高負荷)
test3 1.002505 1.000042 test2+ usleep()で負荷軽減
test4 1.000225 1.000001 test3+ 最後1/1000秒のみ
usleep()なし
参考 1.001476 1.000147 time_nanosleep(1, 0)での
1000回ループの平均

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0