今回は、FlyweightパターンになるCocoaの実装を探ってみよう。

Flyweightと言ってパッと思いつくのは、NSNumberクラスだ。NSNumberは、数値や論理値といったプリミティブ型をラップするためのクラスである。数値の場合、同じ値を持つオブジェクトを繰り返し使うことになるので、Flyweightとは相性がいいだろう。

このクラスの実装を調べてみよう。

現在の実装での実験

手始めに、現在どういう実装が行われているのかを知るために、実験をしてみよう。

まずは、整数を指定してNSNumberを作ってみる。

List 1.

NSNumber*   number0 = [NSNumber numberWithInt:0];
NSNumber*   number1 = [NSNumber numberWithInt:0];

NSLog(@"number0 0x%x", number0);
NSLog(@"number1 0x%x", number1);

値に0を指定して、2つのNSNumberのインスタンスを作り、そのアドレスを表示させてみた。実行結果は、次のようになる。

number0 0x303750
number1 0x303750

number0とnumber1は、同じインスタンスになっていることが確認できた。Flyweightパターンになっている。

次に、論理値型を試してみよう。

List 2.

NSNumber*   number0 = [NSNumber numberWithBool:YES];
NSNumber*   number1 = [NSNumber numberWithBool:YES];
NSNumber*   number2 = [NSNumber numberWithInt:1];

NSLog(@"number0 0x%x", number0);
NSLog(@"number1 0x%x", number1);
NSLog(@"number2 0x%x", number2);

論理値YESを指定して、インスタンスを2つ作る。また、YESは数値1として定義されているので、整数の1を使ったインスンタスも作って比較してみよう。結果は、次のようになる。

number0 0xa080a88c
number1 0xa080a88c
number2 0x303750

論理値YESを指定した2つは、同じインスタンスとなり、Flyweightパターンになっている。このアドレスを見ると、どうやらこの場でインスタンスが作られたのではなく、あらかじめ存在していたようだ。そして、整数1を指定したものは、別のインスタンスになっている。

次は、小数を使ってみよう。

List 3.

NSNumber*   number0 = [NSNumber numberWithInt:1];
NSNumber*   number1 = [NSNumber numberWithFloat:1.0f];

NSLog(@"number0 0x%x", number0);
NSLog(@"number1 0x%x", number1);

整数1と小数1.0のインスタンスを作ってみた。結果は、こうなる。

number0 0x303750
number1 0x304540

別のインスタンスになる。整数と小数は、別々に扱われるようだ。

そして、最後の実験。再び整数を使ってみる。

List 4.

NSNumber*   number0 = [NSNumber numberWithInt:100];
NSNumber*   number1 = [NSNumber numberWithInt:100];

NSLog(@"number0 0x%x", number0);
NSLog(@"number1 0x%x", number1);

List 1.と似ているが、今度は整数値100を指定してみた。この結果は、次のようになる。

number0 0x303750
number1 0x304540

別のインスタンスになる。この結果は、予想外だった。0を指定したときはFlyweightパターンになるが、100の場合はならないようだ。

このように、NSNumberはFlyweightパターンと言えるところもあるが、すべてそうなっている訳ではないようだ。もっと踏み込んで調べてみよう。

論理値とそれ以外の値

まず、作成されたインスタンスのクラスを調べてみよう。NSNumberは、クラスクラスタを構成している(本連載の第32回を参照)。従って、作成されるクラスはNSNumberではなく、そのサブクラスになっている。

調べてみると、論理値を指定した場合はNSCFBooleanクラス。それ以外はNSCFNumberクラスとなっていた。論理値の取りうる値は、YESとNOの2種類しかないので、別クラスにして効率化を図ろうとしているのだろうか。

このことから、NSNumberは論理値に対してFlywegihtパターンになっている、と言えそうである。

整数値と小数値

これ以上のことを調べるには、ソースコードを見る必要がある。幸い、NSNumberはCore FoundationとのToll-free bridge(本連載の第38回1を参照)がある。Core Foundationのソースコードは公開されているので、それを調べればいい。

今回調べることになるのは、NumberDate.subproj/CFNumber.cのファイルである。そこにある、CFNumberCreate関数を見てみよう。この関数で、CFNumberのインスタンスが作られる。これがそのままNSNumberになる。

NumberDate.subproj/CFNumber.c

CFNumberRef CFNumberCreate(
        CFAllocatorRef allocator, CFNumberType type, const void *valuePtr)
{
    CFNumberRef num;
    CFNumberType equivType, storageType;
    int32_t valToBeCached = NotToBeCached;

    equivType = __CFNumberGetCanonicalTypeForType(type);

    switch (equivType) {
    case kCFNumberFloat32Type: {
        // 値のチェック
        ...
        break;
    }
    case kCFNumberFloat64Type: {
        // 値のチェック
        ...
        break;
    }
    default: {
        switch (equivType) {
        case kCFNumberSInt64Type: { ...; break;}
        case kCFNumberSInt32Type: { ...; break;}
        case kCFNumberSInt16Type: { ...; break;}
        case kCFNumberSInt8Type:  { ...; break;}
        default:;
        }
        ...

ここでは、はじめに渡されてきた値のタイプをチェックしている。チェックでは、小数型であるkCFNumberFloat32Type、kCFNumberFloat64Typeと、整数型であるkCFNumberSInt64Typeなどは、別々に行われている。つまり、ソースコード上では小数を指定した場合と整数を指定した場合では、明確に処理が分けられているようだ。

これにより、小数から作ったNSNumberと、整数から作ったNSNumberとでは、たとえ値が同じだったとしても、別インスタンスになることが分かる。

インスタンスのキャッシュ

先ほどのソースコードの続きを見てみよう。整数タイプの値を処理するところで、インスタンスのキャッシュをしている部分がある。ここが、Flyweightパターンの鍵となる部分だ。

NumberDate.subproj/CFNumber.c

if (valToBeCached != NotToBeCached) {
    __CFSpinLock(&_CFNumberCacheLock);
    CFNumberRef result = _CFNumberCache[valToBeCached - MinCachedInt];
    __CFSpinUnlock(&_CFNumberCacheLock);
    if (result) return CFRetain(result);
    ...

_CFNumberCacheという配列があり、ここにキャッシュしたインスタンスを入れている。配列のインデックスとして使われるvalToBeCachedは、CFNumberを作るときに指定する数値になる。

では、この配列の定義を見てみよう。

NumberDate.subproj/CFNumber.c

#define MinCachedInt (-1)
#define MaxCachedInt (12)
static CFNumberRef _CFNumberCache[MaxCachedInt - MinCachedInt + 1] = {NULL};

この定義から、配列にキャッシュされる値は、最小値-1、最大値12までである事が分かる。つまり、配列の大きさは14だ。-1から12までの値を持つ、14個のインスタンスしかキャッシュされないのである。NSNumberをFlyweightパターンとしてみると、非常に限られた性能しかない事が分かる。

さて、こうなると気になるのが、この値の正当性だ。Flyweightパターンは、言うなれば、インスタンス作成のコストと、メモリの使用量を天秤にかけているものだ。作成したインスタンスをすべてキープしておけば、次のインスタンスを作成する時間がかからなくなり、パフォーマンスは上がる。しかし、その代わりにメモリの使用量は増大する。このバランスをうまく調整するのが、Flyweightパターンを有効に使う鍵になる。

Core Foundationでは、-1から12までの値をキャッシュしている。これではもちろん、作成可能なインスタンスのうち、キャッシュにひっかかるのはごくわずかだ。だが、実用という観点から考えると変わってくる。実際にアプリケーションを動かしているとき、どの値を持つNSNumberのインスタンスが作られるのだろうか?もしかすると、-1から12までの間に、作成されるインスタンスの大部分が入ってしまうのではないだろうか?

これはこれで、興味深い疑問だ。Flyweightパターンを適用する場合は、闇雲にインスタンスをキープしておくのではなく、実用上使用頻度が高いものに限定しておく方が、メモリの使用量も抑えて、適切なパフォーマンスを得られることになるだろう。

この件に関してはさらに調査して、分かったことがあったらお知らせしよう。

提供:毎日キャリアバン ク

毎日キャリアバンクではITエンジニア出身のキャリアコンサルタントで形成する IT専門のチームを編成し、キャリアに応じた専任コンサルタントがご相談を承り ます。キャリアチェンジから市場価値の可能性、ご収入などの相談から面接のア ドバイスまでお気軽にご相談ください。求人情報誌や転職情報サイトなどで一般 に公開されていないような「急募求人案件」も随時ご紹介が可能です。まずはご 登録ください!