工作と競馬2

電子工作、プログラミング、木工といった工作の記録記事、競馬に関する考察記事を掲載するブログ

nanosleepの精度を上げる努力をしてみる

概要

C/C++のsleep系の関数のひとつnanosleepで、時間精度を上げる方法を検討し、改善できた。(だいたい10マイクロ秒以下)


背景と目的

Raspberry Piで、10マイクロ秒オーダーのsleepをしたかったのだが、C/C++のsleep系の関数のひとつnanosleepを使用したところ、関数の名前に反してあまり精度が良くなかった。そこで、少しだが精度を稼ぐ方法を検討してみる。


詳細

0.実行環境


1.状況確認

nanosleepを実行したときに、実際にどのくらいスリープするかを、よくあるgettimeofday関数を使って以下のコードで測定してみた。 スリープ時間は100usecとした。

コード1: nanosleep現状確認

#include <stdio.h>
#include <time.h>

int main(void) {

  struct timespec ts0;
  struct timespec ts1;
  struct timespec req;

  req.tv_sec = 0;
  req.tv_nsec = 1000 * 100; // 100usecを与える

  clock_gettime(CLOCK_REALTIME, &ts0);

  nanosleep(&req , NULL);

  clock_gettime(CLOCK_REALTIME, &ts1);
  
  printf("time0: %10ld.%09ld\n", ts0.tv_sec, ts0.tv_nsec);
  printf("time1: %10ld.%09ld\n", ts1.tv_sec, ts1.tv_nsec);
  printf("delta: %10ld.%09ld\n", ts1.tv_sec - ts0.tv_sec, ts1.tv_nsec - ts0.tv_nsec);

}

結果は、

time0: 1575178266.950976692
time1: 1575178266.951192627
delta:          0.000215935

で、216usecとなり、設定よりも100usec以上長いのだが、これは、

  • TS = clock_gettimeで時間を取得してから関数を出るまで
  • TE = clock_gettimeに入ってから時間を取得するまで
  • TN = nanosleepの設定時間分のスリープ以外の時間
  • T_SLEEP = 設定したいスリープ時間、ここでは100usec

という要素が関わって以下のような関係になっていると考えられる。

delta = TS + TE + TN + T_SLEEP --- 式1

TSやTEは計測に関わる時間であり、本来のプログラム中では発生しない時間なのでどうでもいいとして、TNがnanosleepの時間精度を悪化させる要因である。


2.改善案

基本的には、TNの分を差し引いてT_SLEEPを与えてやることで、所望の時間だけsleepができそうだ。なので、TNを調べる方法を検討する。

nanosleepをX回実行した場合、表示される時間delta_xは、

delta_x = TS + TE + (TN + T_SLEEP) * X --- 式2

となるはずだから、先ほどのdeltaとの差を取ると、

TN = (delta_x - delta) / (X - 1) - T_SLEEP

となり、TNが推定できると考えられる。そこで、以下のようなコード、つまりX=10の場合を作成し、TNを求めてみた。

コード2: nanosleep X回実行時

#include <stdio.h>
#include <time.h>

int main(void) {

  struct timespec ts0;
  struct timespec ts1;
  struct timespec req;

  req.tv_sec = 0;
  req.tv_nsec = 1000 * 100; // 100usecを与える

  clock_gettime(CLOCK_REALTIME, &ts0);

  nanosleep(&req , NULL);
  nanosleep(&req , NULL);
  nanosleep(&req , NULL);
  nanosleep(&req , NULL);
  nanosleep(&req , NULL);
  nanosleep(&req , NULL);
  nanosleep(&req , NULL);
  nanosleep(&req , NULL);
  nanosleep(&req , NULL);
  nanosleep(&req , NULL);

  clock_gettime(CLOCK_REALTIME, &ts1);
  
  printf("time0: %10ld.%09ld\n", ts0.tv_sec, ts0.tv_nsec);
  printf("time1: %10ld.%09ld\n", ts1.tv_sec, ts1.tv_nsec);
  printf("delta: %10ld.%09ld\n", ts1.tv_sec - ts0.tv_sec, ts1.tv_nsec - ts0.tv_nsec);

}

結果は、

time0: 1575178344.374890977
time1: 1575178344.376587469
delta:          0.001696492

となったので、

TN = (1696.492 - 215.935) / 9 - 100 = 64.5 usec

と推定できる。ここから、式1に代入して、

TS + TE = 51.5 usec

と、おおよその値がわかる。コード2のreq.tv_nsecに100 - 64.5 = 35.5 usecを設定して計測すると、

time0: 1575178695.052308488
time1: 1575178695.052460101
delta:          0.000151613

となり、表示上の所要時間は151.6usecである。TS + TEの分51.5 usecを引けば、

nanosleep1回分のsleep時間 = 100.1 usec

とほぼ設定値通りの時間になる。ただし、TNの値は、CPUの使用状況などによって変わったりする可能性があり、実際に上記のコードを何度か走らせてみると、±1割程度つまり、5~10マイクロ秒程度のばらつきがある。というわけで、

TNの分を差し引いてnanosleepに値を設定してやれば、5~10マイクロ秒程度の精度でsleepできそう

だといえる。


3.注意点

  • この方法は、TNよりも短い時間のsleepは設定できない
  • TNは、毎回固定値を使うのではなく、sleepさせたいタイミングの直前に、TNを算出し直して適用するともう少し精度もあがるのかもしれない。


まとめと今後の課題

nanosleepで、精度を向上できた。