より高速、より効率のいいプログラムを書く


プログラムをやる中で 「高速」「効率」という単語をよく聞きますが、これらの要素はそれほど重要なものなのでしょうか?
答えは「場合による」というのがベストでしょうか。

もともと、「プログラム」とはコンピュータを動かすための命令語群です。よって、同じ動作をさせるなら、命令の語数は少ない方が高速になります。
コンピュータの要素をもつ機器は幅広く、パソコンを始め電卓や時計、炊飯器、テレビ、リモコン、自動販売機、プリンタ、車、・・・、数えればキリが無いほど多くのモノに組み込まれています。「プログラム」を利用するのはパソコンだけではありません。これらの機器にも立派なプログラムが内蔵してあるのです。

特に、パソコン以外の機器ではCPUの性能やメモリの容量など、あらゆる性能がかなり低く抑えられています。(これはコストや電力、スペース的な問題などがあるからです)
これらのコンピュータ部分の性能の低い機器ではよりコンパクトでより高速なプログラムが求められます。そうしないと、操作に対する反応が遅くなり使い物になりません。

学生の間は最新型の高速なパソコン環境に恵まれるため、どんな無駄な処理を書き連ねたプログラムでも快適に動いてくれます。
しかし、プログラムを勉強して仕事としてプログラマーを目指す人は、将来会社で「性能の低い環境を対象に」プログラムをすることも考慮に入れてプログラミングの勉強をすべきだと思います。

プログラムは書くときに2つの要素をバランスよく混ぜ合わせる必要があります。

1つは「人に読みやすい」プログラム、もう1つは「コンピュータに無駄な処理をさせない」プログラムです。

一番ベストな方法は「人に読みやすくするためにコメントをたくさん書き、コンピュータに無駄な処理をさせないシンプルなプログラムを書く」ことです。コメントは無くても問題無く動きますが、コメントがないと後から見たとき「何を書いているのか」さっぱり分かりません。特に、後者の「高速性」を重視させたプログラムを書く場合、この傾向は必然と強くなってしまいます。少々キーボードを入力するのが面倒でも、後で時間をかけて解析する手間を考えたら分かっているうちにコメントを書くほうが得策です。

では、実例を挙げながらどうすれば効率がいいのかを説明します。



○無駄な処理を省く

詳しくはこちらのページに書いてありますのでご覧下さい。
要するに、真偽判定、ポインタの上手な使い方です。



○真偽判定

真偽判定とは

if( ○●◎ )

while( ◆■□ )

という条件文で使う判定項目○●◎、◆■□のことです。
これらの条件文は0か0以外かで処理をするかしないか判断します。

また、評価式

▲▽ == ☆★
▲▽ != ☆★
▲▽ >= ☆★
▲▽ <= ☆★
▲▽ > ☆★
▲▽ < ☆★

は、式が成り立つ時は1、成り立たない時は0を意味します。
例えば、

int n;
n = (8 == 6);

とするとn = 0

n = (9 == 9);

とするとn = 1
となります。
これは、以下の条件演算子と呼ばれる文法で書いた式と同等です。

▲▽ == ☆★ ?  1 : 0;

この「 ? : 」構文が条件演算子と呼ばれるものです。「▲▽ == ☆★ 」が成り立てば 1、そうでなければ0を意味します。この構文はベテランプログラマーは比較的よく使います。
ちょっと、横道にそれましたが(^_^; 、「0か0以外か」で覚えにくかったら「重みのある数値が入っているか入っていないか」で覚えてもらっても構いません。
この真偽判定の法則を知っていると、多くの場面で「プログラムの効率化」が行えます。

また、if文では例えば次のような書き方もできます。

if(data - 6)

これはdataが6以外の時に実行されます。

if(data != 6)

と等価です。(この場合、どちらが高速なのかな?(汗))



○メモリの直接操作

もう1つ、「ポインタの上手な使い方」、すなわち、メモリを意識したプログラムを心がけることです。
例えば、構造体のコピー処理があったとします。

struct tagData{
  int age;
  int height;
  char name[256];
  char address[512];
  char tel[20];
  char email_addr[256];
} MyData,newMyData;

・・・
MyData の初期値設定
・・・

/* newMyData に MyData をコピー  */
newMyData.age = MyData.age;
newMyData.height = MyData.height;
strcpy(newMyData.name.MyData.name);
strcpy(newMyData.address.MyData.address);
strcpy(newMyData.tel.MyData.tel);
strcpy(newMyData.email_addr.MyData.email_addr);

これは、かなりの無駄です。
この処理はメモリ操作関数を使えば1行で行えます。

memcpy(&newMyData,&MyData,sizeof(MyData));

memcpy関数(メモリコピー)は非常に便利な関数で、
memcpy( コピー先の先頭アドレス , コピー元の先頭アドレス  , コピーする大きさ(単位バイト) );
と、メモリ上にあるデータを一気に指定した大きさだけコピーしてくれます。

「メモリ」という単語が出てきただけでほとんどの人は「意味不明」だとおもいますが、「宣言した変数がメモリ上でどうなっているのか?」を考えればすぐに理解できます。

たとえば、上記の構造体は
 
MyData.age MyData.height MyData.name MyData.address MyData.tel MyData.email_addr 空き 空き newMyData.age newMyData.height newMyData.name newMyData.address newMyData.tel newMyData.email_addr 空き 空き 空き 空き 空き 空き

といった感じにメモリ上に配置されます。構造体の約束事を思い出してください!「メモリ上で途切れることなく連続する」でしたね。「連続性」がないと、この処理は行えません。

上記のmemcpy関数を詳しく分析すると
 
&MyData MyData.height MyData.name MyData.address MyData.tel MyData.email_addr 空き 空き &newMyData newMyData.height newMyData.name newMyData.address newMyData.tel newMyData.email_addr 空き 空き 空き 空き 空き 空き

と、2つの領域間をコピーしています。
 

この色のセルはsizeof(MyData)が及ぶ範囲を色付けしたものです。
これで、代入文を使わずに一気にデータのコピーがメモリ上で行えます。
ちなみに、構造体のコピーに関しては

newMyData = MyData;

でも問題ありません。(コンパイラの内部ではmemcpyと同じ処理を行っているような気がしますが・・・。

もう1つ、初期化もメモリ操作関数を使えば一気に出来ます。例えば、次のような配列を初期化する場合、みなさんはこのとおりにするでしょう。

double data[300];
int n;

for(n = 0;n < 300;n++)
  data[n] = 0;

この処理も無駄がかなり多いといえます。300回for文の真偽判定を行い、変数nの値の加算を行い、しかもdata配列への代入も行います。
これも、とあるメモリ操作関数を使えば一気に行えます。

memset( &data, 0, sizeof(data));

memset関数(メモリセット)は
memset(先頭のアドレス, セットする1バイトの値, 大きさ(範囲));
という構文を使い、「先頭のアドレス」から「大きさ」分だけ「セットする1バイトの値」で値を埋めます
上の例ではこの配列の領域を全て0で埋めます。ただし、この関数は0で初期化する場合、文字列配列である区間を特殊な文字で埋める場合以外にはあまり使い道はありません。
なぜなら、1バイト単位で同じデータを埋めていくため、2バイト、4バイト変数となると違う意味合いになってしまうからです。例えば4バイト変数を0x01で埋めた場合、変数の中身は0x01010101となり、0x00000001ではありません。


関数ポインタの配列を使って高速化

 switch〜case文というものがあります。これは比較的みなさんもよく使うことと思います。

switch(value)
{
  case 1:
       ・・・
      break;
  case 2:
       ・・・
      break;
  case 3:
       ・・・
      break;
  case 4:
       ・・・
      break;
  case 5:
       ・・・
      break;
  case 6:
       ・・・
      break;
  case 7:
       ・・・
      break;
  default:
       ・・・
     break;
}

しかし、コンピュータから見れば実は次のような構造として解釈されます。

if(value == 1){
       ・・・
}else if(value == 2){
       ・・・
}else if(value == 3){
       ・・・
}else if(value == 4){
       ・・・
}else if(value == 5){
       ・・・
}else if(value == 6){
       ・・・
}else if(value == 7){
       ・・・
}else{
       ・・・
}

すなわち、下に行くほど真偽の判定回数が多くなってしまいます。この場合は1〜7以外の値が最も判定回数が多く、次いで7が多くなります。逆に一回の判定で済むのは1です。
この構造を知っていたら、switch〜case文は上の方に頻繁に起こりうる値の文を持ってきたら良いことが分かります。

しかし、全ての条件に同等のコストで条件分岐することは出来ないのでしょうか?
それは、関数ポインタの配列を使えば可能です。イメージ的には関数ポインタの配列をスイッチとして使います。

関数ポインタ配列で切り替える場合、下準備をする必要があります。それは各処理に対応した関数を作ることです。
上のswitch〜case文の場合、1〜7の各値に対応した関数、それにそれ以外の値に対応した関数の計8つの関数を作ります。
8つの関数は返り値も引数も統一してあげます。

(例)
int func_1(int x,int y);
int func_2(int x,int y);
int func_3(int x,int y);
int func_4(int x,int y);
int func_5(int x,int y);
int func_6(int x,int y);
int func_7(int x,int y);
int func_other(int x,int y);

各関数内でswitch〜case文の・・・部分に相当する処理を行います。
次に、関数ポインタの配列を宣言します。valueの値は0〜255の範囲を取るとし、それに対応するため256個のサイズにします。

int (*switch_func[256])( );

次に、main関数内で次のような初期化をまず行います。

for( n = 0;n < 256;n++)
    switch_func[n] = &func_other;

これで、nの値がどの値でもfunc_other関数が呼ばれるようになりました。
続いて、1〜7に対して次のように設定します。

switch_func[1] = &func_1;
switch_func[2] = &func_2;
switch_func[3] = &func_3;
switch_func[4] = &func_4;
switch_func[5] = &func_5;
switch_func[6] = &func_6;
switch_func[7] = &func_7;

これで、全ての設定が終わりました。
あとはswitch〜case文の場所を次の1行で置き換えます。

switch_func[value]( data1, data2);

この1行だけで、任意の値に対して必要な処理を行ってくれます。しかも値に対する処理コストは均一で、高速です。
ただし、若干メモリの必要量が増えることは覚悟してください。