9.計算時間を測る


[ next | prev | index]
目次

[目次]


この実験では、計算機が計算に要する時間を計リます。考察では、なぜそのような時間になるのかを考えてください。

ところでC言語が初めての人に対応する為に9-1から順番に説明を用意しましたが、不要な人は9-4までを飛ばして進めてください。

9-1.HelloWorld

Cを学んだ人は9-2に飛んでください

 C言語によるプログラムは変数と関数の集まりです。最初に実行される関数は名前がmainに決められています。そこで最初のプログラム例は下記の様にしました。

#include <stdio.h>
 
int main(void){
    printf("Hello World\n");
    return 0;
}

 実際に作る前に、まずは上記の定番プログラムの意味を少々

(i) #include <stdio.h>

 名前が「stdio.h」のファイルの中身をここに挿入する。

 頭に#のついているコマンドはプリプロセッサの命令でC言語のコンパイラで翻訳する前にプリプロセッサでテキストを編集することを指示しています。「stdio.h」の中にはprintf等の良く使う関数の頭部や変数があらかじめ書いてあります。

(ii)  int main(void){
     ..関数の中身..;
     return 0;
   }

 Cプログラムの関数の頭部は必ず

 返値の型名 関数名(引数型 引数名, 引数型 引数名, ...)

の形をしています。このmain関数では引数が無いことを示すためvoidと書かれています。標準ではmain関数は整数値を返すことになっているためプログラムの最後にreturn文で0を返しています。Cでは返値を返さない場合も関数として一緒にあつかうので、pascalの関数と手続きの様な区別はありません。そのためfunction/proceduerといった記述は存在しません。返り値がない場合はvoid main(void)の様になります。

関数かどうかは名前の後に「()」が在るかどうかで区別されます。

(iii) printf("Hello World\n");

 これはpascalのwrite文に相当する関数で「"」で囲んだ文字列定数を標準出力にかき出します。writelnの様に改行する為には、改行を表す文字"\n"を文字列の改行したい位置に加えます。

(iv) 複文や区切りを表す記号{};

pascalとCでの対応表 

begin

{

end

}

; ※endの直前なら省略できる

;  ※ただし } の前でも必要

[目次]


9-2.まずは動かしてみよう

(1)実行環境
 ここではコンパイラgccを用いてコンパイルして実行してみましょう。 gccはGNUプロジェクトの一部として無料で配布されているCコンパイラです。gccを使用するために、まずCygwinを起動します。デスクトップにあるCygwinのアイコンをダブルクリックします。


(2)ソースファイルの作成
次に、マイコンピュータからZドライブを開き、Z:\の直下に実験III用の作業フォルダを作成します。例では、作業フォルダ「z:\exp3」を作成しています。次に、フォルダの中に新規テキストファイルを作成し、「p1.c」という名前に変更します。

次に、今作成した「p1.c」をテキストエディタ(メモ帳など)で開き、以下のコードを入力します。

これを上書き保存すれば、ソースファイルの完成です。

(3)コンパイルと実行
次に、コンパイルと実行を行います。まず、Cygwinのウィンドウに移り、以下のコマンドを実行してください。
$ cd
$ cd exp3
$ ls
「ls」はフォルダ内のファイルを表示するコマンドです。

次に、コンパイルを行い、実行ファイルを作成します。
$ gcc -o p1 p1.c
プログラムが正しければ、プロンプトが表示されます。
再びlsコマンドを実行してみましょう。

「p1.c」の他にもう一つ、「p1.exe」というファイルが作成されています。これが実行ファイルとなります。
最後に、
$ ./p1.exe
これで実行ファイルを起動できます。

 

プログラミングのHowToばかり書きました。つぎに、簡単ですがプログラミングの実行までの手順を説明します。実験が終わるまでにはこの当たりまで理解してもらいたいです。

[目次]


9-3.プログラム作成と実行の手順

Cを学んだ人は9-4に飛んでください

 プログラムは命令を並べた物です。計算機ではCPUがメモリー上の命令を順番に読みながら実行することでプログラムが動きます。CPUは機械語しか理解しませんからメモリー上に置くのは機械語プログラムでなければ動きません。

 機械語には命令をあらわす小数の単語しかないので人間がやらせたい処理をこれで作るのは困難です。従って、人間が機械語でプログラムを書くことは普通しません。多くの場合、機械語プログラムは高級言語と呼ばれる人工言語で書かかれた文書をコンパイラと呼ばれる翻訳ソフトで機械語に翻訳して作られています。

※機械語の単語はCPUの電子回路で解釈し実行されますが、単語の数を増やすと回路が複雑で大規模なものとなってしまいます。この結果、LSIの製産がより難しくなり、しかもCPUの処理時間が長くなったり、電力消費が増えたりします。現在は単語を最小限にしてCPUは少しでも高速で低消費電力にする方向へ向かっています。

(1) ソースファイルの作成と保存

 プログラマはまずエディタでC言語で書かれたプログラムの文書を作り保存します。これをソースファイルと呼びます。このファイル名は拡張子を「.c」とする決まりです。

※さて高級言語でプログラムを作るといっても一から作っていては大きなプログラムは作れません。そこで良く使う処理はプログラム部品としてライブラリの形で用意されています。部品の名前とデータの交換法が分かっていれば中身は分からなくても使えます。C言語では部品のユーザーズマニュアルに相当する文書がヘッダーファイルとして用意されています。標準的なヘッダーファイルは拡張子が「.h」となつていて、「include」等の名前が付いたフォルダにまとめられています。また、部品本体のファイルも「lib」等の名前が付いたフォルダにまとめられています。

(2) コンパイル

 C言語では翻訳の前にプリプロセッサと呼ばれるソフトで先のソースファイルの編集を行います。主な作業はヘッダーファイルの中身をソースファイルに挿入する作業です。これでコンパイラが翻訳を行うときにライブラリーの部品を使うための情報がソースファイルに追加されます。

 プリプロセッサに続いてコンパイラで機械語への翻訳が行われます。1個のソースファイルが1個の機械語ファイルに翻訳され保存されます。C言語では1個のプログラムを複数のソースファイルに分けて作ることができますが、これは複数の人間が分担してプログラムを作るために必要な機能です。この場合、コンパイラが作る機械語ファイルは一般にはプログラムの一部分でしかないのでこのままではプログラムとしては実行できません。

 

(3) リンク

プログラムの断片を繋いで実行可能なプログラムとする為の作業がリンクで、この処理を行うソフトがリンカーです。コンパイラが出力した機械語ファイルや使われているライブラリの機械語ファイルを集めて繋ぎ、実行可能ファイルを出力します。このような処理を静的リンクといいます。多くの実行可能ファイルに同じ機械語ファイルが重複して含まれることになるので無駄が多いと言えますが、ライブラリと切りはなして実行可能ファイル単体で実行できる利点があります。Windowsの拡張子「.exe」のファイルがこのような実行可能ファイルです。

(4) 実行

 実行可能ファイルをHDD等からメモリー上へコピーしCPUにプログラムを実行させるのはオペレーティングシステムの仕事です。ここで実行中に必要になった時点でライブラリから機械語ファイルを読むことも可能です。このような処理を動的リンクといいます。実行してみないと使うかどうか分からないような部品のリンクで効果的です。実行可能ファイルを小さくする目的でも有効です。欠点は動的リンクライブラリが実行時に常に必要なことです。Windowsの拡張子「.dll」のファイルがこのような動的リンクライブラリのファイルです。

(5) 統合環境

 さて、上記の処理をふまえてプログラムを作るわけですが、これはなかなかたいへんです。そこで最近のプログラム言語には上記の作業をまとめて行う環境が用意されています。皆さんは初心者ではないはずですが、これを使えば初心者でもある程度のプログラミングが行なえると言われています。

 とは言うものの、実際はやはり上記の手順が分からないと、この統合環境はとても使えません。(^^); 長々と書いてきたのはそのためです。結局のところ統合環境は大きなプログラムをチームで開発するプロの為の環境です。

(6) まとめ

統合環境では

  1. プロジェクトファイルを作りプログラムの構成を決める
  2. エディタでソースプログラムを書いて保存
  3. 実行でコンパイルとリンクを行い実行可能ファイルを作り、メモリーに読込んで実行する
    コンパイル時エラーがあればソースの編集へ
    実行時エラーがあればデバッガーで虫とりへ

となりますが基本は下図の手順です

 

[目次]


9-4.計算時間を測る(実験課題)

いろいろな計算の処理時間を測定して記録し、計算ごとの処理時間をレポートで報告して下さい。
 なお、ループ回数については、5年前のPC(3.2GHz Pentium4)をもとに見積もっています。測定値が0秒となった場合は、各自適当な回数に修正し、実験を行ってください。

(1) 時間を測る方法

ヘッダーファイルtime.hに記述されたclock()関数は時計の時刻を読み出す関数です。これを用いて時間を測ることにします。Windowsではclock()は1000分の一秒単位の符号無し整数型(unsigned long)の値を返します。下記の例ではlongの足し算とdouble(倍精度実数型)の足し算を1億回行って処理時間を見積もります。ループ自体も時間を食うので、ループ回数は10分の1の1000万回として1回のループの中で10回の計算をまとめて行うことにしました。中身が空のループも含めて3種類で時間を計測しています。また/**/で挟まれた部分はコメントでコンパイラは翻訳にさいしてこの部分を無視します。

注意:clock()が返す値の単位は決まっていません。実際、Macでは60分の1秒単位でした。

#include <stdio.h>
#include <time.h>
        
int main(void){
        unsigned long t0,t1,i;  
        long a; 
        double x;
        a=0;x=0;
                
        t0=clock();
        for(i=0;i<10000000;i++){/*C言語ではi=i+1をi++のように短縮して書ける*/
                ;
        }
        t1=clock();
        printf("      ; %f[s]\n", (t1-t0)/1000.0 );
        
        t0=clock();
        for(i=0;i<10000000;i++){
                a+=1; /*1 C言語ではa=a+1をこのように短縮して書くこともできる*/
                a+=1; /*2*/
                a+=1; /*3*/
                a+=1; /*4*/
                a+=1; /*5*/
                a+=1; /*6*/
                a+=1; /*7*/
                a+=1; /*8*/
                a+=1; /*9*/
                a+=1; /*10*/
        }
        t1=clock();
        printf("a+=1  ; %f[s]\n", (t1-t0)/1000.0 );
        
        t0=clock();
        for(i=0;i<10000000;i++){
                x+=1.0;/*1*/
                x+=1.0;/*2*/
                x+=1.0;/*3*/
                x+=1.0;/*4*/
                x+=1.0;/*5*/
                x+=1.0;/*6*/
                x+=1.0;/*7*/
                x+=1.0;/*8*/
                x+=1.0;/*9*/
                x+=1.0;/*10*/
        }
        t1=clock();
        printf("x+=1.0; %f[s]\n", (t1-t0)/1000.0 );
        return 0;
}

実行結果を見るとループの部分の計算時間が無視できないことが分かります。計算機によって処理時間は違います。みなさんの計算機では違う結果になると思います。以下は私の計算機の結果です。

(i)C言語の変数型(主なもの)

変数の定義がpascalとは逆で

型名 変数名;

となります。以下に主な型名を書いておきます

1文字

char

符号付き整数

signed char, short, int, long

符号無し整数

unsigned char,unsigned short, unsigned int, unsigned long

実数

float, double

型の無い場合

void

(ii)代入

値の代入はpascalでは演算子「:=」でしたがcではただの「=」です。そのかわり比較演算子はpascalの「=」からcでは「==」にかわります。

pascal言語

  C言語  

:=

=

=

==

<>

!=

and

&&

or

||

not

!

(iii)forループ

Cのforループの形は以下の様になります

for(初期化;繰り返しの条件;カウンタの更新){ループ内の処理}

ここで実行順は下記のフローチャートの様になります

Cのfor文はwhile文で

初期化;while(繰り返しの条件){ループ内の処理;カウンタの更新}

と書くのと全く同じです。上記のプログラムではカウンタの更新でi++と書いてありますがこれはi=i+1の短縮表現です。

(4)printfの出力フォーマット

関数printfの最初の引き数は出力フォーマットを示す文字列です。ここに普通の文字列を入れれば、そのまま書き出されますが、「%dl」とか「%f」を挟むとその部分が2番目以降の引き数の値で置き換えられる。ここの例では

printf("x+=1.0; %f[s]\n", (t1-t0)/1000.0 );

%fが2番目の引き数(t1-t0)/1000.0の値で置き換えられています。

(2) 四則演算

先ほどの例を基にlongとdoubleの四則演算の処理時間を測定し報告する。

(3) 初等関数

sin、tan、exp、logの関数はヘッダーファイルmath.hに宣言されている。これらの関数の処理時間を測定し報告する。

四則演算にくらべると桁違いに時間がかかる。

#include <stdio.h>
#include <time.h>
#include <math.h>
                
int main(void){
        unsigned long t0,t1,i;  
        double x;
        x=0;
                
        t0=clock();
        for(i=0;i<10000000;i++){
                x+=exp(1.1);/*1*/
                x+=exp(1.1);/*2*/
                x+=exp(1.1);/*3*/
                x+=exp(1.1);/*4*/
                x+=exp(1.1);/*5*/
                x+=exp(1.1);/*6*/
                x+=exp(1.1);/*7*/
                x+=exp(1.1);/*8*/
                x+=exp(1.1);/*9*/
                x+=exp(1.1);/*10*/
        }
        t1=clock();
        printf("x+=exp(1.1); %f[s]\n", (t1-t0)/1000.0 );
        return 0;
}

(4) 関数の呼び出し

関数を呼び出す処理は引き数の受け渡しやプログラムの実行位置の制御などで時間を喰う。この時間を測定し報告する。

#include <stdio.h>
#include <time.h>
        
long longFn(long a){
        return a+1;
}
                
int main(void){
        unsigned long t0,t1,i;  
        long a;
        a=0;
                
        t0=clock();
        for(i=0;i<10000000;i++){
                a=a+1;
                a=a+1;
                a=a+1;
                a=a+1;
                a=a+1;
                a=a+1;
                a=a+1;
                a=a+1;
                a=a+1;
                a=a+1;
        }
        t1=clock();
        printf("a=a+1; %f[s]\n", (t1-t0)/1000.0 );
        
        t0=clock();
        for(i=0;i<10000000;i++){
                a=longFn(a);
                a=longFn(a);
                a=longFn(a);
                a=longFn(a);
                a=longFn(a);
                a=longFn(a);
                a=longFn(a);
                a=longFn(a);
                a=longFn(a);
                a=longFn(a);
        }
        t1=clock();
        printf("a=longFn(a); %f[s]\n", (t1-t0)/1000.0 );
        return 0;
}

関数は定義の前に関数頭部を記述することがANSIで推奨されているため実行しようとすると警告が表示されるかも知れませんが、プログラム自体は実行されると思います。

※実数の場合も測定してください。上記の計算でlongFnに対応する関数はもちろん自分でかかないといけません。この関数は以下の様になります。

double doubleFn(double x){return x+1;}

※pascalでは返す値は関数名に代入しましたが、Cではreturnの後に返す値を書きます。

(5) 測定結果のまとめ

自分の測定結果で下記の表をうめてレポートで報告して下さい。

四則演算

1億回の計算

処理時間[s]

1回の処理時間[ns]

 0.14

-
-

a+=1;

 1.25

 1.11

 11.1

a-=1;

 

 

 

a*=2;

 

 

 

a/=2;

 

 

 

x+=1.0;

 2.70

 2.56

 25.6

x-=1.0;

 

  

 

x*=1.0000001;

 

  

 

x/=1.0000001;

 

  

 

※処理時間は何度か測定してみるとわかりますが100分の数秒程度の誤差が含まれます。

初等関数の計算
1億回の計算
処理時間[s]
1回の処理時間[ns]
 0.14 - -

x+=exp(1.1);

 38.84

 38.70

 387.0

x+=log(1.1);

 

 

x+=sin(1.1);

 

 

 

x+=cos(1.1);

 

 

 

x+=tan(1.1);

 

 

 

関数呼び出しのオーバーヘッド
1億回の計算
処理時間[s]
1回の処理時間[ns]
a=a+1;

1.31

-

-

a=longAdd1(a);

2.33

 1.02

 10.2

x=x+1;

 

-

-

x=doubleAdd1(x);

 

 

 

 

[目次]


9-5.考察課題

実験に関する考察を2点以上書いてください。自由な課題で考察を書いてかまいません。もし課題が思い付かない場合は以下から選んでください。


[ next | prev | index]