入出力系システムコールの基礎的な負荷を可視化

ID: 31
creation date: 2009/10/19 15:49
modification date: 2009/10/19 15:49
owner: mikio

openとかreadとかのシステムコールを呼ぶだけの簡単なお仕事に携わる皆様、こんにちは。

普段自分が使っているシステムコールって、他にオーバーヘッドがない理想的な状態だとどれくらいの負荷になるんだろう。そんな漠然とした疑問を持ちつつも、面倒なんで自分で測ることなく日々を過ごしている人も多いかもしれない。それじゃいけない。純粋にそのシステムコールを呼ぶとどれくらいの負荷がかかるのかを知っておかねばならない。

ということで、簡単なテストプログラムを書いてみた。各システムコールの典型的な組み合わせでどれくらい時間がかかるかを見るのだ。「gcc -std=c99 -Wall -O2 -o mytest mytest.c」でビルドできる。結果は環境依存性が強いだろうから、ぜひ自分の環境で動かしてみてほしい。

#define _XOPEN_SOURCE 500

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/time.h>

#define LOOPNUM 1000000
#define IOSIZE 8

double mytime(void){
  struct timeval tv;
  gettimeofday(&tv, NULL);
  return tv.tv_sec + (double)tv.tv_usec / (1000 * 1000);
}

int main(int argc, char **argv){

  // open and close
  double stime = mytime();
  for(int i = 0; i < LOOPNUM; i++){
    int fd = open("TESTFILE", O_RDWR | O_CREAT, 00644);
    close(fd);
  }
  double etime = mytime() - stime;
  printf("open&close: total=%.8f  qps=%.0f\n", etime, LOOPNUM / etime);

  // open and write and close
  stime = mytime();
  for(int i = 0; i < LOOPNUM; i++){
    int fd = open("TESTFILE", O_RDWR | O_CREAT, 00644);
    write(fd, "hogehoge", IOSIZE);
    close(fd);
  }
  etime = mytime() - stime;
  printf("open&write&close: total=%.8f  qps=%.0f\n", etime, LOOPNUM / etime);

  // open and read and close
  char buf[256];
  stime = mytime();
  for(int i = 0; i < LOOPNUM; i++){
    int fd = open("TESTFILE", O_RDWR | O_CREAT, 00644);
    read(fd, buf, IOSIZE);
    close(fd);
  }
  etime = mytime() - stime;
  printf("open&read&close: total=%.8f  qps=%.0f\n", etime, LOOPNUM / etime);

  // lseek and write
  int fd = open("TESTFILE", O_RDWR | O_CREAT, 00644);
  stime = mytime();
  for(int i = 0; i < LOOPNUM; i++){
    lseek(fd, 0, SEEK_SET);
    write(fd, "hogehoge", IOSIZE);
  }
  etime = mytime() - stime;
  printf("lseek&write: total=%.8f  qps=%.0f\n", etime, LOOPNUM / etime);

  // lseek and read
  stime = mytime();
  for(int i = 0; i < LOOPNUM; i++){
    lseek(fd, 0, SEEK_SET);
    read(fd, buf, IOSIZE);
  }
  etime = mytime() - stime;
  printf("lseek&read: total=%.8f  qps=%.0f\n", etime, LOOPNUM / etime);

  // pwrite
  stime = mytime();
  for(int i = 0; i < LOOPNUM; i++){
    pwrite(fd, "hogehoge", IOSIZE, 0);
  }
  etime = mytime() - stime;
  printf("pwrite: total=%.8f  qps=%.0f\n", etime, LOOPNUM / etime);

  // pwrite
  stime = mytime();
  for(int i = 0; i < LOOPNUM; i++){
    pread(fd, buf, IOSIZE, 0);
  }
  etime = mytime() - stime;
  printf("pread: total=%.8f  qps=%.0f\n", etime, LOOPNUM / etime);

  // mmap and output
  void *map = mmap(NULL, IOSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
  stime = mytime();
  for(int i = 0; i < LOOPNUM; i++){
    memcpy(map, "hogehoge", IOSIZE);
  }
  etime = mytime() - stime;
  printf("mmap&output: total=%.8f  qps=%.0f\n", etime, LOOPNUM / etime);

  // mmap and input
  stime = mytime();
  for(int i = 0; i < LOOPNUM; i++){
    memcpy(buf, map, IOSIZE);
  }
  etime = mytime() - stime;
  printf("mmap&input: total=%.8f  qps=%.0f\n", etime, LOOPNUM / etime);

  munmap(map, IOSIZE);
  close(fd);

  return 0;
}

俺の環境だと、以下のような結果になった。100万回の実行なので、total timeの秒数を1回のマイクロ秒数に読み替えられる。

operations total time (sec) throughput (/sec)
open&close 2.47212291 404511
open&write&close 8.41249514 118871
open&read&close 2.87695003 347590
lseek&write 1.94846702 513224
lseek&read 0.72205496 1384936
pwrite 1.76872778 565378
pread 0.53991008 1852160
mmap&output 0.00088000 1136359794
mmap&input 0.00085902 1164114349

考察の前に、このテストケースでは、あくまで単一のファイルの特定の部分を読み書きするという特殊性があることに留意されたい。実際のユースケースでは、複数のファイルを読み書きするかもしれないし、おそらくファイル内の特定でない部位をシークして読み書きすることだろう。したがって、各種のレベルのキャッシュ機構やロック機構に起因する負荷が実際には加わることになるので、当然上記のスループットは実際のユースケースでは期待できないだろう。

その上で、興味深い点がいくつか考察できるので、箇条書きにしてみる。

  • open&close は意外に早い。両者でたった2.47usしかかからない。ただ、ここでは更新をしていないので、キャッシュのフラッシュ処理がないから早いだけだ。
  • その証拠に、open&write&close だと一気に遅くなる。おそらくフラッシュ処理のために、closeの時間はwriteの量に強く相関する。
  • open&write&close は lseek&write に比べると4倍くらい遅い。よって、ファイルディスクリプタは使いまわした方が当然よい。とはいえ、所詮4倍しか変わらず遅くとも11万QPSくらい出るため、要求スループットがそれほどタイトではないユースケースではファイルを毎回開いても実は問題ないとも言える。
  • open&read&close も lseek&read に比べてやはり4倍くらい遅い。よって、以下同文。
  • lseek&write に比べて pwrite はちょっと早い。よって、pwriteが使える場合には使うべきである。
  • lseek&read に比べて pread はちょっと早い。よって、preadが使える場合には使うべきである。
  • mmap の入出力は桁違いに早い。よって、可能であれば、ひたすらmmapを使うとよい。

総括として言いたいことは、以下の2点である。

  • カジュアルなユースケースでは、毎回ファイルを開いて閉じても別に構わない。
  • シリアスなユースケースでは、なるべくmmapを使った方がよい。

繰り返しになるが、この結果はあくまで傾向を見るためのものである。実際のユースケースではもっと複雑な要因が絡み合う。しかし、各システムコールが基本的にどのような負荷を伴うのかを単純なテストケースで確認しておくことは有用である。

0 comments
riddle for guest comment authorization:
Where is the capital city of Japan? ...