単純にシステムコールを使えば、プログラムの処理速度が一気に上がるという都合のいい話はありませんと説明してきました。しかし、簡単に使えて、ある程度の処理速度向上を見込めるシステムコールも存在します。今回は、このシステムコールを使うと、どうして処理速度が上がるのかということを解説します。この点を理解すると、プログラムの処理速度を上げるための戦略が見えてくるはずです(編集部)
前回は、システムコールを直接記述する場合には「使い方を間違えると、まったく性能を発揮できないひどいプログラムができてしまう」ということを、実例を交えて紹介した。標準ライブラリの機能はよく考えて作ってあり、特に強い理由や、はっきりとした目的があるというわけでもなければ、システムコールは利用せずに標準ライブラリを利用した方が良い結果を得られることが多い。
今回からしばらくの間は、まったく逆の例、つまり使うとプログラムの処理性能が上がるというシステムコールを紹介していく。システムコールを呼ぶ回数は少ない方が処理性能は高くなるという原則は変わらないが、呼び出しておくと処理性能が向上するシステムコールというものが存在するのだ。こうしたシステムコールを使わないでいることは、とてももったいない。
今回紹介するシステムコールは「mmap(2)」だ。ここでは詳しく仕組みを解説しないが、mmap(2)は、プログラムの処理性能に必ず良い影響を与える。Linux、FreeBSD、Mac OS X、Solaris、AIXなどUNIX系のOSでは有名なシステムコールであり、多くのアプリケーションやコマンドが利用している。mmap(2)を利用するためにシステムコールの使い方を調べたという人も多いかもしれない。それほど、mmap(2)はプログラムに良い影響をもたらす。
今回もファイルコピーという処理を例に取り、その処理速度を比較しながら、それぞれのシステムコールの動きや、もたらす効果の違いについて解説していく。前回も取り上げたが、まずは標準ライブラリ関数fgetc(3)、fputc(3)を使ってファイルをコピーするプログラムのソースコードを次に示す。ソースコードを短くしたいので、ファイルを閉じる処理は省いた。このプログラムでは、最終的にread(2)、write(2)といったシステムコールを呼び出すことになる。
#include 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); } }
コードを記述して、「copy-fgetc-fputc.c」という名前でファイルとして保存したら、次のようにコマンドを実行してビルドする。
clang -o copy copy-fgetc-fputc.c
続いて、read(2)やwrite(2)といったシステムコールを駆使して記述したコードのサンプルを見ていただこう。簡略化してあるが、これも基本的に前回用意したものと同じだ。今回は100Mbytesのファイルをコピーするようにしたので、ファイルサイズは100Mbytes(104857600bytes)に固定してある。バッファサイズを100Mbytesにしてread(2)を1回、write(2)も1回だけ使って処理をしているところがこのソースコードのポイントとなる。
#include #include int main(void) { int fdi, fdo, fsize = 104857600; char b[fsize]; fdi = open("in", O_RDONLY); fdo = open("out", O_WRONLY); read(fdi, b, fsize); write(fdo, b, fsize); }
先の例と同じようにコードの記述が済んだら、「copy-read-write.c」という名前でファイルとして保存し、次のようにコマンドを実行してビルドする。
clang -o copy copy-read-write.c
単純に考えると、100Mbytesもの巨大なバッファを用意して、システムコールも使ったcopy-read-write.cの方が高速に動作しそうだ。しかし、利用するファイルシステムの影響やキャッシュの影響などもあるので、測定結果はばらつくものの、この程度ならそれほど大きく結果に差が出ることはない。fgetc(3)やfputc(3)はよくできており、使用するメモリ量も少なく、処理も高速だ。今回のケースではシステムコールを呼び出した回数ではなく、別の要因がプログラムの処理性能を劣化させている。
それでは、上記の2つのサンプルプログラムを書き換えて、mmap(2)を利用するようにしてみよう。その結果が下に示すサンプルコードだ。ソースコードをシンプルにするためにファイルを閉じる処理を省いてあるほか、最初からinとoutという2つのファイル(ファイルサイズはそれぞれ100Mbytes)がファイルシステム上に存在することを前提にしている。
#include #include #include 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++; } }
このサンプルも、先の例と同じようにコードの記述が済んだら、「copy-mmap.c」という名前でファイルとして保存し、次のようにコマンドを実行してビルドする。
clang -o copy copy-mmap.c
ここからは、mmap(2)を利用するサンプルコード(copy-mmap.c)の一部を抜粋しながら、その部分が意味するところを解説していく。まずは、システムコールopen(2)を呼び出すところだ。ファイルを開いてファイルディスクリプタを得ている。指定するフラグを変えてあるものの、この処理までは100Mbytesのバッファを確保したサンプル(copy-read-write.c)とほぼ同じ。
fdi = open("in", O_RDONLY); fdo = open("out", O_RDWR);
copy-read-write.cでは、このあとread(2)/write(2)でファイルをコピーしているが、mmap(2)を使ったサンプルでは、ここでファイルをメモリにマッピングしている。mmap(2)には、ほかにもいくつか機能がある。詳しい説明はmmap(2)のマニュアルを参照してほしい。
ファイル全体を読み込み専用でメモリにマッピングしているのが次に挙げる部分だ。ここから先、メモリへの操作はファイルへの操作に置き換わる。
i = mmap(NULL, fsize, PROT_READ, MAP_SHARED, fdi, 0);
次はmmap(2)を呼び出して、書き出し専用でファイルをメモリにマッピングしている。直前に挙げた例と同様、この処理を実行すると、以降のメモリへの操作、つまり書き込みは、そのままファイルへの書き込みということになる。
o = mmap(NULL, fsize, PROT_WRITE, MAP_SHARED, fdo, 0);
in(コピー元)とout(コピー先)をメモリにマッピングしたので、あとは次のようにメモリ上でデータをコピーする処理を記述してやれば良い。ポインタの使い方を知らないと、次のコードはちょっと理解しにくいかもしれないが、メモリの内容を逐次コピーしていると思ってもらえれば良い。
while (fsize--) { *o++ = *i++; }
次のように書いた方が理解できるという方も多いかもしれない。
for (int j = 0; j < fsize; j++) { o[j] = i[j]; }
mmap(2)の機能、仮想メモリの仕組みとページングの仕組みが分かっていないと、なぜこのコードでファイルをコピーできるのか理解できないかもしれない。詳しい仕組みはおいおい説明するとして、ここでは「mmap(2)を使うとファイルをメモリにマッピングできる」ということだけを把握してほしい。
Copyright © ITmedia, Inc. All Rights Reserved.