やはりあった? 高速化に効くシステムコール:知ってトクするシステムコール(3)(2/2 ページ)
単純にシステムコールを使えば、プログラムの処理速度が一気に上がるという都合のいい話はありませんと説明してきました。しかし、簡単に使えて、ある程度の処理速度向上を見込めるシステムコールも存在します。今回は、このシステムコールを使うと、どうして処理速度が上がるのかということを解説します。この点を理解すると、プログラムの処理速度を上げるための戦略が見えてくるはずです(編集部)
処理速度に大きな差
では、実際にどの程度処理速度が違うのか調べてみよう。まず、プログラム実行前に、100Mbytesのファイルを作っておく。以下のコマンドを実行すれば、in、つまりコピー元のファイルができる。
dd if=/dev/random of=in bs=512 count=204800
mmap(2)は「ファイルをメモリにマッピング」するものだ。out、つまりコピー先として100Mbytesのファイルを作ることも忘れてはいけない。以下のコマンドを実行してほしい。この段階でinをoutにコピーすると、コピープログラムが処理に成功しているかどうかを確認できないので注意してほしい。最初はdd(1)コマンドを使って、でそれぞれ個別にファイルを作成する。
dd if=/dev/random of=out bs=512 count=204800
プログラムでファイルの準備を自動化することもできるが、ソースコードが複雑になるので今回は止めておいた。まずmmap(2)がどう動くものなのかをつかんでほしい。
この状態で、inとoutのそれぞれのハッシュ値を求めると、値は異なったものになっているはずだ。
% md5 in out MD5 (in) = 4b5c11779d7f6d9ada20a308c1b15090 MD5 (out) = ff76dfcfd807c3dd3fa645fde5968704 %
ファイルのコピーが成功した後で再びハッシュ値を比較すると、次のように同じ値になるはずだ。
% md5 in out MD5 (in) = 4b5c11779d7f6d9ada20a308c1b15090 MD5 (out) = 4b5c11779d7f6d9ada20a308c1b15090 %
処理速度を比較した結果は表1の通り。ここではそれぞれコンパイル後に1回コマンドを実行して、その後に5回実行して、それぞれの処理時間の平均値を挙げている。
プログラムのファイル名 | 処理にかかった平均時間(単位=秒) |
---|---|
copy-fgetc-fputc.c | 1.42 |
copy-read-write.c | 1.40 |
copy-mmap.c | 0.30 |
表1 100Mbytesのファイルをコピーする3種類のプログラムがそれぞれ処理を完了させるまでにかかった時間 |
mmap(2)を使ったサンプルは、read(2)/write(2)やfgetc(2)/fputc(2)を使ったものに比べて4〜5倍高速に動作していることが分かる。圧倒的な速度差だ。ここではシンプルに実行速度だけに注目しているが、実はmmap(2)を使うと、メモリの使用量やスケーラビリティなどさまざまな面でメリットを享受できる。
データをユーザーランドにコピーしない
ではなぜmmap(2)を使うと処理速度が上がるのか。これは「カーネルとコピープログラムの間でデータのコピーが発生しないから」と説明できる。mmap(2)を使うと、データをいったんユーザーランドへコピーする操作が発生しないのだ。
copy-read-write.cを例に考えると分かりやすい。システムコールは、特権処理が必要なときに、特権を持ったカーネルに処理を依頼する手段であるということは第1回で説明した。copy-read-write.cではread(2)とwrite(2)の2種類のシステムコールを利用している。これら2種類のシステムコールは、特権モードで動作し、読み書きするデータをいったん配列bにコピーする。あらかじめ用意しておいた100Mbytesの配列だ。このコピー先が問題だ。ファイルinのデータを一度、コピープログラム、つまりユーザーランドで動作するプログラムの配列へコピーしている。これが処理速度を低下させている原因だ。
mmap(2)はファイルをメモリにマッピングしているが、データをプログラム内の変数や配列にコピーする必要がない仕組みになっている。仮想メモリの仕組みを活用して実現している機能であり、ごく自然な方法で実現したメモリマッピング機能である。
なるべくデータのコピーが発生しないように
処理速度を比較したところで、今度はtruss(1)を使って、どのシステムコールが呼ばれているか、それぞれ何回呼ばれているのかを調べてみよう。copy-fgetc-fputc.cではread(2)とwrite(2)がそれぞれ3200回ほど呼ばれていることが分かる。12種類のシステムコールを呼び出しており、呼び出し回数の合計は6435回だ。
syscall seconds calls errors readlink 0.000045816 1 0 lseek 0.000014527 1 0 mmap 0.000146526 8 0 open 0.031803904 5 1 close 0.000033314 2 0 fstat 0.000090725 3 0 write 0.060363421 3200 0 break 0.000016553 1 0 access 0.000025911 1 0 sigprocmask 0.000119290 8 0 munmap 0.000034431 2 0 read 0.114219237 3203 0 ------------- ------- ------- 0.206913655 6435 1
次はcopy-read-write.cを調べてみよう。read(2)とwrite(2)の呼び出し回数はそれぞれ3回と1回だ。read(2)の2回分はコピー以外の処理で呼ばれているものなので、実質的にコピーのためのシステムコール呼び出しは1回ずつだ。10種類のシステムコールを呼び出しており、呼び出し回数の合計は29回。先ほどの例と比べると、システムコールの呼び出し回数が非常に少ないことが分かる。
syscall seconds calls errors lseek 0.000015854 1 0 mmap 0.000121665 6 0 open 0.000121663 5 1 close 0.000035689 2 0 fstat 0.000035339 1 0 write 1.086137903 1 0 access 0.000025492 1 0 sigprocmask 0.000128857 8 0 munmap 0.000018718 1 0 read 0.130443603 3 0 ------------- ------- ------- 1.217084783 29 1
最後にcopy-mmap.cを調べてみる。9種類のシステムコールを呼び出しており、呼び出し回数の合計は29回。mmap(2)を呼び出す回数がほかの2つのサンプルに比べて2回多いこと、シンボリックリンクを読み取るreadlink(2)を呼び出していないことを除けば、copy-read-write.cの傾向によく似ている。
syscall seconds calls errors lseek 0.000006705 1 0 mmap 0.000065791 8 0 open 0.000046864 5 1 close 0.000014039 2 0 fstat 0.000012781 1 0 access 0.000010127 1 0 sigprocmask 0.000087791 8 0 munmap 0.000007054 1 0 read 0.000042673 2 0 ------------- ------- ------- 0.000293825 29 1
つまりデータをファイルからコピープログラムにコピーし、そこからさらにファイルに書き出すという処理が、コンピュータに重い負担を掛けていることが分かる。システムコールの呼び出し回数を減らす、というのは処理性能を引き上げる方策の1つであり、さらにもう1つ、なるべくデータのコピーが発生しないようにするという方策もあるということを覚えておきたい。
mmap(2)は比較的簡単に使えて大きな効果を期待できる魅力的なシステムコールだ。次回以降しばらくは、mmap(2)の使い方を紹介しながら、mmap(2)のように呼び出すだけで良い効果を得られるシステムコールもあるということを示していこうと思う。
追実験時の注意
今回紹介したサンプルプログラムを使って、追実験を実施するときは、以下の2点に注意していただきたい。
まず、仮想環境のゲストOSでサンプルプログラムを実行し、処理時間を比較すると今回示した結果と大きく食い違う可能性がある。仮想環境の仮想ハードディスクは、実際のハードディスクにOSをインストールしたときと比べると異なる動きを見せることがあるからだ。実験は仮想環境でなく、実際のハードウェアにOSをインストールした環境で実施した方が良い。
Solaris 11やFreeBSD 9が搭載しているファイルシステムZFS(Zettabyte File System)が提供する、重複排除機能(dedup)を利用していると、同一データのコピーが一瞬で完了することがある。今回のサンプルプログラムを通して知りたいのは、read(2)とwrite(2)を組み合わせたプログラムと、mmap(2)も利用したプログラムで処理性能がどの程度異なるのかということである。ファイルシステムの機能が働いて、コピーが一瞬で済んでしまっては検証ができない。UFS(Unix File System)上で実験するか、重複排除機能や圧縮機能などを無効にして試すべきだろう。
そして、今回は紹介しなかったが、memcpy(3)という標準ライブラリ関数も試してみると良いだろう。この関数は、指定したメモリ領域のデータをほかの領域にコピーする機能を持つ。copy-mmap.cに、memcpy(3)でメモリ領域のデータをコピーする機能を付け加えると、処理速度が大幅に上がる可能性がある。これはmemcpy(3)が関数内部の処理を工夫して高速化を達成しているのではなく、プロセッサの機能を利用しているためだ。今回はハードウェアになるべく頼らないようにするため、サンプルプログラムではmemcpy(3)は利用しなかったが、実際に試して、使い方とその効果を知っておいた方が良いだろう。
追実験に利用する環境や、OSごとのコマンドの違いなどは連載第2回にまとめてある。こちらも参考にしていただきたい。
著者紹介
BSDコンサルティング株式会社取締役/オングス代表取締役
後藤 大地
@ITへの寄稿、MYCOMジャーナルにおけるニュース執筆のほか、アプリケーション開発やシステム構築、『改訂第二版 FreeBSDビギナーズバイブル』『D言語パーフェクトガイド』『UNIX本格マスター 基礎編〜Linux&FreeBSDを使いこなすための第一歩〜』など著書多数。
Copyright © ITmedia, Inc. All Rights Reserved.