ホーム » 「malloc」タグがついた投稿 (ページ 2)

タグアーカイブ: malloc

2024年4月
 123456
78910111213
14151617181920
21222324252627
282930  

検索・リンク

効率のよいメモリ使用と動的メモリ確保

次にメモリの利用効率の話について解説する。

配列宣言でサイズは定数

C言語では、配列宣言を行う時は、配列サイズに変数を使うことはできない。

最近のC(C99)では、実は下記のようなものは、裏で後述のalloca()を使って動いたりする。(^_^;

void foo( int size ) {
   int array[ size ] ;         // エラー
   for( int i = 0 ; i < size ; i++ )
      array[ i ] = i*i ;
}
void main() {
   foo( 3 ) ;
   foo( 4 ) ;
}

メモリ利用の効率

配列サイズには、定数式しか使えないので、1クラスの名前のデータを覚えるなら、以下のような宣言が一般的であろう。

#define MEMBER_SIZE 50
#define NAME_LENGTH 20
char name[ MEMBER_SIZE ][ NAME_LENGTH ] ;

しかしながら、クラスに寿限無とか銀魂の「ビチグソ丸」のような名前の人がいたら、20文字では足りない。(“t-saitoh”くんは配列サイズ9byte、”寿限無”くんは配列220byte といった使い方はできない) また、クラスの人数も、巨大大学の学生全員を覚えたいとい話であれば、 10000人分を用意する必要がある。 ただし、10000人の”寿限無”ありを考慮して、5Mbyte の配列を準備したのに、与えられたデータ量が100件で終わってしまうなら、その際のメモリの利用効率は極めて低い。

このため、最も簡単な方法は、以下のように巨大な文字配列に先頭から名前を入れていき、 文字ポインタ配列に、各名前の先頭の場所を入れる方式であれば、 途中に寿限無がいたとしても、問題はない。

char array[2000] = "ayuka¥0mitsuki¥0t-saitoh¥0tomoko¥0....." ;
char *name[ 50 ] = {
   array+0 , array+6 , array+14 , array+23 , ...
} ;

この方式であれば、2000byte + 4byte(32bitポインタ)×50 のメモリがあれば、 無駄なメモリ空間も必要最低限とすることができる。

参考:
寿限無(文字数:全角103文字)

さる御方、ビチクソ丸(文字数:全角210文字)

引用Wikipedia

大きな配列を少しづつ貸し出す処理

// 巨大な配列
char str[ 10000 ] ;
// 使用領域の末尾(初期値は巨大配列の先頭)
char* sp = str ;
// 文字列を保存する関数
char* entry( char* s ) {
   char* ret = sp ;
   strcpy( sp , s ) ;
   sp += strlen( s ) + 1 ;
   return ret ;
}
int main() {
   char* names[ 10 ] ;
   names[ 0 ] = entry( "saitoh" ) ;
   names[ 1 ] = entry( "jugemu-jugemu-gokono-surikire..." ) ;
   return 0 ;
}
// str[] s a i t o h ¥0 t o m o k o ¥0
//       ↑             ↑
//     names[0]        names[1]

このプログラムでは、貸し出す度に、sp のポインタを後ろに移動していく。

スタック

この貸し出す度に、末尾の場所をずらす方式にスタックがある。

int stack[ 100 ] ;
int* sp = stack ;
void push( int x ) {
   *sp = x ;    // 1行で書くなら
   sp++ ;       // *sp++ = x ;
}
int pop() {
   sp-- ;
   return *sp ; // return *(--sp) ;
}
int main() {
   push( 1 ) ;
   push( 2 ) ;
   push( 3 ) ;
   printf( "%d¥n" , pop() ) ;
   printf( "%d¥n" , pop() ) ;
   printf( "%d¥n" , pop() ) ;
   return 0 ;
}


スタックは、最後に保存したデータを最初に取り出せる(Last In First Out)から、LIFO とも呼ばれる。
このデータ管理方法は、最後に呼び出した関数が最初に終了することから、関数の戻り番地の保存や、最後に確保した局所変数が最初に不要となることから、局所変数の管理に利用されている。

alloca() 関数

局所変数と同じスタック上に、一時的にデータを保存する配列を作り、関数が終わると不要になる場合には、alloca() 関数が便利である。alloca の引数には、必要なメモリの byte 数を指定する。100個の整数データを保存するのであれば、int が 32bit の 4byte であれば 400byte を指定する。ただし、int 型は16bitコンピュータなら2byteかもしれないし、64bitコンピュータなら、8byte かもしれないので、sizeof() 演算子を使い、100 * sizeof( int ) と書くべきである。

#include <alloca.h>
void foo( int size ) {
   int* p ;
   // 
   p = (int*)alloca( sizeof( int ) * size ) ;
   for( int i = 0 ; i < size ; i++ )
      p[ i ] = i*i ;
}
void main() {
   foo( 3 ) ;
   foo( 4 ) ;
}

alloca() は、指定された byte 数のデータ領域の先頭ポインタを返すが、その領域を 文字を保存するために使うか、int を保存するために使うかは alloca() では解らない。alloca() の返り値は、使う用途に応じて型キャストが必要である。文字を保存するなら、(char*)alloca(…) 、 intを保存するなら (int*)alloca(…) のように使う。

ただし、関数内で alloca で確保したメモリは、その関数が終了すると、その領域は使えなくなる。このため、最後に alloca で確保したメモリが、最初に不要となる…ような使い方でしか使えない。

動的メモリ確保(malloc()とfreelist)

C言語では、動的メモリ領域をどのように管理していくのか解説する。

局所変数とスタック

局所変数は、関数に入った時に作られるメモリ領域であり、関数の処理を抜けると自動的に開放されるデータ領域である。

関数の中で関数が呼び出されると、スタックには戻り番地情報を保存し、関数に移動する。最初の処理で局所変数領域が確保され、関数を終えると局所変数は開放される。
この局所変数の確保と開放は、最後に確保された領域を最初に開放される(Last In First Out)ことから、スタック上に保存される。

baz()の中で、「*((&c)+4) = 123 ;」を実行したら、bar()のxを書き換えられるかも…

動的メモリ領域とフリーリスト

動的なメモリ領域(ヒープ領域)は、malloc()関数で処理用のメモリを借りfree()関数で使わなくなったメモリを返却する。

この返却されたメモリ領域は、改めて malloc() が呼び出されたときに再利用を行う。この再利用するメモリ領域は、簡単に扱えるようにリスト構造にして保存する。この free された再利用候補のリスト構造は、free_list と呼ばれる。

mallocが一定サイズの場合

free_list の考え方を説明するために、malloc() でのメモリサイズが一定として説明を行う。

malloc() が呼び出される度に、free_list の先頭から貸し出すメモリを取り出し(a=malloc(),b=malloc(),c=malloc()まで)、free() が呼び出されると、返却されたメモリは、free_list の先頭につないでおく。

任意サイズのメモリ確保の場合

この図の一番下の赤部分は、次のメモリブロックへのポインタとブロックサイズの大きさが20byteの場合の例。

malloc() で、指定されたサイズのものが、free_list の中にあれば、それを使う。

丁度いいサイズが無い場合は、それより大きいメモリブロックの後半を切り分けて、貸し出す。

この例の最後の処理では、20byte,60byte,40byte,50byteが併合された例。併合後のブロックサイズは、すこしいい加減に書いてある。

使用されていたメモリブロックが free() で返却された場合は、free_list につないでいく。ただし、単純にリストに繋ぐだけであれば、malloc(),free() を繰り返すと、小さなメモリブロックばかりになってしまい、大きいメモリのmalloc()ができなくなる。

そこで、free() で返却される際には、隣り合うメモリブロックと併合できるかを確認し、大きなメモリブロックになるような処理を行う。

また、隣り合うメモリブロックが併合できるかの判定が簡単になるように、free_listにつなぐ際は、次のメモリブロックへのポインタは、昇順となるように並べる。

一般的には、上記のようにmalloc(),free()を行うが(K&Rのmallocアルゴリズム)、mallocのサイズが小さい場合には併合処理などは隣接確認などが手間がかかる。
そこで、最初に説明した一定サイズのmalloc()の手法で、8byte専用のfreelist,16byte専用のfreelist,32byte専用のfreelistのように2Nbyteのfreelistで管理する。10byteといった中途半端なサイズの時は、それより大きい16byteのfreelistを使う。(dlmallocのアルゴリズム)

ヒープメモリの断片化

ヒープメモリの malloc() , free() を繰り返すと、最悪、以下の図の様に、使用中領域(赤)とfreeされた未使用領域(黒)が交互に並ぶ状態が発生するかもしれない。この場合、全体の未使用領域の合計では十分なサイズでも、小さなメモリブロックばかりとなって、大きなメモリブロックを要求されても十分な大きさのメモリが見つからない状態が発生する場合がある。

この状態をヒープメモリの断片化といい、使用しづらい小さなメモリブロックはヒープホールと呼ばれる。

(補足) 断片化

断片化というと、OSではハードディスクの断片化(フラグメンテーション)を思い浮かべるかもしれない。ハードディスクの断片化とは、ファイル領域の割り当てとファイルの削除を繰り返すことで、ファイルのセクタが不連続となり、アクセス効率が悪くなる現象。OSによっては、ファイル実体の位置を動かすことで断片化を改善できる。以下の図のようにフラグメンテーションを防ぐための実体の移動を行う最適化はデフラグと呼ばれる。

上記の図では、上の青の図が断片化が発生している事例で、a1→a2,a2→a3の時にヘッド移動(シーク時間)が発生する。下の赤の図のように、デフラグ処理を施すことでシーク時間が減らせる。

様々なメモリ確保

前回の授業で説明していたような、必要に応じて確保するメモリは、動的メモリと呼ばれる。

動的メモリも、局所変数やalloca()を用いたスタック領域と、malloc()とfree()を使うヒープメモリ領域に分類される。

strdup

前回の文字列の確保の説明では、malloc()とstrcpy()をあわせて実行していたが、C言語ではこういった処理が多いので、専用の関数 strdup() がある。

char str[] = "abcdefg" ;
char*pc ;
if ( (pc = (char*)malloc( strlen( str ) + 1 )) != NULL ) {
   strcpy( pc , str ) ;
}
// おなじことを strdup では...
pc = strdup( str ) ;

様々なメモリ確保

自分で定義した構造体を、malloc で領域確保しながら使う場合、1次元配列や2次元配列を作る場合、色々な確保の方法がある。

// 複素数を例に
struct Complex {
   double re ;
   double im ;
} ;
// 基本
struct Complex a ;
a.re = 1.0 ;
a.im = 2.0 ;
// ポインタで確保
struct Complex* b ;
b = (struct Complex*)malloc( sizeof( struct Complex ) ) ;
if ( b != NULL ) {
   b->re = 1.0 ;
   b->im = 2.0 ;
}
// 一次元配列
struct Complex c[ 2 ] ;  // 通常の使い方
c[0].re = 2.0 ;
c[0].im = 3.0 ;
c[1].re = 4.0 ;
c[1].im = 5.0 ;
// 一次元配列を動的に確保
struct Complex* d ;      // Complexの配列
d = (struct Complex*)malloc( sizeof( struct Complex ) * 2 ) ;
if ( d != NULL ) {
    d[0].re = 2.0 ; d[0].im = 3.0 ;
    d[1].re = 4.0 ; d[1].im = 5.0 ;
}
// 一次元のポインタ配列
struct Complex* e[ 2 ] ; // Complexのポインタの配列
e[0] = (struct Complex*)malloc( sizeof( struct Complex ) ) ;
if ( e[0] != NULL ) {
    e[0]->re = 2.0 ; e[0]->im = 3.0 ;
}
e[1] = (struct Complex*)malloc( sizeof( struct Complex ) ) ;
if ( e[1] != NULL ) {
    e[1]->re = 4.0 ; e[1]->im = 5.0 ;
}

C++での new, delete 演算子

複雑なデータ構造のプログラムを作成する場合には、このような malloc() , free() をよく使うが煩雑であるため、C++ではこれらをすっきりと記述するために、new 演算子、delete 演算子があり、それぞれ malloc(), free() に相当する。

// 単独
Complex* b = new Complex ;
b->re = 1.0 ;
b->im = 2.0 ;
delete b ;
// 配列
Complex* d = new Complex[2] ;
d[0].re = 2.0 ;
d[0].im = 3.0 ;
d[1].re = 4.0 ;
d[1].im = 5.0 ;
delete[] d ;  // 配列のdeleteには[]が必要
// ポインタの配列
Complex* e[2] ;
e[0] = new Complex ;
e[0]->re = 2.0 ;
e[0]->im = 3.0 ;
e[1] = new Complex ;
e[1]->re = 4.0 ;
e[1]->im = 5.0 ;
delete e[0] ;
delete e[1] ;

2次元配列

2次元配列の扱いでも、注意が必要。

int cs = 何らかの値 ; // データ列数
int rs = 何らかの値 ; // データ行数
int a[ rs ][ cs ] ;  // C言語ではエラー
a[ y ][ x ] = 123 ;

// 1次元配列を2次元配列のように使う
int* b ;
b = (int*)malloc( sizeof( int ) * rs * cs ) ;
b[ y * cs + x ] = 123 ;  // b[ y ][ x ] への代入

// 配列へのポインタの配列
int** c ;
c = (int**)malloc( sizeof( int* ) * rs ) ;  // NULLチェック省略
c[0] = (int*)malloc( sizeof( int ) * cs ) ;
c[1] = (int*)malloc( sizeof( int ) * cs ) ;
:
c[ y ][ x ] = 123 ;

レポート課題

メモリの動的確保の理解のために、自分の理解度に応じて以下のプログラムのいずれかを作成せよ。
ただし、プログラム作成時には、配列サイズは未定で、プログラム起動時に配列サイズを入力するものとする。

  • 固定長の名前で、人数が不明。
  • 長い名前かもしれない名前で、人数も不明
  • 複素数のデータで、データ件数が不明。
  • 名前と電話番号のデータで、データ件数が不明。

このような状況で、データを入力し、検索などの処理を通して自分のプログラムが正しく動くことを検証せよ。
レポートには、プログラムリスト、プログラムの説明、動作確認した結果、考察を記載すること。

C++のvectorクラスを使ったら

// C++であればvectorクラスを使えば配列なんて簡単
#include <vector>
int main() {
   // 1次元配列
   std::vector<int> a( 10 ) ;
   for( int i = 0 ; i < 10 ; i++ )
      a[ i ] = i ;
   // 2次元配列
   std::vector< std::vector<int> > b( 9 , std::vector<int>(9) ) ;
   //                           ↑ ここの空白は重要
   for( int i = 0 ; i < 9 ; i++ ) {    // ">>" と書くとシフト演算子
      for( int j = 0 ; j < 9 ; j++ ) { // "> >" と書くと2つの">"
         b[i][j] = (i+1) * (j+1) ;
      }
   }
   return 0 ;
}

mallocとfree

前回の講義での、「長いかもしれない名前」を覚える処理は、最悪の場合をどう扱うかでメモリのムダが発生する。
ここで、前回講義で説明した、大きな配列を少しづつ分けて使う処理を考える。

大きな配列を少しづつ貸し出す処理

char str[ 10000 ] ;
char* sp = str ;
char entry( char* s ) {
   char* ret = sp ;
   strcpy( sp , s ) ;
   sp += strlen( s ) + 1 ;
   return ret ;
}
int main() {
   char* names[ 10 ] ;
   names[ 0 ] = entry( "saitoh" ) ;
   names[ 1 ] = entry( "tomoko" ) ;
   return 0 ;
}
// str[] s a i t o h ¥0 t o m o k o ¥0
//       ↑             ↑
//     names[0]        names[1]

このプログラムでは、貸し出す度に、sp のポインタを後ろに移動していく。

スタック

この貸し出す度に、末尾の場所をずらす方式にスタックがある。

int stack[ 100 ] ;
int* sp = stack ;
void push( int x ) {
   *sp = x ;    // 1行で書くなら
   sp++ ;       // *sp++ = x ;
}
int pop() {
   sp-- ;
   return *sp ; // return *(--sp) ;
}
int main() {
   push( 1 ) ;
   push( 2 ) ;
   push( 3 ) ;
   printf( "%d¥n" , pop() ) ;
   printf( "%d¥n" , pop() ) ;
   printf( "%d¥n" , pop() ) ;
   return 0 ;
}


スタックは、最後に保存したデータを最初に取り出せる(Last In First Out)から、LIFO とも呼ばれる。
このデータ管理方法は、最後に呼び出した関数が最初に終了することから、関数の戻り番地の保存や、最後に確保した局所変数が最初に不要となることから、局所変数の管理に利用されている。

スタック上の動的メモリ確保 alloca

最初のプログラム例のような方法で、スタック上にメモリを確保する関数として、alloca() がある。

// C言語では、配列サイズに変数を使えない。
int size = ... ;
int array[ size ] ;

// これを alloca で書くと...
int size = ... ;
int* array ;
array = (int*)alloca( sizeof( int ) * size ) ;
if ( array != NULL ) { // スタック溢れはNULLで検知できないか...
   :
   // array[]を使った処理
   :
}

ただし、alloca はスタック領域を使用するため、数MBといった巨大なデータを確保するのには使えない。
この領域は、スタックのように末尾だけを覚えておけばいいので、管理が簡単である。一方で、関数の局所変数として確保して、「この場所を使ってこの計算してね」的な使い方をしなければならない。「この場所を返すから後は自由に使って」的な使い方はできない。

malloc()とfree()

alloca を使うような処理は、スタックのように「最後に確保したものが最初に不要となる」という状況でしか使えない。
確保した領域が不要となる順序が判らない場合には、malloc() を使う必要がある。

ポインタ = malloc( 確保するbyte数 ) ;
   メモリ不足で malloc に失敗したら NULL を返す。
free( ポインタ ) ;
   確保したメモリ領域を解放する。
   解放されたメモリは、mallocで再利用してくれる。

最初に説明した、入力された文字を次々と保存する処理を malloc で記述すると以下のようになる。

char* names[ 100 ] ;
char buff[ 1000 ] ;
int size ;

// データ入力
for( size = 0 ; size < 100 ; size++ ) {
   fgets( buff , sizeof( buff ) , stdin ) ;
   names[ size ] = (char*)malloc( strlen( buff ) + 1 ) ;
   if ( names[ size ] == NULL )
      break ;
   strcpy( names[ size ] , buff ) ;
}
// データを使う処理
for( int i = 0 ; i < size ; i++ ) {
   // names[] を使う処理...
   printf( "%s" , names[ i ] ) ;
}
// データ領域をfreeで解放
for( int i = 0 ; i < size ; i++ )
   free( names[ i ] ) ;

malloc() で確保したメモリ領域は、free() で解放しない場合、メモリ領域は使われないムダな領域が蓄積して、最終的にはメモリ不足で止まるかもしれない。また、大量のムダなメモリ領域ができると、仮想メモリが使われ処理速度の低下が発生するかもしれない。
このような、解放されないメモリ領域が発生することは、メモリーリークと呼ばれる。

確保したメモリは、プロセス毎に管理されているので、長時間動きっぱなしのプログラムでメモリリークが発生すると問題となる。
ただし、プロセス終了と共に確保されているメモリはOSに回収されるので、処理が終わってすぐにプロセス自体も終わるのであれば、free() を書き忘れても問題は発生しない。

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー