はじめに
メモリプールと動的メモリ確保、どんな違いがあり、どのように使い分ければ良いのでしょうか。
最近、このような質問をいただいたため、本記事で簡単にまとめようと思います。
メモリプール(Memory Pool)
概要
メモリプールは、ある一定サイズのメモリブロック(Memory Block)をあらかじめ確保し、
プログラムが必要なときに「確保」の要求を受け、 Chunk を払い出す構造です。
プログラムが不要になったときにプールへ返却し、次の要求があった際に Chunk を再利用します。
メモリプールはメモリのフラグメンテーションを軽減し、効率的なメモリ割り当てを提供できます。
uITRON などのリアルタイムシステムに適しています。
MMU については、「【図解】いまさら人には聞けないMMUの仕組み」で解説しています。
特徴
- メモリブロックのサイズ、および Chunk のサイズは固定長
- Chunk サイズで小分けされた領域を切り売り・再利用
- メモリ確保要求、プール返却に要する処理時間が一定
動的メモリ確保(Dynamic Memory Allocation)
概要
動的メモリ確保は、プログラムが実行時に必要となるメモリを必要なサイズで動的確保できる仕組みです。
C言語では malloc(3)/free(3)、C++ では new/delete 演算オペレータなどが該当します。
malloc については、「mallocの動的メモリ管理構造」で詳しく解説しています。
動的メモリ確保では、さまざまなサイズのメモリ確保、メモリ解放を行うため、
イメージで示している通り、歯抜けの状態、つまりフラグメンテーションが発生します。
これにより、次の「確保」要求で空き領域を探索する時間がメモリプールに比べて余分にかかるため、
動的メモリ確保では、処理時間が一定にはなりません。
特徴
- プログラムの実行時に必要な任意のサイズのメモリを動的に確保可能
- メモリのフラグメンテーションが発生するリスクあり
- メモリ確保にかかる処理時間は一定ではない(解放はほぼ一定)
比較と相違点
①最初に確保するメモリ
メモリプールでは、Memory Block を最初に確保する必要があります。
uITRON のような MMU を備えていない場合は、外部変数として実アドレスの領域を割り当て、
Linux のように MMU を備えている場合は、mmap(2) などで初期化時に確保するのが一般的です。
Chunk サイズが固定のため、確保する総容量は単純な掛け算で算出できます。
また、動的メモリ確保を行う malloc(3) も一定サイズのメモリを確保して切り売りします。
sbrk(2) システムコールを呼び出し、Heap 領域からメモリを確保しています。
確保したメモリが足りなくなれば、再度 sbrk(2) を呼び出して拡張します。
一定サイズのメモリを確保して切り売りする仕組みは、どちらも大差ありません。
そのため、メモリプールは「固定長メモリ確保」、動的メモリ確保は「可変長メモリ確保」と
呼び分けるのが正しいかもしれません。
②確保・解放にかかる処理時間
メモリプールでは、各 Chunk の先頭アドレスをリストで繋げて管理することが多いため、
メモリ確保・解放の要求に応じる処理時間が毎回同じになります。
ただし、複数スレッドからアクセスがある場合で、mutex などで排他してしまうと、
競合した場合に待ち時間が発生し、その時間が加算されます。
mutex を使用していません。
一方、動的メモリ確保を行う malloc(3) は、可変サイズのメモリを切り売りするため、
管理が複雑になっているため、メモリプールに比べて処理時間は多くかかります。
malloc のメモリ管理構造でも解説したとおり、使用効率や処理性能を確保するための
さまざまな仕掛けが施されているため、mmap(2) を使用する場合を除き、
メモリプールと大差ない性能を出せるようになっています。
[おまけ] 処理性能
ここまでお読みいただいた方は、一つ気になることがあるのではないでしょうか。
メモリプールと malloc(3) の処理時間の差です。
ここでは、256 [byte] のメモリを 100万回確保、解放するのにかかる時間を計測し、
比較してみたいと思います。
固定長メモリプール
「固定長メモリプールの実装」を改造して計測します。
uITRON などの MMU を搭載していないシステムを想定、スレッドセーフではありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
#include <stdio.h> #include <stddef.h> #include <time.h> // 1回で確保するメモリサイズ #define ALLOC_SIZE (256) // 100万回 #define ALLOC_NUM (1000000) union max_align_type { long long a; double b; long double c; void *d; }; const size_t max_align_size = offsetof(struct { char a; union max_align_type b; }, b); union chunk { union max_align_type aligner; char array[ALLOC_SIZE]; union chunk *next; }; union chunk memory[ALLOC_NUM]; union chunk *top; // 固定長メモリプールの初期設定 void fmp_setup(void) { clock_t begin, end; begin = clock(); const size_t n = sizeof(memory) / sizeof(memory[0]); for (size_t i = 0; i < n - 1; i++) memory[i].next = &memory[i + 1]; memory[n - 1].next = NULL; top = &memory[0]; end = clock(); printf("Execution Time: %s() : %lf [s]\n", __func__, difftime(end, begin) / CLOCKS_PER_SEC); } // 固定長メモリプールから Chunk を割り付ける。 void *fmp_alloc(void) { if (top == NULL) return NULL; void *r = top->array; top = top->next; return r; } // 固定長メモリプールから割り付けた Chunk を解放する。 void fmp_free(void *p) { if (p == NULL) return; ((union chunk *)p)->next = top; top = p; } int main(void) { clock_t begin, end; void *array[ALLOC_NUM]; fmp_setup(); begin = clock(); // メモリ確保 for (int i = 0; i < ALLOC_NUM; i++) { void *p = fmp_alloc(); array[i] = p; } // メモリ解放 for (int i = 0; i < ALLOC_NUM; i++) { void *p = array[i]; fmp_free(p); } end = clock(); printf("Execution Time: %s() : %lf [s]\n", __func__, difftime(end, begin) / CLOCKS_PER_SEC); return 0; } |
動的メモリ確保(malloc)
次に、malloc(3) を使用し、メモリプールと同じように確保・解放する処理を記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <stdio.h> #include <stdlib.h> #include <stddef.h> #include <time.h> // 1回で確保するメモリサイズ #define ALLOC_SIZE (256) // 100万回 #define ALLOC_NUM (1000000) int main(void) { clock_t begin, end; void *array[ALLOC_NUM]; begin = clock(); // メモリ確保 for (int i = 0; i < ALLOC_NUM; i++) { void *p = malloc(ALLOC_SIZE); array[i] = p; } // メモリ解放 for (int i = 0; i < ALLOC_NUM; i++) { void *p = array[i]; free(p); } end = clock(); printf("Execution Time: %s() : %lf [s]\n", __func__, difftime(end, begin) / CLOCKS_PER_SEC); return 0; } |
計測結果
command
$ gcc -g -o mempool -O0 -Wall -Werror fixed_memory.c
$ gcc -g -o malloc -O0 -Wall -Werror malloc.c
$ ./mempool
Execution Time: fmp_setup() : 0.049662 [s]
Execution Time: main() : 0.041507 [s]
$ ./malloc
Execution Time: main() : 0.090232 [s]
結果の考察
- メモリプールの実装では、管理情報の初期化に 49 [ms] かかる
- malloc(3) 内部の sbrk(2) 呼び出しにかかる時間は計測できていないが、
計測結果の時間には含まれている - 100万回のメモリ確保・解放にかかる時間より、1回のメモリ確保・解放にかかる時間は
MemPool=0.41 [us]、Malloc=0.90 [us] である
処理するマイコン(CPU)の性能にもよると思いますが、100万回実施してこの程度のオーバーヘッドです。
フラグメントした場合はもう少し時間がかかるかと思いますが、
sbrk(2) の時間を除くと、もっと差が縮まるものと思います。
このオーバーヘッドが気になるようなシステムでしたら、メモリプールを採用し、
malloc(3) や C++ は使用しないでコーディングした方が良いでしょう。
さいごに
いかがでしたでしょうか。
昨今ではメモリプールを使うケースが減ってきたように思いますが、
- 確保・解放ともに毎回一定の処理時間になる(スレッドセーフに実装する場合を除く)
- メモリ使用量を自分で制限・管理できる
というメリットが得られることから、リアルタイム性や厳格なメモリ管理が求められる
組込みソフトウェアでは、採用されるケースもあります。
メモリプール、動的メモリ確保の仕組みについて、少しでも伝わっていれば幸いです。