メモリの動的確保(Dynamic Memory Allocation)は、プログラムの実行中に必要に応じてメモリを割り当てる手法です。C言語ではmallocやcalloc、C++ではnew演算子がこれに該当します。
しかし、特に組み込みシステムやリアルタイムシステムの開発において、動的メモリ確保はしばしば避けられる傾向があります。
この記事では、特に組み込みシステムやリアルタイムシステムのC/C++プログラミングにおいて動的メモリ確保が嫌われる構造的な理由と、その代替手法について説明します。
そもそもどうやって動いているの?
伝統的な格言 「ソース読め」 に従って、実物のソースを読んでみましょう。もっとも単純な実装の一つとして、元祖Arduinoで使われている、AVRマイコン向けのmallocの実装を見てみます。
mallocの基本的な動作
mallocは、ヒープ領域(動的に確保されるメモリのための領域)から、要求されたサイズのメモリブロックを割り当てます。
ヒープの空き領域は、リスト構造で管理されています。mallocはこのリストを走査し、要求されたサイズが収まる空きメモリブロックを見つけて、そこに割り当てを行い、余った分は新たな空きブロックとしてリストに追加します。
かなり端折って説明しましたが、実際は、最適な空き領域を見つけるための賢い実装がされています。詳細はソースコードを参照してください。
freeの基本的な動作
freeは、mallocで確保されたメモリブロックを解放します。解放されたブロックは再びヒープの空き領域としてリストに追加されます。
また、隣接する空きブロックがあれば、それらを結合して一つの大きな空きブロックにします。
それでなぜ嫌われるの?
メモリ断片化
動的にメモリを確保・解放することで、ヒープ領域が細かく分割され、使用可能な大きな連続したメモリブロックが不足することがあります。
実例を見てみましょう。元祖Arduino Duemilanove(ATmega328P搭載)は、2KBのSRAMを持ち、スタックやその他の領域を差し引いて、最大で約1800バイトがヒープとして利用できます。
まず、以下のコードは正常に動作します。
// 1600バイトのメモリを確保
void* a = malloc( 1600 );
Serial.print( "a = " );
Serial.println( (uint16_t)a );
出力:
a = 512
1600バイトのメモリ確保に成功しました。SRAMの大部分を使っていますが、問題ありません。
次に、以下のコードを実行してみます。
// 1000バイトのメモリを確保
void* b = malloc( 1000 );
Serial.print( "b = " );
Serial.println( (uint16_t)b );
// 1バイトのメモリを確保
void* c = malloc( 1 );
Serial.print( "c = " );
Serial.println( (uint16_t)c );
// 最初に確保したメモリ(1000バイト)を解放
free( b );
// 1200バイトのメモリを確保
void* d = malloc( 1200 );
Serial.print( "d = " );
Serial.println( (uint16_t)d );
出力:
b = 512
c = 1514
d = 0
実質1バイトしか使っていないのに、1200バイトのメモリ確保に失敗してしまいます。これは、ヒープ領域が以下のように断片化されているためです。
+-------------------+
| |
| 1000バイト空き |
|(bに割り当てた領域)|
| |
+-------------------+
| 1バイト使用中 |
|(cに割り当てた領域)|
+-------------------+
| |
| 残り空き領域 |
| 約800バイト |
| |
+-------------------+
ご覧の通り、1200バイトの連続した空き領域が存在しないため、malloc(1200)は失敗します。
このような単純なケースであれば、対処は容易ですが、実際のアプリケーションでは、メモリの確保と解放が複雑に入り組むため、予測不可能なメモリ断片化が発生します。
昨今のリッチな環境では、仮想メモリやメモリコンパクションがあるため、あまり問題になりませんが、限りあるリソースの中で動作する組み込みシステムでは、メモリ断片化は深刻な問題となります。
予測不可能な動作時間
先に説明したように、mallocやfreeはヒープを走査したり、隣接ブロックの結合を行ったりするため、ヒープの状態によって動作時間が大きく変動します。リアルタイムシステムでは、処理が一定時間内に完了することが求められるため、予測不可能な動作時間は許容されません。
メモリリーク
mallocで確保したメモリをうっかりfreeし忘れてしまうと、いわゆるメモリリークが発生します。これはC言語の文法的にはエラーにならず、静的解析ツールでの検出も難しく、小メモリで長時間稼働する組み込みシステムでは致命的な事態を招くことがあります。
代替手法
動的メモリ確保の問題を回避するために、以下のような手法がよく用いられます。
静的メモリ確保
プログラムのコンパイル時に必要なメモリを確保する手法です。C言語ではstaticキーワードやグローバル変数を使用して実現します。
一括メモリ確保
プログラムの起動時(Arduinoのsetup()関数内など)に必要なメモリを一度に確保し、その後はmallocやfreeを使用しない手法です。これにより、予測不可能なメモリ断片化や動作時間の変動を回避できます。
プール型メモリ管理
簡単に言うと、自前でメモリを管理する手法です。面倒くさいですが、自分で動作を保証できる強みがあります。
まとめ
ジリ貧リソースな組み込みシステムの開発をしていると、こういったトラブルを偶に見かけるのですが、意外と、指摘しても判ってもらえない!?というわけで、記事にしてみました。
動的メモリ確保は便利ですが、安定した動作が求められる組み込みシステムやリアルタイムシステムでは、メモリ断片化、予測不可能な動作時間、メモリリークなどの問題が発生しやすいため、避けられることが多いです。代替手法を検討し、システムの要件に最適なメモリ管理方法を選択することが重要です。