home link download back number special issue

HMDT - Special Issue / CoreFoundation の秘密 / Base Services - 参照回数によるメモリ管理


- Base Services -
参照回数によるメモリ管理

retain と release

CoreFoundation では参照回数をベースにしたメモリ管理を行っているんだ。えっ?参照回数ってのは何かって?簡単に説明しよう。

参照回数は、メモリの管理の手法の 1 つだ。ほら、プログラミングってのは、つまるところメモリの確保と解放の繰り返しでしょ。メモリを確保して、必要なデータを突っ込んで、それを処理して、解放する。最も基本的な動作だけど、多くの陥りやすい問題が潜んでいるんだ。メモリを確保して解放するのを忘れたり(メモリリーク)、解放したものをまた解放しちゃったり(メモリの二重解放)。

この問題を解決するには、メモリ管理のポリシーをしっかりしておかなくてはいけない。それには、確保したメモリを「誰が」解放するか、というのがポイントだ。確保した人が解放する、っていうのが一番シンプルだけど、これ、なかなかうまくいかないんだよね。なぜなら、確保した人が、メモリがいつ解放できるか決められるとは限らないから。他にも、ガベージコレクションを使うというのもあるけど、システムの負荷が高くなる。

CoreFoundation(および Cocoa)が採用している手法は、「メモリを使い終わった最後の人が解放する」っていうモデルだ。それを実現するために参照回数を使っているんだぜ。すべてのオブジェクト(つまり確保されたメモリ)には、参照回数がくっついているんだ。オブジェクトが確保されたときは、参照回数は 1 だ。そして、そのオブジェクトが必要な人は、参照回数を 1 上げる(retain する)。いらなくなったら、参照回数を 1 下げる(release する)。参照回数が 0 になったら、そのオブジェクトを削除する。こういう仕組みだ!

感覚としては、よく C++ で使われるスマートポイントを用いたメモリ管理に近いかな。あっちは暗黙的に行われるけど、こっちは参照するのを明示的に呼ぶ必要がある。明示的に呼ぶ方が、プログラマから制御しやすいという利点がある。まぁ、どっちがいいかは好みでしょう。

Objective-C でのメモリ管理は Nito's Page の Cocoa 倶楽部が大変詳しいので、正確な情報はそちらを参照するといいです。

Cocoa 倶楽部 (http://wwwa.dcns.ne.jp/~nito/CocoaClub)

参照回数の初期化 (allocate)

では、CoreFoundation の retain、release の仕組みを見てみよう!まず、参照回数がどこに保持されているかだけど、前の記事で見たように、各オブジェクトが持っている CFRuntimeBase の中の _rc フィールドだ。

CoreFoundation/Base.subproj/CFRuntime.h
typedef struct __CFRuntimeBase {
    ...
    uint16_t _rc;
    ...
} CFRuntimeBase;

この _rc がが参照回数の実体ね。ここでは 16 bit で定義されていることに注意。

続いて、オブジェクトを確保するところ。ここで参照回数の初期値を設定する。

CoreFoundation/Base.subproj/CFRuntime.c
CFTypeRef _CFRuntimeCreateInstance(
                CFAllocatorRef allocator, 
                CFTypeID typeID, 
                uint32_t extraBytes, 
                unsigned char *category) 
{
    CFRuntimeBase *memory;
    ...

    /* メモリ確保 */
    memory = CFAllocatorAllocate(allocator, size, 0);
    if (NULL == memory) {
        return NULL;
    }
    ...

    /* CFRuntimeBase のフィールド設定 */
    memory->_isa = __CFISAForTypeID(typeID);
    memory->_rc = 1;
    memory->_info = 0;
    __CFBitfieldSetValue(memory->_info, 15, 8, typeID);
    ...

    return memory;
}

こんな感じだ。参照回数は 1 に設定されてるね。

参照回数を上げる (retain)

続いて retain。参照回数を上げる操作だ。

CoreFoundation/Base.subproj/CFRuntime.c
CFTypeRef CFRetain(CFTypeRef cf) {
    CFIndex highBits = 0, lowBits = 0;
    ...

    /* 参照回数の取得 */
    lowBits = ((CFRuntimeBase *)cf)->_rc;
    if (0 == lowBits) {	// Constant CFTypeRef
        ...
        return cf;
    }
    /* 参照回数を上げる */
    lowBits++;
    /* 16 bit 目のチェック */
    if ((lowBits & 0x07fff) == 0) {
        /* 外部にあるはずの highBits を取得 */
        // Roll over another bit to the external ref count
        highBits = (CFIndex)CFDictionaryGetValue(
                        __CFRuntimeExternRefCountTable, 
                        cf); // NULL return -> 0 ref count
        /* highBits を上げる */
        highBits++;
        CFDictionarySetValue(
                        __CFRuntimeExternRefCountTable, 
                        cf, 
                        (void *)highBits);
        /* lowBits を 0x8000 に設定 */
        lowBits = 0x8000; // Bit 16 indicates external ref count
    }
    /* 参照回数をメモリに設定 */
    ((CFRuntimeBase *)cf)->_rc = lowBits;
    ...

    return cf;
}

こんなわけで、CFRuntimeBase の _rc フィールドは 16 bit だけど、参照回数自体は 32 bit としてあつかっていることが分かるんだ。カラクリはどうなっているかというと、参照回数の 32 bit を _rc の lowBits (16 bit) と、外部にある highBIts (16 bit) に分ける。_lowBits を上げていって、0x8000 になったら外部に highBits を確保する。lowBits の 16 bit 目は highBits があるかどうかの判定に使われるんだ。だからビットフィールドの使われ方はこうなる。

Definition of reference count

だから実際に参照回数として使われるのは 31 bit だね。仮に 16 bit だとすると、参照回数の最大値は 65535。これだと足りなくなることもあるか。でも、各オブジェクトに 32 bit 持たせたりすると、無駄なメモリ領域ができることになってしまう。というわけで、上位 16 bit を外部に持たせているようだ。それともただ単に、初め 16 bit にしておいて、後で足りなくなっちゃったー、ということで拡張しただけかも。

参照回数を下げる (release)

上げたら下げなくてはいけない。というわけで、次は release するときのコード。

CoreFoundation/Base.subproj/CFRuntime.c
void CFRelease(CFTypeRef cf) {
    CFIndex highBits = 0, lowBits = 0;
    ...

    /* 参照回数の取得 */
    lowBits = ((CFRuntimeBase *)cf)->_rc;
    if (0 == lowBits) {	// Constant CFTypeRef
        ...
        return;
    }
    if (1 == lowBits) {
        if (__kCFAllocatorTypeID_CONST == __CFGenericTypeID(cf)) {
            /* オブジェクトを削除 */
            __CFAllocatorDeallocate((void *)cf);
        } else {
            CFAllocatorRef allocator;
            if (NULL != __CFRuntimeClassTable[__CFGenericTypeID(cf)]->finalize) {
                /* 各オブジェクトの finalize() を呼び出す */
                __CFRuntimeClassTable[__CFGenericTypeID(cf)]->finalize(cf);
            }
            ...

            CFAllocatorDeallocate(allocator, (void *)cf);
            ...
        }
    } else {
        if (0x8000 == lowBits) {
            /* highBits があるときの処理 */
            // Time to remove a bit from the external ref count
            highBits = (CFIndex)CFDictionaryGetValue(
                            __CFRuntimeExternRefCountTable, 
                            cf); // NULL return -> 0 ref count
            /* highBits を下げる */
            highBits--;
            if (0 == highBits) {
                /* highBits を削除する */
                CFDictionaryRemoveValue(
                                __CFRuntimeExternRefCountTable, 
                                cf);
                lowBits = 0x07fff;
            } else {
                CFDictionarySetValue(
                                __CFRuntimeExternRefCountTable, 
                                cf, 
                                (void *)highBits);
                lowBits = 0x0ffff;
            }
        } else {
            /* 参照回数を下げる */
            lowBits--;
        }
        /* 参照回数をメモリに設定 */
        ((CFRuntimeBase *)cf)->_rc = lowBits;
    }
}

こんな感じ。参照回数が 1 のときはオブジェクトを削除する。オブジェクトに finalize() があれば呼び出す。これはデストラクタみたいなものだね。そして CFAllocatorDeallocate() を呼び出してオブジェクトを削除する。

1 より大きいときは、参照回数を下げる。このとき、highBits を使っているかどうかで処理が変わるんだ。どっちにしても 1 つ減らして、処理はおしまい。

こんな感じで retain と release は実装されている。メモリ管理のポリシーはしっかりしているので、守っていればメモリの問題は起きないはずだ。ちなみに、Cocoa にはある autorelease は?どうも、ないみたい。autorelease 使えないので注意。


Home | Link | Download | Back Number | Speciall Issue

mailto: mkino@xd5.so-net.ne.jp

HMDT