組み込みソフトウェアのメモリアロケータ
組み込みソフトウェアの設計ルールは会社組織やプロジェクトによって様々だが、 メモリアロケータに関して使用禁止、もしくはプログラムの初期化以後は使用禁止という場合が多いと思う。 これはOSレスやuITRON系のような小~中規模システムだけでなく、 大規模システム向けのOSを採用している場合でも同様ではないだろうか。
(とても古い環境やごく小規模な環境ではmalloc/freeが提供されない場合もある)
使用禁止の理由はもちろんメモリの断片化による問題を引き起こさせないためだが、 使用禁止されている場合はオペークポインタでのカプセル化が難しくなることや、 設計によっては初期化時に限定されているかのチェックも煩雑なので、 ルールを守りつつ弊害が出ない対応を考えてみよう。
そもそも通常のメモリアロケータが使用禁止なのだから、もちろんメモリブロックの解放も不要となるため、 メモリブロック解放の実装が無い単純なアロケータを作成するとどうだろうか?
単純なアロケータ仕様
- メモリアロケータで管理するメモリプールの先頭アドレスとサイズを指定可能とする
- アドレス指定の理由は、プログラムコードとは物理的に異なるメモリデバイスを使用する場合を考慮するため(linker設定を使用しない)
- アドレス指定なしの場合はstaticに確保する
- 獲得するメモリブロックのアラインを静的に指定可能とする
- 解放がないので管理領域を設けずに無駄なく割り当てる
実装
allocator_cfg.h等の専用の設定ファイルを作成し、コンフィギュレーションのデフォルト設定を記述する。
//#define ALLOCATOR_MEMORY_POOL_BASE 0x00000000UL enum { kALLOCATOR_SIZE_MAX = (1024 * 16) }; static const uintptr_t kALIGNMENT_UNIT = (1 << 3); /*!< power-of-two */
メモリ領域
#if defined(ALLOCATOR_MEMORY_POOL_BASE) static uint8_t* const memoryPool_ = (uint8_t*)ALLOCATOR_MEMORY_POOL_BASE; #else static uint8_t memoryPool_[kALLOCATOR_SIZE_MAX]; #endif static uint8_t* next_ = 0; static uint8_t* end_ = 0;
アラインされたアドレスを得る
なぜアラインされたアドレスが必要なのだろうか?
例えば32bitデータアクセスをする場合、32bitアラインでなければ通常はアドレスの異常としてCPU例外となってしまう。 同じように16bitデータアクセスなら、16bitアラインでなければならない。 8bitデータアクセスはどうかというと、CPUがバイトマシーンならどのようなアドレスでもバイト単位となるので問題ない。
x86系CPUのデータアクセス
x86系CPUの場合は特殊で、アライメントされていない場合は複数のメモリアクセスに分割されてしまうため、 CPUのアドレス例外となるようなことはないが、分割された分はパフォーマンスが落ちる。 このため、x86系CPUでもアライメントされたアドレスでアクセスする方がよいだろう。
ところがメモリブロックを獲得する際は、呼び出し側の必要最小アライメントを知ることが出来ないので、 最大のアライメントで返す必要がある(アライメントサイズを渡せるようにしてもいいが、変更が必要になった場合の対応が煩雑なので)。
このため、以下のようなアラインされたアドレスを得る関数を作成して対応する。 この関数は与えられたアドレスに対して、アドレスの大きい方へ指定アライメントの調整をするようになっている。
static uintptr_t next_aligned_address(const uintptr_t addr) { return ((addr + (kALIGNMENT_UNIT - 1)) & ~(kALIGNMENT_UNIT - 1)); }
初期化
memoryPool_のアドレスがアライメントされている保証がないため、 memoryPool_先頭のアライメントアドレスをnext_に設定している。 end_はmemoryPool_の有効な領域の次のアドレスを設定している。
void Allocator_initialize(void) { next_ = (uint8_t*)next_aligned_address((uintptr_t)&memoryPool_[0]); end_ = &memoryPool_[kALLOCATOR_SIZE_MAX]; }
メモリブロック獲得
メモリブロックの獲得は単純で先頭から順番に割り当てていくだけとなる。
void* Allocator_allocate(const size_t size) { if (((uintptr_t)next_ + size) > (uintptr_t)end_) { return NULL; } void* const ptr = next_; next_ = (uint8_t*)next_aligned_address((uintptr_t)next_ + size); return ptr; }
メモリブロック解放
メモリブロックの解放はサポートしていないので、 意図せずfreeを使ってしまっているコードの流入を防ぐ意味から以下の関数で置き換えます。
void Allocator_deallocate(void* const ptr) { (void)ptr; /* ptr未使用のwarningを抑制する */ for (;;) {} /*!< dynamic stop(最適化の影響を考慮するとvolatile変数を使用した無限ループにする必要があるかもしれない) */ }
使い方
struct SimpleLed* const led = (struct SimpleLed*)Allocator_allocate(sizeof(struct SimpleLed));
SimpleLed* const led = new(Allocator_allocate(sizeof(SimpleLed))) SimpleLed(kBASE_ADDR, initial_value);
まとめ
単純なアロケータなので改善の余地は沢山あります。 プログラムで必要なメモリ量を計算させたり、 様々な環境で利用するためにメモリアロケータの実装を差し替えられる仕組みにしたり、 色々と機能追加してみると面白いかもしれません。