連載
» 2012年02月21日 00時00分 UPDATE

知ってトクするシステムコール(4):高速化に効くシステムコールが犠牲にするもの (1/2)

前回、mmap(2)というシステムコールを使って、ファイルコピーの速度を上げる方法を解説しました。このシステムコールは大きな問題を起こすこともないので、使いやすく、実際に多くの開発者が利用しています。今回は、プログラムが使用した物理メモリサイズやページフォールトの回数、コンテキストスイッチの回数などを調べて、mmap(2)の性格を分析していきます(編集部)

[後藤大地,@IT]

前回とは違う側面からmmap(2)を調べる

 前回、ファイルをコピーするためのシステムコールの一種である「mmap(2)」を紹介した。このシステムコールを使えば、「read(2)」や「write(3)」といったシステムコールを使うよりも高速に動作するファイルコピープログラムを作れる。処理にかかった平均時間を比較したところ、mmap(2)を利用したプログラムは、read(2)/write(2)を使ったプログラムの4〜5倍高速に動作していた。

 mmap(2)はプログラムの処理性能を引き上げることを考えた時に、多くの開発者が思い付き、実際に利用しているシステムコールだ。経験を積んだ開発者にとっては「おなじみの」システムコールと言えるかもしれない。UNIX系OSを構成するプログラムの中にも、活用しているものが多い。

 前回は、mmap(2)を利用したファイルコピープログラムをの動きをtruss(1)コマンドで調べ、どのようなシステムコールをどれくらい頻繁に呼び出しているのかを調べた。今回は「time(1)」というコマンドも使って、違う側面からmmap(2)の性格を調べてみる。

 サンプルプログラムの作成には、連載「いまさら聞けないVim」の第8回で紹介した環境を利用する。OSによるコマンドの違いなどは本連載の第2回にまとめてあるので、こちらも参考にしていただきたい。

3種類のサンプルで動作を比較

 今回使用するサンプルプログラムのソースコードを見ていこう。まずは標準ライブラリ関数である「fgetc(3)」と「fputc(3)」を使ったプログラム(copy-fgetc-fputc.c)だ。前回用意したものとまったく同じものである。関数fgetc(3)とfputc(3)は最終的にシステムコールread(2)/write(2)を呼び出す。シンプルで、かつ、それなりに高速に動作し、メモリもあまり消費しないプログラムだ。

#include <stdio.h>
int
main(void)
{
    FILE *fi, *fo;
    int b;
    fi = fopen("in", "r");
    fo = fopen("out", "w");
    b = fgetc(fi);
    while (EOF != b) {
        fputc(b, fo);
        b = fgetc(fi);
    } 
}

 続いて、システムコールmmap(2)を使ったコピープログラム(copy-mmap.c)を用意した。これも、前回用意したプログラムと同じものだ。mmap(2)を実行すると、read(2)/write(3)を使わなくとも、メモリにマッピングしたものをコピーするだけでファイルをコピーできる。サンプルのソースコードをなるべく短く、シンプルにするために、入力用に「in」というファイル、出力用に「out」というファイルがすでに存在していることを前提としている。実用のプログラムで利用するには、ファイルを用意する部分なども実装する必要があるが、そこまで用意するとソースコードが込み入ったものになるので、あらかじめテスト用ファイルが存在する前提でサンプルコードを作らせてもらった。

#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
int
main(void)
{
    int fdi, fdo, fsize = 104857600;
    char *i, *o;
    fdi = open("in", O_RDONLY);
    fdo = open("out", O_RDWR);
    i = mmap(NULL, fsize, PROT_READ, MAP_SHARED, fdi, 0);
    o = mmap(NULL, fsize, PROT_WRITE, MAP_SHARED, fdo, 0);
    while (fsize--) {
        *o++ = *i++;
    }
}

 最後に用意したサンプルプログラムは、標準ライブラリ関数memcpy(3)を利用したものだ(copy-mmap2.c)。memcpy(3)はライブラリ関数であって、システムコールではないのだが、コンパイル時にコンパイラが最適なバイナリコードに置き換えるため、ファイルをとても速くコピーできるという特性を持っている。実用の面から見ても有益なので、覚えておきたいライブラリ関数だ。

#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
int
main(void)
{
    int fdi, fdo, fsize = 104857600;
    char *i, *o;
    fdi = open("in", O_RDONLY);
    fdo = open("out", O_RDWR);
    i = mmap(NULL, fsize, PROT_READ, MAP_SHARED, fdi, 0);
    o = mmap(NULL, fsize, PROT_WRITE, MAP_SHARED, fdo, 0);
    memcpy(o, i, fsize);
}

比較の準備

 以上、用意した3つのソースコードは次のようにコンパイルすれば良い。

clang -o 出力ファイル名 ソースコード.c

 3種類とも、100Mbytesの一時ファイルをコピーするプログラムなので、大きさ100Mbytesの「in」という入力ファイルと、同じく大きさ100Mbytesの「out」という出力ファイルを用意する。それぞれのファイルにはランダムデータを書き込むようにする。これは、inとoutの中身が違うものになるようにして、ファイルコピー後に内容が同じものになることを確認するためだ。ファイルの中身の違いはハッシュ値を調べれば分かる。中身がまったく同じなら、ハッシュ値も同じものになる。

 入力ファイルであるinは次のようにコマンドを実行して用意する。

dd if=/dev/random of=in bs=512 count=204800

 出力ファイルoutも同じようにコマンドを実行して用意する。

dd if=/dev/random of=out bs=512 count=204800

メモリ使用量なども調べる

 先に説明したように、今回はtruss(1)コマンドでシステムコール呼び出し回数を調べるだけでなく、time(1)コマンドを利用して物理メモリの使用量、ページフォールトの発生回数、コンテキストスイッチの発生回数なども調べる。truss(1)コマンドは以下のように実行すれば良い。

truss -c ./プログラム名

 今回使用するtime(1)コマンドは、シェル組み込み関数のtimeではなく、FreeBSD/PC-BSDのユーザーランドにインストールされている/usr/bin/timeであることに注意してほしい。今回調べるデータを得るには、以下のように「-lph」オプションを付けて実行すれば良い。

/usr/bin/time -lph ./プログラム名

 なお、/usr/bin/timeは同じ結果を得るためのオプションがOSによって異なる。例えばMac OS Xなら「-lp」、Ubuntuなら「-pv」となる。

 テストをまとめて実行するために、Makefileを作ると次のようになる。

CC=clang
all: test1 test2 test3 test4 test5 test6 test7 test8
build: copy-fgetc-fputc copy-mmap copy-mmap2 
copy-fgetc-fputc:
        ${CC} -o copy-fgetc-fputc copy-fgetc-fputc.c
copy-mmap:
        ${CC} -o copy-mmap copy-mmap.c
copy-mmap2:
        ${CC} -o copy-mmap2 copy-mmap2.c
test1: copy-fgetc-fputc
        dd if=/dev/random of=in bs=512 count=204800
        dd if=/dev/random of=out bs=512 count=204800
        md5 in out
        truss -c ./copy-fgetc-fputc
test2: copy-fgetc-fputc
        dd if=/dev/random of=in bs=512 count=204800
        dd if=/dev/random of=out bs=512 count=204800
        md5 in out
        /usr/bin/time -lph ./copy-fgetc-fputc
test3: copy-mmap
        dd if=/dev/random of=in bs=512 count=204800
        dd if=/dev/random of=out bs=512 count=204800
        md5 in out
        truss -c ./copy-mmap
test4: copy-mmap
        dd if=/dev/random of=in bs=512 count=204800
        dd if=/dev/random of=out bs=512 count=204800
        md5 in out
        /usr/bin/time -lph ./copy-mmap
test5: copy-mmap2
        dd if=/dev/random of=in bs=512 count=204800
        dd if=/dev/random of=out bs=512 count=204800
        md5 in out
        truss -c ./copy-mmap2
test6: copy-mmap2
        dd if=/dev/random of=in bs=512 count=204800
        dd if=/dev/random of=out bs=512 count=204800
        md5 in out
        /usr/bin/time -lph ./copy-mmap2
test7: 
        dd if=/dev/random of=in bs=512 count=204800
        dd if=/dev/random of=out bs=512 count=204800
        md5 in out
        truss -c cp in out
test8: 
        dd if=/dev/random of=in bs=512 count=204800
        dd if=/dev/random of=out bs=512 count=204800
        md5 in out
        /usr/bin/time -lph cp in out    
clean:
        rm -f copy-fgetc-fputc copy-mmap copy-mmap2 in out

 2ページ目では、truss(1)とtime(1)で調べた結果から、それぞれのサンプルプログラムの特性を分析していく。

       1|2 次のページへ

Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。