1|2やはりシステムコールを利用した方が速いのか?では先に挙げた3種類のサンプルプログラムを調べて、実際にシステムコールを呼び出している回数と、システムコールごとの処理時間を調べてみる。ここではtruss(1)を使うが、OSによってはこれがstraceや、dtrussになる。指定するオプションも変わることがあるので、末尾の説明を参考にしてほしい。最初に、システムコールを使わずに作ったサンプルプログラムを調べてみよう。truss(1)を実行すると、以下のようになる。2%12種類のシステムコールを合計677回呼び出していることが分かる。read(2)は320回、write(2)は323回呼び出されている。システムコールの処理に限ると、かかった時間は0.03秒ほどということになる。この値は環境によって変わる。次は、システムコールを使って1byteごとにデータコピーしたサンプルを調べてみよう。その結果は下記の通りだ。1%システムコールの合計呼び出し回数を見ると、2097万1550回にもなっている。システムコールの処理にかかった時間の合計は457秒を超えている。read(2)とwrite(1)は、それぞれ1千万回以上呼び出されている。10Mbytesはおよそ1千万bytesになる。丁寧にファイルサイズの分だけシステムコールを呼び出したことが分かる。最後に、10Mbytes分の配列を用意して、一度にコピーしたサンプルプログラムを調べてみよう。1%システムコールの合計呼び出し回数はわずか32回。システムコールの処理にかかった時間の合計も、約0.02秒と極めて短い。write(2)は1回、read(2)は4回呼び出されている。この連載で使用している環境では、プログラム実行前に必ずread(2)を3回呼び出す。データをコピーするためのread(2)とwrite(2)はそれぞれ1回だけ呼び出されたことになる。サンプルコードから予想できる動きだ。速ければそれでよいのか?以上、3種類のサンプルプログラムを比較すると、システムコールを使って、1byteずつデータをコピーするようにした2番目のプログラムは、システムコールの呼び出し回数がほかのプログラムよりもかなり多く、プログラム実行時間も長いという結果になった。システムコールを直接呼ぶプログラムで、その使い方を間違えると、あっという間にプログラムが役に立たないものになってしまうという例だ。システムコールの呼び出し回数が最も少なく、プログラムの処理時間が最も短いのは、10Mbytesのデータをまとめてコピーするようにしたプログラムということになった。これもシステムコールを利用したものだ。このサンプルが3種類の中で最も優れたものなのかと考えると、そう簡単には結論は出ない。それならば、巨大な配列(バッファ)を用意すればよいだけの話ではないかということになる。今度は3種類のサンプルプログラムをそれぞれ実行し、メモリ消費量を比べてみたい。先に挙げたサンプルコードそのままでは、プログラムがあっという間に終了してしまうので、コードを追加しなければならない。最初に挙げた、標準ライブラリの関数で作成したサンプルプログラムなら、最後の閉じカッコの前に以下のコードを挿入する。fgetc(stdin);残る2種類、つまりシステムコールを直接呼び出したサンプルプログラムの場合は、最後の閉じカッコの前に以下のコードを挿入してほしい。1);こうしておけば、プログラムの処理が終わっても、[Enter]キーを入力するまでプログラムは待ち状態となる。この状態になったら、次のようにps(1)コマンドを使ってプログラムが消費しているメモリ量を調べてみよう。RSSの項目が実際に使っているメモリ量だ。表示はKbyte単位になっている。標準ライブラリの関数を使ったサンプルでメモリ消費量を調べてみる。./copy-fgetc-fputc%1120Kbytes(およそ1Mbytes)のメモリを消費していることが分かる。次は、システムコールを使って1byteずつデータをコピーするサンプルだ。このサンプルは実行にかなり時間がかかったが、メモリ消費量は828Kbytesと少ない。最後に、10Mbytesをまとめて転送するサンプルだ。./copy-read-write2%処理速度では、このサンプルが最も優れていたが、メモリ消費量を見ると、10Mbytes以上になっていることが分かる。当然と言えば当然だが、10Mbytesもの配列を用意したプログラムだ。それだけメモリ消費量も大きい。ここで注目したいのが、標準ライブラリの関数を使ったサンプルだ。処理速度が速い割には、メモリ使用量が少ない。fgetc(3)やfputc(3)を使うと、FILE構造体経由でデータを読み書きする段階でバッファ処理が入るため処理が高速になるのだ。このため、巨大なバッファを持たなくても、それなりに速く処理を済ませてくれるのだ。このような特性の違いが、システムコールと標準ライブラリの違いをよく表している。システムコールは使いよう今回は標準ライブラリの関数であるfgetc(3)とfputc(3)、そしてシステムコールであるread(2)とwrite(2)を例に挙げて動作の違いを追ってみた。read(2)とwrite(2)は、その名前の通りデータを読み書きする機能を持つが、fgetc(3)やfputc(3)を使うと、途中でバッファ処理が入り、メモリ消費量を抑えながらそれなりに速く処理が済むようにしてくれている。システムコールの動きを熟知しており、やりたいこともその利用シーンもはっきりしていれば、システムコールを便利に活用できるが、場面を選ばずに利用するには難がある。システムコールを使いながら、自分でバッファ処理を実装し、ロックも実装し、という具合に自分で必要な処理を記述していくと、最終的にはFILE*、fgetc(3)、fputc(3)と同じものを自分で作るということになる。それなら最初からある標準ライブラリを使った方が便利だ。さらなる高速化を狙って自身で関数を実装してもほとんど意味はない。バッファ処理を活用しているfgetc(2)とfputc(2)を使って、その上で自身でバッファ処理を記述するようなことをしても、さらなる高速化を期待できないばかりか、ソースコードが必要以上に複雑になる上に、処理速度が遅くなる。必要のない機能を自身で実装してみても、実は性能が落ちるなど、よい結果にならないということは、上達してきたプログラマが陥りがちな問題の1つだ。11では、dd(1)の引数やtruss(1)と同じ機能を持つコマンドの名前が表1のように変わるので参考にしてほしい。OSで使えるddコマンドの引数と、trussコマンドと同じ機能を持つコマンドの名前なお、ファイルコピーの実験をするときは、ファイルシステムが重複排除の機能や圧縮機能を持っていることがあるので注意してほしい。System)は重複排除機能も圧縮機能も備えている。このような場合は、実験をしてみても想定と異なる結果になることがある。ファイルシステムや中間レイヤが提供しているキャッシュ機能などが影響して、1回目と2回目以降の実行結果が大きく異なることも多い。この点をよく注意して、何度か実行して変化を見た方が良い。メモリ使用量はハードウェア環境やOSによって異なるが、この値が小さい方が優れているというものではない。大きいと良くないということでもない。OSとしてはメモリを余らせるのではなく、使い切った方が性能が期待できるので、メモリ空間に余りがある限り使い切ろうとするカーネルもある。反対の戦略を取るものもある。カーネルがメモリをサブシステムキャッシュに使いながら、ユーザーにそのことを知らせないものもある。OSの動き方は種類と場合によってケースバイケースなのだ。メールマガジンをご購読ください。申し込みページへITmediaはアイティメディア株式会社の登録商標です。