コンピューター:C言語講座:メモリーについて(1)
小規模なプログラムを作っている時にはメモリーはほとんど気にせずに組めますが、大きなデータを扱うプログラムや速度を気にしなければならないプログラムではメモリーの知識は非常に重要です。
静的なメモリー割当
まず、静的なメモリー割り当てについて考えてみましょう。ここでいう静的とはmalloc()などで割り当てる動的に対するものと考えてください。静的なメモリー割当というとほとんどの場合、配列のことになると思います。配列を使用するメリットとしては、以下のようなものがあげられると思います。
・簡単に使える。
・関数内で宣言したローカルな配列は使い終った時に自分で開放を気にしなくて良い。
・ローカルな配列以外は初期化が簡単にできる。
デメリットとしては以下のようなことではないでしょうか?
・サイズの拡張ができない。
・余った時に無駄。
・グローバルの場合、不要になっても開放できない。
・ローカルで大きな配列を取ると、スタックを消費してしまう。
・ローカルな配列では戻り値として使えない。
まだまだあると思いますが、とりあえずすぐに思い付くものとしてはこんなところでしょうか。メリットとデメリットはほとんど対になっています。やはり初心者に取って最もありがたいのが、メリットの「簡単に使える」という点でしょう。入門書などを見ても配列の説明はいくらでもありますが、動的なメモリー割当についてはまず書かれていません。
静的な割当は上記のようにわかりやすく、気軽に使えるのですが、それゆえに気くばりを忘れて無駄な使い方していることがよくあります。まず、サイズをデータ量に合わせるのはそもそも無理として、ローカルで大きな配列を気にせずに取ってしまうことはよくありますが、これは関数が呼ばれた時にスタックに取られますので、当然その関数を呼び出すだけで使わなくても確保されてしまいます。下の例のように使わずにリターンするような処理では関数呼び出しのオーバーヘッドが非常に大きくなります。
int func(flag)
int flag;
{
int ary[10000];
if(flag){
return(-1);
}
/* aryを使った処理 */
return(0);
}
このような場合は条件判断を先にしてからこの関数を呼び出すように書き換えるべきでしょう。また、意外と気づかずにやってしまうのが次のような関数です。
char *func(val)
int val;
{
char str[512];
sprintf(str,"val = %d",val);
return(str);
}
おわかりでしょうが、strはローカル宣言なので、この関数が終ると同時に開放されてしまいます。したがってこのアドレスを戻った先で使おうとしても内容は保証されません(意外と問題無く動くことも多いのですが、大抵後で変更を加えた時におかしくなり、原因を探すのが大変になります)。この場合は無駄を覚悟でstatic宣言するか、格納先を呼び出し側から引数で渡すようにする必要があります。当然、staticにしてしまうと、静的に取られますので、プログラムが起動した時点から終るまでメモリーを占拠し続けますし、1つのアドレスしか持てないので、いつの内容しか格納できません。したがって、以下のような記述はダメです。
char *func(val)
int val;
{
static char str[512];
sprintf(str,"val = %d",val);
return(str);
}
int func_main(a,b)
int a,b;
{
printf("%s,%s\n",func(a),func(b));
return(0);
}
この場合、どちらも同じ値が表示されてしまいます。どちらが表示されるかは処理系によって違いますが、大抵はbの値が出ます。
また、スタックに取られるということはサイズに制限があるということで、スタック領域は無限に拡張されて使うことはできませんので、ローカルであまり大きなサイズを取ると、その関数が呼ばれた時点で死ぬか、あるいはそこから関数が呼べなくなる事態に陥ります。
さらに、配列はあまりにも便利すぎて、多次元の配列の時にどういう形でメモリーが取られているかを気にせずにプログラミングしてしまいがちです。次の例を見てください。
int ary[1000][1000];
int func()
{
int x,y;
for(y=0;y<1000;y++){
for(x=0;x<1000;x++){
ary[x][y]=0;
}
}
return(0);
}
ビットマップのようなデータを0で初期化しているようなプログラムです。何の問題もないように見えるかも知れませんが、この処理では、xを連続でアクセスしています。しかし、配列では左側程連続して取られますので、この場合はアドレスで考えると、
ary[0][0]=0(先頭)
ary[1][0]=1000*sizeof(int)
ary[2][0]=2000*sizeof(int)
です。これではxのループを連続して回るだけでかなり離れたアドレスをアクセスする必要があり、処理が遅くなります。文字列の配列ですと、次のように、
char ary[1000][1000];
int func()
{
strcpy(ary[0],"No.1");
strcpy(ary[1],"No.2");
printf("%s,%s\n",ary[0],ary[1]);
return(0);
}
気にしなくても左側を連続してアクセスするのですが、とくにx,y座標の配列とかですと、ついついxを右側にしてしまい、その割にアルゴリズムのほうでは人間は横(x)優先に考えてしまうので無駄なことをしてしまいます。この点は動的に割り当てていれば自分で気がつきますので(忘れることも多いですが)、はじめから考えることが多いのですが、配列では宣言が簡単すぎて気がつかないことが多いので注意しましょう。なお、更に高速にしたい場合にも連続アクセスのアドレスが連続していればポインターを使ってアクセスして容易に高速化が行なえます。
int ary[1000][1000];
int func()
{
int x,y,*ptr;
for(y=0;y<1000;y++){
ptr=ary[y];
for(x=0;x<1000;y++){
*ptr=0;
ptr++;
}
}
return(0);
}
ちょっと強引ですが、これでも1.2倍くらいは高速になります(この処理ならもっと速くできますが、とりあえず、xを連続で回る利点ということで)。
これらの点を注意すれば、簡単に使えますし、便利なものです。格納したいデータの大きさがほぼ決まっていて、とくにグローバルにする場合や、小さな大きさのローカルの場合には静的割当で十分でしょう。それ意外の場合は動的割当も検討する必要があります。