C言語

メモリプールと動的メモリ確保の違い

はじめに

メモリプールと動的メモリ確保、どんな違いがあり、どのように使い分ければ良いのでしょうか。
最近、このような質問をいただいたため、本記事で簡単にまとめようと思います。

 

メモリプール(Memory Pool)

Memory Pool のイメージ

概要

メモリプールは、ある一定サイズのメモリブロック(Memory Block)をあらかじめ確保し、
プログラムが必要なときに「確保」の要求を受け、 Chunk を払い出す構造です。

 

プログラムが不要になったときにプールへ返却し、次の要求があった際に Chunk を再利用します。
メモリプールはメモリのフラグメンテーションを軽減し、効率的なメモリ割り当てを提供できます。

SANACHAN
SANACHAN
特に、MMU を搭載していない組込みシステムや、
uITRON などのリアルタイムシステムに適しています。

MMU については、「【図解】いまさら人には聞けないMMUの仕組み」で解説しています。

 

特徴

  • メモリブロックのサイズ、および Chunk のサイズは固定長
  • Chunk サイズで小分けされた領域を切り売り・再利用
  • メモリ確保要求、プール返却に要する処理時間が一定

 

動的メモリ確保(Dynamic Memory Allocation)

Dynamic Memory Allocation のイメージ

概要

動的メモリ確保は、プログラムが実行時に必要となるメモリを必要なサイズで動的確保できる仕組みです。
C言語では malloc(3)/free(3)、C++ では new/delete 演算オペレータなどが該当します。

malloc については、「mallocの動的メモリ管理構造」で詳しく解説しています。

 

動的メモリ確保では、さまざまなサイズのメモリ確保、メモリ解放を行うため、
イメージで示している通り、歯抜けの状態、つまりフラグメンテーションが発生します

これにより、次の「確保」要求で空き領域を探索する時間がメモリプールに比べて余分にかかるため、
動的メモリ確保では、処理時間が一定にはなりません

SANACHAN
SANACHAN
一般的にこれを「オーバーヘッド」と呼んでいます。

 

特徴

  • プログラムの実行時に必要な任意のサイズのメモリを動的に確保可能
  • メモリのフラグメンテーションが発生するリスクあり
  • メモリ確保にかかる処理時間は一定ではない(解放はほぼ一定)

 

比較と相違点

①最初に確保するメモリ

メモリプールでは、Memory Block を最初に確保する必要があります。

uITRON のような MMU を備えていない場合は、外部変数として実アドレスの領域を割り当て、
Linux のように MMU を備えている場合は、mmap(2) などで初期化時に確保するのが一般的です。
Chunk サイズが固定のため、確保する総容量は単純な掛け算で算出できます。

 

また、動的メモリ確保を行う malloc(3) も一定サイズのメモリを確保して切り売りします。
sbrk(2) システムコールを呼び出し、Heap 領域からメモリを確保しています。
確保したメモリが足りなくなれば、再度 sbrk(2) を呼び出して拡張します。

 

一定サイズのメモリを確保して切り売りする仕組みは、どちらも大差ありません。
そのため、メモリプールは「固定長メモリ確保」、動的メモリ確保は「可変長メモリ確保」と
呼び分けるのが正しいかもしれません。

 

②確保・解放にかかる処理時間

メモリプールでは、各 Chunk の先頭アドレスをリストで繋げて管理することが多いため、
メモリ確保・解放の要求に応じる処理時間が毎回同じになります。

ただし、複数スレッドからアクセスがある場合で、mutex などで排他してしまうと、
競合した場合に待ち時間が発生し、その時間が加算されます。

SANACHAN
SANACHAN
malloc(3)はスレッドセーフな関数ですが、クリティカルな条件時以外は
mutex を使用していません。

 

一方、動的メモリ確保を行う malloc(3) は、可変サイズのメモリを切り売りするため、
管理が複雑になっているため、メモリプールに比べて処理時間は多くかかります。

malloc のメモリ管理構造でも解説したとおり、使用効率や処理性能を確保するための
さまざまな仕掛けが施されているため、mmap(2) を使用する場合を除き、
メモリプールと大差ない性能を出せるようになっています。

SANACHAN
SANACHAN
実際に、計ってみましょう。

 

[おまけ] 処理性能

ここまでお読みいただいた方は、一つ気になることがあるのではないでしょうか。
メモリプールと malloc(3) の処理時間の差です。

ここでは、256 [byte] のメモリを 100万回確保、解放するのにかかる時間を計測し、
比較してみたいと思います。

SANACHAN
SANACHAN
Win10, Core i5-11400 上で動く VM (Ubuntu22) 内で実行しています。

固定長メモリプール

固定長メモリプールの実装」を改造して計測します。
uITRON などの MMU を搭載していないシステムを想定、スレッドセーフではありません。

動的メモリ確保(malloc)

次に、malloc(3) を使用し、メモリプールと同じように確保・解放する処理を記述します。

計測結果

 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) の時間を除くと、もっと差が縮まるものと思います。

SANACHAN
SANACHAN
いかに動的メモリ確保の malloc(3) が最適化されているのかが分かります。

このオーバーヘッドが気になるようなシステムでしたら、メモリプールを採用し、
malloc(3) や C++ は使用しないでコーディングした方が良いでしょう。

 

さいごに

いかがでしたでしょうか。
昨今ではメモリプールを使うケースが減ってきたように思いますが、

  • 確保・解放ともに毎回一定の処理時間になる(スレッドセーフに実装する場合を除く)
  • メモリ使用量を自分で制限・管理できる

というメリットが得られることから、リアルタイム性や厳格なメモリ管理が求められる
組込みソフトウェアでは、採用されるケースもあります。

メモリプール、動的メモリ確保の仕組みについて、少しでも伝わっていれば幸いです。

 

こちらの記事もよく読まれています

  • この記事を書いた人
  • 最新記事
SANACHAN

SANACHAN

「生涯一エンジニア」を掲げ、大手グローバル企業でSE/PGとして8年勤め、キャリアアップ転職した現役のエンジニアです。世にあるメジャーな全プログラム言語(コボル除く)を自由に扱えます。一児の父。自分のため、家族のため、日々勉強してます。システムエンジニア、プログラミングに関する情報を蓄積している雑記帳です。

-C言語
-, ,