特に新しいネタではないです。
最初ファイバーについて書こうかなと思ったんですが、特筆すべき点が中々見つからなくてテーマを変えました。
去年のAdvent Calendarに引き続き、ゲーム作る際に必要になるかもしれないGCとメモリに関すること第二弾ということで一つ…
#カスタムアロケータとは
D言語のクラスはJavaやC#に類似しており、インスタンスは基本的にGC(ガベージコレクタ)によって管理されます。
しかしC++でnew,deleteをオーバーロードするように、D言語でもカスタムアロケータ/デアロケータを記述することによってプログラマがインスタンスのメモリを管理することが可能です。
class Foo
{
new(size_t size) {
return std.c.stdlib.malloc(size);
}
delete(void* p) {
if (p) {
std.c.stdlib.free(p);
}
}
}
#なぜ自分でメモリ管理するのか
まずD言語にはGCがありますので、自分でメモリを管理する必要がありませんし推奨もされていません。
しかし高度なリアルタイム性が求められる分野で使う際にGCやnewのオーバーヘッドが問題になることがあるかもしれません。
そういったとき、あらかじめメモリを確保しておいてそれを使いまわすメモリプールという手法があります。
#メモリプールの実装例
import core.exception, std.stdio;
class MemoryPool
{
// メモリブロック管理用
align(8) struct Block {
void delegate(void*) deallocate;
Block* next;
}
ubyte[] memory; // 確保されたメモリ
Block* topBlock; // プールされているメモリブロックの先頭
size_t blockSize; // ブロックサイズ
int restCount; // 残りのブロック数
this(size_t blockSize, int count) {
// 管理領域を含めてメモリを一括確保
size_t realSize = blockSize + Block.sizeof;
this.memory = new ubyte[realSize * count];
ubyte* p = this.memory.ptr;
Block* nextBlock;
for (int i = 0; i < count; i++) {
Block* block = cast(Block*)p; // メモリブロックを切り出す
block.next = nextBlock; // 次のブロックを連結
block.deallocate = &deallocate; // 解放メソッドをセット
nextBlock = block;
p += realSize;
}
this.topBlock = nextBlock;
this.blockSize = blockSize;
this.restCount = count;
}
void* allocate(size_t size) {
if (this.topBlock && size < this.blockSize) {
Block* block = this.topBlock;
this.topBlock = block.next;
this.restCount--;
// アドレスをシフトして使用可能領域を返す
return cast(ubyte*)block + Block.sizeof;
}
return null;
}
void deallocate(void* p) {
// アドレスをシフトして管理領域を取得
Block* block = cast(Block*)(cast(ubyte*)p - Block.sizeof);
if (this.topBlock) {
block.next = this.topBlock;
this.topBlock = block;
} else {
block.next = null;
this.topBlock = block;
}
this.restCount++;
}
static void deallocateS(void* p) {
// アドレスをシフトして管理領域を取得
Block* block = cast(Block*)(cast(ubyte*)p - Block.sizeof);
block.deallocate(p);
}
}
↑ごちゃっと書かれていますけど、
指定されたサイズのメモリブロックを任意の数分確保してプールしておき、必要になときに取り出せるようにしています。
それを使い、クラスのカスタムアロケータから確保するようにしたのが↓です。
import std.stdio;
import memorypool;
class A
{
int value = 6;
this() {
writeln("A.ctor()");
}
~this() {
writeln("A.dtor()");
}
void say(string msg) {
writefln("%s value=%d", msg, this.value);
}
new(size_t size, MemoryPool memoryPool) {
writefln("A.new(%d)", size, memoryPool);
void* p = memoryPool.allocate(size);
if (p == null) {
// 失敗してもnullを返してはダメ
throw new OutOfMemoryError;
}
return p;
}
delete(void* p) {
writefln("A.delete(0x%08x)", p);
if (p) { // nullの場合があるらしい?
MemoryPool.deallocateS(p);
}
}
}
void main()
{
// 64byteのメモリを16個持つメモリプールを作成
auto memoryPool = new MemoryPool(64, 16);
// メモリプール使ってインスタンスを作成
A a = new(memoryPool) A;
// 適当に使う
a.say("Hello");
// インスタンスを削除(使用していたメモリはメモリプールに戻る)
delete a;
}
A.new(12)
A.ctor()
Hello value=6
A.dtor()
A.delete(0x00443cc0)
#↑のコードの問題
上記のアロケータから作成したインスタンスはちゃんとdeleteしないとメモリプール内でメモリリークします。
MemoryPoolが大元のメモリを握っているためGCが働かないのです。
もちろんMemoryPoolがGCによって解放されれば、プールされているメモリは全部回収されます。
実際使う際はC++のようにスマートポインタで運用するのが良いかもしれないですね。
#以上
普段の方法とは別に、こんな方法でもインスタンスを作成できるよという紹介でした。
↓では他にも何種類かのメモリ確保方法が載っています。D言語の自由度の高さがお分かりいただけるかと。
メモリ管理 - プログラミング言語D