コンピューター:C言語講座:メモリーについて(2)


 メモリーについて(1)に続いて動的割り当てを考えてみます。

動的なメモリー割当
 動的なメモリー割当とはプログラム実行中に必要に応じてメモリーを確保していく方法で、一般的にはmalloc(),calloc(),realloc(),free()関数を使用します。他にもsbrk()システムコールを直接呼び出す方法もありますが、これはmalloc()などと共存できませんので、X,Motifなどのライブラリーを使ったりするだけで既に直接sbrk()を使うことはできません。また、mmap()システムコールを使い、ファイルをマッピングする方法も動的割り当てとして考えられます。ここでは一般的なmalloc()関連の関数を用いた方法を対象にしてみます。
 メモリーを動的に割り当てるメリットとしては次のものが考えられます。
     ・必要な量を実行時に割り当てられる。
     ・サイズの拡張もできる。
     ・関数の内外に関係無く開放しないかぎり保持される。
 デメリットとしては以下のような点でしょう。
     ・難しそう(入門書にかかれていない)。
     ・不要になった時に開放し忘れがち。
     ・プログラム実行時に必要なメモリーサイズがあらかじめわからない。
 こんなところでしょうか。一番の問題はやはり、難しそう、という点ではないでしょうか?それ以前に、入門書に出てこないのでそもそも知らない、ということも考えられます(私自身だいぶ長いあいだ知りませんでした)。知ってしまえば別に難しいものではありません。

使い方
 念の為基本的な使い方を説明してみます。まず、malloc(),calloc()でメモリーを確保します。両者の違いは引数が1つか2つかという点と、calloc()は0で初期化されるという点です。calloc()は内部でmalloc()を呼び出しますし、初期化しますので、どうしても速度が気になる方はmalloc()を直接呼ぶほうが速いですが、実際はmalloc()自体が遅いのであまり気にしてもしょうがないのですが。サイズを拡張したければrealloc()を使用します。realloc()には多くの注意が必要なので、これは後でまとめて考えます。そして不要になったらfree()します。

#include    <malloc.h>

void main()
{
int   *ptr;

    ptr=(int *)malloc(100*sizeof(int));
    /* or ptr=calloc(100,sizeof(int));*/

    ptr=(int *)realloc(ptr,200*sizeof(int));

    free(ptr);
}

 この例ではint型を100個分確保し、その後、200個分に拡張し、開放しています。malloc(),calloc(),realloc()の返り値はchar *またはvoid *なのでキャストは必ず行なっておきます。このように確保したメモリーは*ptr,*(ptr+1)などポインター風にアクセスしてもかまいませんし、ptr[0],ptr[1]と配列風にアクセスしてもかまいません。もちろんポインター風の方が高速ですが、配列風の見やすさも捨てがたいので速度が重要でない時は使い分けましょう。

注意
 さて、使うのはわかればいたって簡単なのがおわかりいただけたと思いますが、配列などの静的割り当てに比べ注意しておかねばならないこともあります。
 動的メモリー割り当てといっても実際のメモリーはシリアルに並んでいるわけでいろいろなサイズで確保・開放を繰り返すと無駄が生じます。これらの動的メモリー割り当て関数は割り当て要求が来ると、まず、既にプログラムが確保した領域の中から空いているところを探します。一度確保したものは開放してもプログラムから開放されるわけではなく、空き領域として管理されます。このとき要求したサイズが空き領域の中で収まるものがあればそこが確保され、そのアドレスが返されます。従ってプログラムの実行サイズは変わりません。しかし、空きの中に収まらない場合はsbrk()を呼び出してヒープ領域を拡大します。そして新たにシステムからもらったアドレスを返します。
 おわかりのように、開放した時に空きメモリーを寄せ集めて大きな領域を作ることはできません(使用中のアドレスが変わってしまうので)。従って、小さな領域をバラバラに開放されてもなかなか再利用ができないのです(実際はmallocなどはもうすこし賢くて小さなエリアは別管理したりしていますが)。これはとくにrealloc()を使うと問題になります。
 realloc()はmalloc()で割り当てた領域を拡張しますが、malloc()は大抵少し大きめに確保するのですが、その先が既に使用中の場合は拡張できず、新しい領域を求めてヒープを拡大し、そこに元のデータをコピーし、元のデータを開放します。従って、realloc()とmalloc()がループで回るような場合はほとんど拡大ができず、毎回realloc()は新しい領域を求めてしまうことになり、メモリーのすき間がたくさんできてしまうことになります。下のようなプログラムです。

int func()
{
int   *ptr;
int   i;

    for(i=0;i<100000;i++)
        {
        if(i==0){
            ptr=(int *)malloc(1*sizeof(int));
        }
        else{
            ptr=(int *)realloc(ptr,i*sizeof(int));
        }
        ptmp=malloc(???);
        ptmp=malloc(???);
        ptmp=malloc(???);
        ptmp=malloc(???);
    }

    return(0);
}

 CADなどを作っているとこういうように書きたいことは非常に多く、データを読み込みながら格納先を拡張し、中身を更に割り当てる場合などはまさにこのようになります。
 これは非常に深刻な問題で、計算上は10MByteしかいらないプログラムがひどい場合は100MByteでも足りないということも置きます(私は何回も経験しています)。データを新しい領域にコピーする必要もあるので、速度も非常に遅くなります。この問題を嫌ってrealloc()は全く使わないというプログラマーもいます。また、実はmalloc()自体にいろいろなバージョンがあって、バージョンによって速度優先型と使用量優先型に大別され、それによって結果がだいぶ異なります。
 これを回避するにはrealloc()する単位を大きめに取り、realloc()を呼び出す回数をできるだけ減らすしかありません。それでも無駄は完全には無くせませんが、空きになるサイズが大きくなるので他のmalloc()がそこを使用できる確立が上がり、コピーの回数も減らせますので、速度も上がります。

int func()
{
int   *ptr;
int   i,size;

    size= -1;
    for(i=0;i<100000;i++){
        if(i>=size){
            size+=1000;
            if(i==0){
                ptr=(int *)malloc(size*sizeof(int));
            }
            else{
                ptr=(int *)realloc(ptr,size*sizeof(int));
            }
        }
        ptmp=malloc(???);
        ptmp=malloc(???);
        ptmp=malloc(???);
        ptmp=malloc(???);
    }

    return(0);
}

 このようにすると、realloc()は1000回に一度しか呼ばれませんのでだいぶ効率は上がります。何回に一度にするかは扱うデータによって考える必要があります。これでももちろん空きはできますので、計算通りのプログラムサイズにはなりません。どうしてもこれが気に入らない人は配列形式のデータの持ち方をあきらめて連結方式などに切替えるべきでしょう。しかし、そういった方式にも欠点はもちろんあり、前後の情報を持つメモリーが余計に必要ですし、なんといっても全開放が時間がかかります。あるいはmmap()を使って、別個にスワップさせるような方式も有効ですが、手間は結構かかります。

 さらに、realloc()には注意が必要で、拡張できずに再割当をするとアドレスが変わってしまうということがあります。

int func()
{
int   *ary,*ptr;
int   i;

    ary=(int *)malloc(100*sizeof(int));

    ptr=ary;
    for(i=0;i<200;i++){
        if(i>=100){
            ary=(int *)realloc(ary,200*sizeof(int));
        }
        *ptr=i;
        ptr++;
    }

    return(0);
}

 このような書き方をしますと、realloc()された時点で運が悪いとaryの指しているアドレスが移動しますので、ptr++で増やしていっても使わなくなってしまったアドレスに対して増やしてしまうことになります。配列風アクセスにするか、aryからのオフセットでアクセスするか、realloc()した時にptrを再指定する必要があります。1つの関数内ですと滅多にこのようなことはしないのですが、関数の階層が深くなると忘れてしまうことがよくあります。

 また、realloc()はmalloc()された物に対してしかできないことも注意が必要です。はじめからrealloc()だけを使うことはできません。Xのライブラリ関数などではXtRealloc()のようにNULLならmalloc()のように動くようになっているものもありますが、本物のrealloc()はだめです。

 大きめのプログラムを作成する際にはメモリー管理関数を自分で作り、そこでエラーチェックや、malloc(),realloc()の割り振りを行なうようにしたほうが安全なプログラムを作れます。毎回NULLチェックを記述するのは大変ですし、普通はNULL(メモリー不足など)なら、警告を出して死ぬか、他のアプリケーションを終らせてメモリーが空くまで待つ処理をするでしょうから、1つの関数で管理したほうが便利です。

 このように、配列には無い便利さの半面、注意も必要ですので、できれば「パッケージ化」を参考に、C++のコンストラクタ・デストラクタのように確保・初期化関数や開放関数などもデータ構造と組みで準備して毎回メモリー関数を考えながら組まなくて済むように準備するのが賢い方法といえるでしょう。そうしておけば、後でmmap()に移行せざるを得ない場合などにもパッケージ側の変更だけで済みます。


よろしければブログもご覧ください
ipv400190967 from 1998/3/4