home link download back number special issue

HMDT - Special Issue - Objective-C 最適化 - メモリ確保の基本と Foundation


以下のドキュメントは、Obj-C Optimization: Allocation Basics & Foundation を翻訳したものです。Mac OS X でのObjective-C の最適化手法について述べられています。


Mulle kybernetiK - Tech Info: v0.2

Obj-C 最適化:メモリ確保の基本と Foundation

シリーズ「Objective-C 最適化」の二回目は、class 実行システムと、Foundation と Foundation から継承されたクラスがどうやってオブジェクトを作るか、ってことをカバーするぜ。これは、最適化問題の中に踏み込んでいく前に、必要になるんだ。

(c) 2000 Mulle kybernetiK - text by Nat!

Objective-C のオブジェクト確保

すべての Objective-C のオブジェクトは、関連づけられたメモリを持っているんだ。最小の可能なオブジェクトは、少なくとも 4 byte なんだ(32 bit マシンならね)。この、Objective-C のオブジェクトがそれぞれ必要とする、4 byte のオーバーヘッドとはなにか?それは、isa って呼ばれるポインタだ。ちょうど、C++ のコードが、仮想関数のための __vtab エントリーと関連づけられているようにね。ま、技術的には __vtabisa は、もう、まったくぜんぜん別ものなんだけど。C++ の __vtab は仮想関数のリストで、isa はオブジェクトのクラスへのポインタだ。さて、それはなんだ?

実行時のクラスを理解する

C++ と Java とはぜんぜん違って、Objective-C のクラスは、実行時に視ることができたり、管理したりすることができるんだ。なぜなら Objective-C のクラスは、実行時にはオブジェクトになるかるなんだ。しかも、特殊なオブジェクトだからだ。NSArray に突っ込んだり、desription を得たり、メッセージを送ったりできるけど、コピーしたり、解放したりすることはできないんだ。それぞれのクラスオブジェクトは 2 つのメソッドを管理している。インスタンスメソッドと、クラスメソッドだ。

実行時の 3 つのクラスを調べてみよう。FooBar と Foundation のルートクラス NSObject だ。

@interface Foo : Bar
{
}
@end



@interface Bar : NSObject
{
}
@end




クラスオブジェクトはもっといろんな情報を持ってるんだけど、単純化のためにこれだけ見せてるよ。

見ての通り、superclass ポインタは、継承しているクラスを指す。Foo から superclass ポインタをたどっていけば、継承の木を、まず Bar に登って、次に NSObject に辿り着き、最後に nil になる(NSObject はルートクラスだかんな)。ここではオブジェクトを取り扱っているので、思い出してくれ「すべてのクラスはオブジェクトだ」、あるクラスは isa ポインタを持っていて、そいつはそのクラスオブジェクトのインスタンスであるクラスオブジェクトを指している。すべてのクラスは、isa を使って NSObject を指している。NSObject 自身であることすらあるよ(isa ポインタを辿っていけば、終わりのないループに陥る。NSObject に辿りついたときにね。superclass の場合は nil に落ち着く)。

クラスオブジェクトが NSObject であると考えるかもしれないけど、これは意味をなさないんだ。NSObject オブジェクトは、クラスオブジェクトとはみなせないからだ!NSObject クラスは isa 以外に何も持たない。だけどクラスオブジェクトは superclass ポインタを持つ。残念なことに、この事実を受け入れないといけないんだ(1)。すでに述べたように、クラスオブジェクトは、いくぶん特別なものなんだ。

オブジェクトの作り方

Objective-C のオブジェクトを作るには、必要な量のメモリを確保してやって、クラスオブジェクトのポインタを isa 、つまり最初の 4 byte、に突っ込んでやればいい。それだけだ。そしてメモリのアドレスを、メソッド呼び出しの引き数にしてやれば、動く。練習としての例を見てみよう。

#import <Foundation/Foundation.h>
@interface Bar : NSObject
{
}
@end
@implementation Bar
@end
@interface Foo : Bar
{
    int value;
}
@end
@implementation Foo
- (NSString *) description
{
    return( [NSString stringWithFormat:@"%@ value=%d",
        [super description], value);
}
@end
main()
{
    NSAutoreleasePool *pool;
    // 2 ints = 8 bytes, 1 for isa, 1 for value
    unsigned int anObject[2];

    // need this for NSLog
    pool = [[NSAutoreleasePool alloc] init];

    anObject[0] = (int) [Foo class];
    anObject[1] = 2;

    NSLog( @"Using anObject as a Foo object: %@", 
        (id) anObject);
    [pool release];
    return( 0);
}

Foundation オブジェクトの作り方

Foundation のオブジェクトは、普通 3 つのステップで作られるんだ。alloc-init-autorelease っていわれてるやつね。

次に示されている例、NSCalenderDatealloc-init-autorelease コンボは、

p = [[[NSCalenderData alloc] init] autorelease];

これといっしょなんだ。

NSCalenderDate *p;

p = [NSCalenderDate alloc];
p = [p init];
[p autorelease];

すべての呼び出しがまとめられているだけね。メモリ確保に NSZone を使うこともできるよ。別になにが変わるわけでもないけど。alloc っていうのは allocWithZone:NULL といっしょなんだ。

[[[NSCalenderDate allocWithZone:someNSZone] init] autorelease];

allocallocWithZone は、クラスメソッドなんだ。そいつらの仕事は、インスタンスが必要とするメモリを確保することと、isa ポインタを設定することだ。メモリの最初の 4 byte をクラスオブジェクトのアドレスで置き換えることによって、ただのメモリのかたまりがオブジェクトに転換されるんだ。まだ初期化されてないけどね。単純すぎるけど、充分な NSCalenderDatealloc のコードはこうなる。

+ alloc
{
    NSObject *p;


    p = (NSObject *) malloc( sizeof( NSCalenderData));
    memset( p, 0, sizeof( *p));
    p->isa = self;
    return( p);
}

NSCalenderData のクラスメソッド allocWithZone: のコードはこんな感じ。

+ (id) allocWithZone:(NSZone *)zone
{
    return( NSAllocateObject( self, 0, zone)); // mallocs and sets isa
}

Foundation の関数を使ってみた(alloc メソッドでやったのと同じ手順だよ)。NSCalenderData のインスタンスメソッド init のコードはこんな感じ。

- (id) init
{
    [super init];

    _somestorage = get_local_system_time(); // _somestorage: instance variable

    return( self);
}

これらのルーチンは、ただ単に説明するためのものだけだよ。alloc を実装するのに、NSCalenderDate の固定サイズの値を使っているけど、これってかならずしも必要ない。NSObject から継承されたメモリ確保ルーチンで充分だ。

Foundation オブジェクトの作り方(微妙な違い)

さっき見たように、Foundation はメモリの確保と初期化を 2 つのステップに分けているんだ。この分割によって、大部分のクラスは、メモリの確保は NSObject のコードを使えばよくて、必要な初期化のコードだけ書けばいいんだ。

alloc/init パラダイムは、ちょっとしたパフォーマンスと(1 回のコールと 2 回の違いね)、柔軟さおよび利便性とのトレードオフなんだ。これは、まぁ、ポジティブな効果だよね。

ネガティブな効果は、分かりにくいってことと、実装が難しい、ってことだ。次の例を見てくれ。

[[[NSString alloc] initWithCString:"string"] autorelease];

これだと、NSString の実装は、2 回メモリ確保をすることが避けられないよね、きっと。alloc が呼ばれた時点だと、init でどのぐらいの大きさの文字列がくるか分からないじゃない?可能な実装の 1 つは、init の中でメモリを確保して、文字列をコピーする、ってやり方だよね。それを、とりあえず myString ってことにして、保持しておくんだ。つまり、メモリ確保のコールの 1 つはオブジェクト用で、もう 1 つは文字列用だ。

+ (id) alloc
{
    return( NSAllocateObject( self, 0, zone));
}

static void copy_unichar_chars( unichar *dst, char *src, unsigned int len)
{
    ...
    ...
}

- (id) initWithCString:(char *) s
{
    [super init];

    myString = malloc( strlen( s) * sizeof( unichar));
    copy_unichar_chars( myString, s, strlen( s));

    return( self);
}

別のやり方は、NSAllocateObject/NSReallocateObject を使う方法だ。これで確保したメモリに追加ができる。だけど、残念ながら、まだ 2 回呼んでるけどね、、、

+ (id) alloc
{
    return( NSAllocateObject( self, 0, zone));
}

static void copy_unichar_chars( unichar *dst, char *src, unsigned int len)
{
    ...
    ...
}

- (id) initWithCString:(char *) s
{
    self = NSReallocateObject( self, strlen( s) * sizeof( unichar), [self zone]);
    [super init];

    copy_unichar_chars( self + l, s, strlen( s));

    return( self);
}

これは、ぜんぜん効果的じゃないよね。まずいことに、これって NSDicationaryNSArrayNSDataNSSet とか、そんな感じのクラスを使うと、けっこうしょっちゅう起きるんだ(これだけじゃないよ)。しょっちゅうメモリ確保しないといけないことになる。

このジレンマから抜け出す方法は、クラスファクトリメソッドを使うことだ。stringWithCString: とか numberWithInt: みたいなやつ。Foundation は、これより、いい実装をしているんだ。

+ generateWithArgument:(id) arg
{
    return( [[[self alloc] initWithArgument:arg] autorelease]);
}

これだとメモリ確保の時にオブジェクトとデータのサイズが決められるからな。これで、2 つのメモリ確保を 1 つにまとめられるよ。(訳注:つまり、上の generateWithArgument: は、へなちょこな実装の例で、Foundation もっとちゃんとした実装をしてる、ってことだ)

だけど、自動的に autorelease しないようにしたり、NSZone を引き数にとることは、できないんだ。自動的に autorelease されないオブジェクトって、大量にオブジェクトを作ったり、autorelease がパフォーマンスに響く時は、すっごく欲しいんだけどな。(訳注:ファクトリメソッドを呼ぶと、そのオブジェクトは autorelease されているので、保持しておく時は retain しないといけない。それはそれで便利なんだけど、パフォーマンスを稼ぎたい時は、いやじゃん、っていう話)

Foundation はどうなってる?

だけど、これはそんなに悪くないんだ。Foundation は placeholder クラスっていうトリックを使っているんだ。これが、最初の無駄なメモリ確保を防ぐんだ。ちょっと戻って、これを見てくれ。

[[[NSString alloc] initWithCString:"foo"] autorelease];

Foundation は、実は、最初の時点ではメモリ確保を必要としないんだ。すべては init が呼ばれた時に起こって、適切なサイズのメモリが確保されるんだ。だから、Foundation の NSString alloc は、NSString を動的に確保しないで、NSPlaceholderStringstatic オブジェクトを返すんだ。これは、解放もされないし、次に来る初期化で使われることもないんだ。だけど、この計画は allocWithZone を使う時に、頓挫しそうだよね。だって placeholder は、init のために NSZone を保持しておかないといけないし、NSZone ごとに 1 つ以上の static な placeholder を確保しないといけないからね。

これって、いいんじゃない?これだと、実質 alloc でなにもしないことになる。ま、少しは時間くうけど。

じゃ、これが Foundation で行われていることを調べるための例だ:

#import <Foundation/Foundation.h>
int main (int argc, const char *argcv[])
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSString          *s1;
    NSString          *s2;
    NSZone            *zone1;
    NSZone            *zone2;
    NSLog( @"alloc");
    s1 = [NSString alloc];
    s2 = [NSString alloc];
    NSLog( @"s1 = <%@$%lX (zone=$%lX)>", [s1 class], (long) s1, [s1 zone]);
    NSLog( @"s2 = <%@$%lX (zone=$%lX)>", [s2 class], (long) s2, [s2 zone]);
    s1 = [s1 initWithCString:"foobar"];
    s2 = [s2 initWithCString:"blabla"];
    NSLog( @"s1 = <%@$%lX (zone=$%lX)>", [s1 class], (long) s1, [s1 zone]);
    NSLog( @"s2 = <%@$%lX (zone=$%lX)>", [s2 class], (long) s2, [s2 zone]);
    //
    // Now the same but with one zone
    //
    NSLog( @"allocWithZone: (1 zone)");
    zone1 = NSCreateZone( 0x1000, 0x1000, YES);
    
    s1 = [NSString allocWithZone:zone1];
    s2 = [NSString allocWithZone:zone1];
    NSLog( @"s1 = <%@$%lX (zone=$%lX)>", [s1 class], (long) s1, [s1 zone]);
    NSLog( @"s2 = <%@$%lX (zone=$%lX)>", [s2 class], (long) s2, [s2 zone]);
    s1 = [s1 initWithCString:"foobar"];
    s2 = [s2 initWithCString:"blabla"];
    NSLog( @"s1 = <%@$%lX (zone=$%lX)>", [s1 class], (long) s1, [s1 zone]);
    NSLog( @"s2 = <%@$%lX (zone=$%lX)>", [s2 class], (long) s2, [s2 zone]);
    //
    // Now the same but with two zones
    //
    NSLog( @"allocWithZone: (2 zones)");
    
    zone1 = NSCreateZone( 0x1000, 0x1000, YES);
    zone2 = NSCreateZone( 0x1000, 0x1000, YES);
    
    s1 = [NSString allocWithZone:zone1];
    s2 = [NSString allocWithZone:zone2];
    NSLog( @"s1 = <%@$%lX (zone=$%lX)>", [s1 class], (long) s1, [s1 zone]);
    NSLog( @"s2 = <%@$%lX (zone=$%lX)>", [s2 class], (long) s2, [s2 zone]);
    s1 = [s1 initWithCString:"foobar"];
    s2 = [s2 initWithCString:"blabla"];
    NSLog( @"s1 = <%@$%lX (zone=$%lX)>", [s1 class], (long) s1, [s1 zone]);
    NSLog( @"s2 = <%@$%lX (zone=$%lX)>", [s2 class], (long) s2, [s2 zone]);
    //  insert your code here
    [pool release];
    exit(0); // insert the process exit status is 0
    return 0; // ... and make main fit the ANSI spec.
}
出力はこうだ。
alloc
s1 = <NSPlaceholderString:$CC68 (zone=$A088>
s2 = <NSPlaceholderString:$CC68 (zone=$A088>
s1 = <NSInlineCString:$D348 (zone=$A088>
s1 = <NSInlineCString:$CC48 (zone=$A088>

同じ NSPlaceholderString が返ってきてるんだ。init は新しいオブジェクト NSInlineCString を生成している。

allocWithZone: (1 zone)
s1 = <NSPlaceholderString:$CCC0 (zone=$A088>
s2 = <NSPlaceholderString:$CCC0 (zone=$A088>
s1 = <NSInlineCString:$E4F0 (zone=$D3E8>
s1 = <NSInlineCString:$E508 (zone=$D3E8>

NSZone が使われる時は、面白いことが起こってるよ。NSPlaceholderString は、allocWithZone: で渡した NSZone の中には存在しないんだ。

allocWithZone: (2 zones)
s1 = <NSPlaceholderString:$13F20 (zone=$A088>
s2 = <NSPlaceholderString:$D418 (zone=$A088>
s1 = <NSInlineCString:$2B640 (zone=$13F80>
s1 = <NSInlineCString:$2C648 (zone=$13FB0>

2 つの NSPlaceholderString が使われているよ。NSZone ごとに、新しい NSPlacehodlerString が作られてるんだ。


まとめ

この記事では、Foundation でのオブジェクトの生成の基本と、メモリ確保を調べたんだ。この準備の後、メモリ確保の戦略に進むよん。


(1) 可能な実装として、isaNSClass を指す、っていうのも考えられるんだ。これだと、クラスオブジェクトが自動的に NSObject のインスタンスメソッドを継承する、っていう利点があるんだ(category も含むよ)。NSClassNSObject のサブクラスじゃないんだ。メソッド探索の無限ループを防ぐためにね。


Home | Link | Download | Back Number | Speciall Issue

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

HMDT