検索
連載

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

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

Share
Tweet
LINE
Hatena
前のページへ |       

システムコールの呼び出し回数を見る

 まずはtruss(1)の実行結果から、システムコールの呼び出し回数を調べてみよう。標準ライブラリ関数であるfgetc(3)とfputc(3)を使ったサンプルプログラムは、システムコールread(2)とwrite(2)をそれぞれ3200回ほど呼び出している。前回の調査と同じ結果だ。fgetc(3)/fputc(3)はシステムコールread(2)/write(2)を呼び出すため、このような結果になる。

truss -c ./copy-fgetc-fputc
syscall                     seconds   calls  errors
readlink                0.000019556       1       0
lseek                   0.000007264       1       0
mmap                    0.000067188       8       0
open                    0.012622972       5       1
close                   0.000015715       2       0
fstat                   0.000038063       3       0
write                   0.055793547    3200       0
break                   0.000008381       1       0
access                  0.000009777       1       0
sigprocmask             0.000073194       8       0
munmap                  0.000008660       1       0
read                    0.092920187    3203       0
                      ------------- ------- -------
                        0.161584504    6434       1

 mmap(2)を使ったサンプルプログラムを調べた結果は次の通りだ。プログラムを起動するために必要なread(2)を呼び出しているだけで、write(2)の呼び出しはない。直前に挙げた、fgetc(3)とfputc(3)を使ったサンプルプログラムを調査した結果と比較するとシステムコール呼び出しの総数がとても少ないところが目を引く。

truss -c ./copy-mmap
syscall                     seconds   calls  errors
lseek                   0.000010267       1       0
mmap                    0.000068167       8       0
open                    0.000046094       5       1
close                   0.000014947       2       0
fstat                   0.000013131       1       0
access                  0.000020184       1       0
sigprocmask             0.000081015       8       0
read                    0.000047423       2       0
                      ------------- ------- -------
                        0.000301228      28       1

 次は、mmap(2)とmemcpy(3)を組み合わせたサンプルプログラムを調べてみる。システムコールの呼び出し回数は上記のmmap(2)のケースとまったく同じだ。memcpy(3)はシステムコールを呼び出していないので、truss(1)の結果には違いは現れない。

truss -c ./copy-mmap2
syscall                     seconds   calls  errors
lseek                   0.000007403       1       0
mmap                    0.000068024       8       0
open                    0.000046374       5       1
close                   0.000014946       2       0
fstat                   0.000013060       1       0
access                  0.000010197       1       0
sigprocmask             0.000079481       8       0
read                    0.000042394       2       0
                      ------------- ------- -------
                        0.000281879      28       1

 最後に、参考までにOSが備えるcp(1)コマンドでファイルをコピーした時の調査結果をご覧いただこう。先に示した3つのサンプルプログラムと比較すると、多くの種類のシステムコールを呼び出している。そして、read(2)とwrite(2)の呼び出し回数は100回ほどとなっている。呼び出しているシステムコールの種類が多いのは、オプションの判断など、先に挙げたサンプルプログラムより多くの処理をしているためだ。read(2)/write(2)の呼び出し回数が少ないのは、呼び出し1回当たりに読み書きするデータのサイズがfgetc(3)/fputc(3)を使う場合よりも大きいからである。

truss -c cp in out
syscall                     seconds   calls  errors
readlink                0.000014108       1       0
lseek                   0.000006705       1       0
mmap                    0.000074799       8       0
open                    0.013091468       5       1
close                   0.000036806       4       0
fstat                   0.000012362       1       0
stat                    0.000037924       3       0
write                   1.028191286     100       0
break                   0.000007194       1       0
access                  0.000010197       1       0
sigaction               0.000009498       1       0
sigprocmask             0.000067046       8       0
munmap                  0.000008241       1       0
read                    0.015894474     103       0
                      ------------- ------- -------
                        1.057462108     238       1

物理メモリサイズやページフォールト回数などを見る

 続いてtime(1)、つまり/usr/bin/timeを使って、プログラムが使用した物理メモリサイズやページフォールトの回数、コンテキストスイッチの回数などを見てみよう。最初に、fgetc(3)とfputc(3)を使ったサンプルプログラムを調べた結果をお見せしよう。

/usr/bin/time -lph ./copy-fgetc-fputc
real 1.15
user 1.06
sys 0.08
      1020  maximum resident set size
         4  average shared memory size
      2020  average unshared data size
       128  average unshared stack size
       115  page reclaims
         0  page faults
         0  swaps
         0  block input operations
       800  block output operations
         0  messages sent
         0  messages received
         0  signals received
         7  voluntary context switches
        72  involuntary context switches

 17種類もの項目を調査した結果が出てきたが、今回はすべての意味を理解する必要はない。「real」「maximum resident set size」「page reclaims」「block output operations」「voluntary context switches」「involuntary context switches」の6項目に注目する。

 それぞれの項目の意味するところを説明しよう。realはプログラムの実行にかかった時間を表す。そして、maximum resident set sizeは、プログラムが利用した物理メモリの最大値を表す(単位はKbytes)。page reclaimsは入出力の発生を伴わないページフォールトが発生した回数を指す。block output operationsはファイルシステムがデータを出力した回数を表す。

 voluntary context switchesは、割り当てられたプロセッサ時間を消費する前に、自発的にコンテキストスイッチを実行した回数を指し、involuntary context switchesは時間切れや、ほかの優先度の高いプロセスの実行のために強制的にコンテキストスイッチが発生した回数を表している。

 ライブラリ関数fgetc(3)/fputc(3)を使ったサンプルプログラムの調査結果を見ると、read(2)/write(2)を頻繁に呼び出しているためか、block output operationsの値がほかのサンプルプログラムよりも多い。一方、maximum resident set sizeの値は小さく、メモリの使用量が少ないことが見て取れる。involuntary context switchesが多いというところにも注目だ。時間いっぱいまで処理を続けた結果、コンテキストスイッチが発生してしまったということが多いようだ。

 次はmmap(2)を使ったプログラムの調査結果を見ていく。

/usr/bin/time -lph ./copy-mmap
real 0.30
user 0.25
sys 0.04
    201012  maximum resident set size
         4  average shared memory size
         4  average unshared data size
       131  average unshared stack size
     30822  page reclaims
         0  page faults
         0  swaps
         0  block input operations
         0  block output operations
         0  messages sent
         0  messages received
         0  signals received
         2  voluntary context switches
         8  involuntary context switches

 処理速度がfgetc(3)/fputc(3)よりも優れているのは前回調べた通りだが、maximum resident set sizeの値が201012Kbytes(193Mbytesほど)とかなり大きい点に注目したい。システムコールの呼び出し回数は少ないものの、物理メモリ使用量が大きいということだ。入出力を伴わないページフォールトの発生回数が30822回と多くなっているところも注目だ。入出力が発生していないのは、メモリの割り当て方が関係している。再割り当て待ちのページキャッシュから、ページフレームを再利用しているのだ。mmap(2)はページフォールトとデマンドページングを利用して動作するシステムコールなので、この観測結果はいわば当然の結果なのだが、mmap(2)の動作の特性がread(2)/write(2)とはまるで異なるものであることがよく分かるだろう。

 mmap(2)とmemcpy(3)を組み合わせたサンプルプログラムの調査結果は以下の通りだ。

/usr/bin/time -lph ./copy-mmap2
real 0.06
user 0.02
sys 0.03
    197480  maximum resident set size
         4  average shared memory size
         4  average unshared data size
       146  average unshared stack size
     30822  page reclaims
         0  page faults
         0  swaps
         0  block input operations
         0  block output operations
         0  messages sent
         0  messages received
         0  signals received
         2  voluntary context switches
         1  involuntary context switches

 まず、ほかのサンプルプログラムと比較すると、処理速度が素晴らしく速い。通常のmmap(2)を利用したサンプルプログラムでは、コピーに0.3秒かかっているが、mmap(2)とmemcpy(3)を組み合わせたサンプルプログラムでは0.06秒しかかかっていない。truss(1)による調査結果と同じように、基本的な動きはmmap(2)のみを使ったサンプルプログラムによく似ているが、実際の処理速度は5倍ほど高速になっている。先に説明したように、コンパイラがコンパイル時にmemcpy(3)の処理を最適なバイナリコードに変換するからだ。memcpy(3)はシステムコールではなく、標準ライブラリ関数だが、大きな効果を期待できる関数である。

 最後に、ユーザーランドにインストールされているcp(1)コマンドを使ったファイルコピーについて調査した結果をお見せする。

/usr/bin/time -lph cp in out
real 1.11
user 0.00
sys 0.07
      2076  maximum resident set size
        14  average shared memory size
      1828  average unshared data size
       115  average unshared stack size
       368  page reclaims
         0  page faults
         0  swaps
         0  block input operations
       800  block output operations
         0  messages sent
         0  messages received
         0  signals received
       662  voluntary context switches
         7  involuntary context switches

 fgetc(3)/fputs(3)を使ったサンプルと似たような結果が得られるが、自発的なコンテキストスイッチの回数(voluntary context switches)が多い。自発的なコンテキストスイッチが発生する理由はいくつもあるので、ここでは説明しきれないが、fgetc(3)/fputs(3)で作成したシンプルなプログラムとは動き方に違いがあるということがよく分かる。

 mmap(2)とmemcpy(3)を組み合わせたプログラムを作れば、コピー処理は高速になるが、実はライブラリの互換性などを考慮すると、どんな状況でも使える手段とは言えない。そして、mmap(2)も処理は高速だが、メモリを消費するという欠点がある。単純な考えで使うべきものではない。

 一方、処理速度の面ではいいところがなかったcp(1)コマンドだが、truss(1)の調査結果が示すように、mmap(2)を呼び出しているなど、高速化のための工夫を凝らしているところが見える。

さまざまな面を見て判断することが大切

 前回はmmap(2)システムコールを使うことによって、処理速度が上がるという点に焦点を当てて解説した。コピープログラムとカーネルの間でデータのコピーが発生しない分、mmap(2)は高速に動作する。今回は物理メモリの使用量やページフォールトの回数、コンテキストスイッチの切り替え回数などにも調査の対象を広げながら、システムコールそれぞれの特性を追ってみた。

 処理速度の面ではmmap(2)はとても魅力的だが、物理メモリを多く消費するなど、考慮すべき点もあり、単純にすべてのシーンに適用できるわけではないことが見えてきたかと思う。大切なのはそれぞれのシステムコールが実際にどのように動き、どのような特徴を持っているかを把握するということである。

 ちなみに、/usr/bin/timeの実行結果はgetrusage(2)システムコールで取得できるrusage構造体のデータを出力しているものなので、プログラムからデータを取得して標準出力へ出力させることもできる。ここではサンプルのソースコードをシンプルにしたいので、/usr/bin/timeを使って得たデータを掲載した。

 なお、Solaris 11 11/11ではgetrusageはシステムコールではなく標準ライブラリ関数となっている。関数をシステムコールとして実装するか、標準ライブラリ関数として実装するかは、それぞれのOSの実装によるのだ。

実際に試すときは

 本連載での各種検証作業には、PC-BSD 9.0/FreeBSD 9.0を利用している。今回の解説内容を自身でも検証してみたいという方は、利用する環境の違いに注意してほしい。環境が変わることによって発生する可能性がある事象や、OSごとのコマンドの使い方の違いなどは本連載第2回の末尾にまとめてあるので、参考にしていただきたい。

著者紹介

BSDコンサルティング株式会社取締役/オングス代表取締役

後藤 大地

@ITへの寄稿、MYCOMジャーナルにおけるニュース執筆のほか、アプリケーション開発やシステム構築、『改訂第二版 FreeBSDビギナーズバイブル』『D言語パーフェクトガイド』『UNIX本格マスター 基礎編〜Linux&FreeBSDを使いこなすための第一歩〜』など著書多数。



前のページへ |       

Copyright © ITmedia, Inc. All Rights Reserved.

ページトップに戻る