ホーム » 「printf」タグがついた投稿
タグアーカイブ: printf
プログラムの処理時間の測り方
今回の課題レポートでは、テスト素点が良くレポート提出でも加点が少ないと思われる人でも、まじめに取り組んだレポート提出が多かった。この中で、興味深いレポートで、2分探索木の検索が偏っていたらO(N),バランスが良ければO(log N)の検証をしているものがあった。ただ、計測方法がちょっと残念だったので解説。
処理時間の計測方法
速度の検証をするにしてもデータ数も少なく、検索は一瞬で終わってしまうので、以下のような 1000 回ループで時間を測定していた。1000回といったループで計測するのは正しいアプローチ。
for( int i = 0 ; i < 1000 ; i++ ) { if ( find( top , key ) ) printf( "みつかった" ) ; else printf( "みつからない" ) ; }
ただ、この方法では、find() の処理時間以上に、printf() の処理時間の方が問題となる。
printf の処理時間
printf は、ただでさえも、第一引数の中にある “%” フォーマットの分析をするという面倒な処理をしているので、かなり複雑な処理をしている関数。メモリ容量の少ない組込み系のシステムを構築する時は、%フォーマットの分析のコードも大きくなるので、使うのを避けることも多い。
また、printf は、%フォーマットの解析に加え、出力バッファリングなどを経て、最終的に文字を標準出力に出力する。標準出力への出力が発生すると、OSのシステムコールを呼び出し、結果を表示するターミナルプログラムは、画面への文字の出力や、画面からはみ出た文字のスクロールアップなどの処理を行う。このため、find() 関数の処理時間以上に手間がかかっている。このため、精度の低い結果となってしまっている。
つまり、自分の関数の処理時間を計測したいなら、余計な時間がかかる printf などの入出力処理は時間計測に含めないこと。
time コマンド
プログラムの処理時間を測る場合には、測定対象のプログラムの処理時間の他にも、最近のOSではマルチタスクだからこそ、他の並列処理にとられている時間、OSでマルチタスクの切替に係る時間なども含まれてしまう。
このため Linux 環境でプロセスの処理時間を測定する場合には、time コマンドを用いる。
((( ちょっと時間のかかりそうな /usr/lib 配下のファイルの一覧出力 ))) $ find /usr/lib -type f -print (略) ((( time コマンドは、time の後ろに計測したい処理を書く ))) $ time find /usr/lib -type f -print (略) 0.00s user 0.33s system 26% cpu 1.242 total ((( 比較のために出力を /dev/null に捨てて実行 ))) $ time find /usr/lib -type f -print > /dev/null find /usr/lib -type f -print > /dev/null 0.00s user 0.25s system 87% cpu 0.287 total
この例では、find 自体の処理時間 user 時間 0秒、find が 出力命令のようなシステムコールを実行したことによる OS の処理時間 system 時間 0.33 秒、出力なども含めて見かけ上の本当の処理時間が 1.242 秒というのがわかる。
この結果を見ても、如何に出力処理が遅いのかが分かる。
だからこそ、自分のプログラムの処理時間として使う場合は、user 時間の部分を使うこと。
プロファイラによる解析
プログラムの処理が遅い場合の原因を究明する場合には、プロファイラというプログラムを用いることが多い。
例えば gcc などのコンパイラのためのプロファイラの gprof では、プログラムの処理に一定時間で割り込みをかけ、そのタイミング毎にどの関数の処理中だったのかを調べることで、全体の処理時間の何パーセントをその関数の処理をしていた…ということを計測できる。(統計的プロファイラ)
((( 測定対象のプログラムにプロファイラ用の情報を埋め込んでコンパイル ))) $ gcc -pg foobar.c ((( プログラムを実行すると gmon.out という統計情報が出力される ))) $ ./a.out ((( gprof で統計結果の確認 ))) $ gprof ./a.out gmon.out
といっても、割り込みをかけて計測しているので、プロファイラの結果は精度の高いデータとは言えない。
C言語での入出力処理のおさらい
テストのプログラム作成の問題で、入力処理の書き方が適切でないものが多いので、基本とテクニックの解説。
scanf()の使い方
// scanf( "フォーマット" , 引数... ) ; // データの型とフォーマット // int %d (10進数として入力) - Digit // %o (8進数として入力) - Octal digit // %x (16進数として入力) - heXa-decimal digit // - short int %hd - half-size digit // - long int %ld - long-size digit // // float %f // double %lf - long-size float // // char[] %s // char %c // 基本 scanf() はポインタ渡し。文字列(char配列)は配列の先頭アドレスを渡す int x ; scanf( "%d" , &x ) ; // ポインタを渡し、xの場所に書き込んでもらう。 char str[ 10 ] ; scanf( "%s" , str ) ; // strは配列の先頭の場所。 & は不要。
通常のscanfの%d,%sなどは、データ区切りは空白や改行となる。
%s で “tohru saitoh”といった空白の入った文字列を入力するには、一工夫が必要。
printf()の使い方
// printf( "フォーマット" , 引数... ) ; // データの型とフォーマット // 基本は、scanf() と同じ。 // float 型の出力 // %f 固定小数点表示 1.23 のような出力 // %e 指数表示 1.23e+10 のような出力 // %g 固定小数点%fと指数表示%eのどちらか // 桁数指定 // %5d - 必ず5桁で出力 " 123" // %05d - 5桁の空白部は0で埋める "00123" // %5.2f - 全体5桁、小数点以下2桁 " 1.23" // %5s - 文字列を5桁で出力 " abc" // 左寄せ // %-5s - 文字列を左寄せ5桁で出力"abc "
scanf(),printf()は成功した項目数を返す
scanf(),printf() は、返り値を使わないように思うけど、実際は入力に成功した項目件数、出力に成功した項目件数を返す。
int x ; char str[ 10 ] ; int ans ; ans = scanf( "%d%s" , &x , str ) ; printf( "%d¥n" , ans ) ; // 入力に成功すれば2。 ans = printf( "%d%s¥n" , x , str ) ; printf( "%d¥n" , ans ) ; // 出力に成功すれば2。
特に、ファイルからの入力であれば、途中でデータがなくなって、これ以上データが入力できない時には、scanfの返り値を用いる。
char name[ 100 ] ; int age ; while( scanf( "%s%d" , name , &age ) == 2 ) { printf( "%s %d¥n" , name , age ) ; }
Microsoft の scanf_s() は安全な入力関数
C言語の標準関数 scanf() の %s では、バッファオーバーフローを検出できない。Microsoft の Visual Studio の C言語では、こういった危険な scanf(“%s”) は危険なので、scanf_s() を使うようになっている。ただし、他のC言語では使えない場合が多いので要注意。
char name[ 10 ] ; scanf( "%s" , name ) ; // バッファオーバーフロー scanf_s( "%s" , name , sizeof( name ) ) ; // バッファオーバーフローの心配がない
fget()とsscanf()を使った安全な入力
C言語では、1行のデータを読む場合には、gets() 関数がある。しかし、配列サイズを指定できないので、バッファオーバーフローの危険がある。一方、ファイルからの入力関数 fgets() は、入力するデータサイズを指定できるため、fgets を使うべき。
char buff[ 1000 ] ; gets( buff ) ; // バッファオーバーフローの危険性 fgets( buff , sizeof( buff ) , stdin ) ; // 入力に失敗すると NULLを返す
一方、入力した文字列のデータから、scanf() のようにデータを抽出する、sscanf() という関数がある。
char string[] = "123 tohru 1.23" ; int x ; char str[ 10 ] ; double y ; sscanf( string , "%d%s%lf" , &x , str , &y ) ; // x = 123 , str = "tohru" , y = 1.23 // scanfと同様に成功した項目数3が返る。
これらを踏まえ、1行に名前と年齢のデータが記録されていて、データが入力できるだけ繰り返す処理を fgets + sscanf で書くと以下のようになる。
char buff[ 1000 ] ; char name[ 1000 ] ; int age ; while( fgets( buff , sizeof( buff ) , stdin ) != NULL ) { if ( sscanf( buff , "%s%d" , name , &age ) == 2 ) { // buffには必ず1000文字以下なので、nameが1000文字を超えることはない printf( "%s %d¥n" , name , age ) ; } }
なお、このプログラムを動かす場合、これ以上データがない場合には、Windowsであれば “Ctrl-Z” を入力。unixやmacOSであれば”Ctrl-D”を入力すること。
その他の気づいた点
文末の「;」を忘れないで
JavaScript では、単純式の末尾の「;」は、忘れてもそれなりに正しく動く。しかし、C言語では「;」を忘れると、文法エラーになるので要注意。
1: struct A { 2: : 3: } ; ←この行のセミコロンを忘れると、5行目で文法エラーの表示となる。 4: 5: for( ... ) { ←この行を見ても文法エラーの原因はわからない。3行目が原因。 6: : 7: }
局所変数やヒープメモリにはゴミデータが入っている
void foo() { int sum ; ←初期化忘れ int array[ 3 ] = { 11 , 22 , 33 } ; for( int i = 0 ; i < 3 ; i++ ) { sum += array[ i ] ; } }
局所変数やヒープメモリは、関数に入って確保やmallocで確保されるメモリ領域だけど、このメモリ領域には以前の処理で使われていたデータが残っている場合がある。こういった初期化されていないメモリ領域は、悪意を持ったプログラムがデータを盗むために使われる場合もある。必要に応じてちゃんと初期化が必要。
また、大域変数(グローバル変数)は、C言語では 0 で初期化される。(ポインタなら NULL が入っている。)