home link download back number special issue

HMDT - Special Issue - Objective-C 最適化 - メソッドと関数呼び出しの内部


以下のドキュメントは、Method and fnction call innards を翻訳したものです。Mac OS X でのObjective-C の最適化手法について述べられています。


Mulle kybernetiK - Tech Info: v0.1

Obj-C 最適化:メソッドと関数呼び出しの内部

「Objective-C 最適化」シリーズ、第三回目は、メソッド呼び出しと普通の関数呼び出しの部分の、内部の仕組みについてだ。

(c) 2000 Mulle kybernetiK - text by Nat!

OK、前回の記事で、今回はメモリ確保の異なる戦略について話す、っていったけど、それはうそだ。いくつか記事を書いてたとき(残念なことにそいつはなくなってしまった。Mac OS X beta のおかげでな!あと、ぼくがばかだったんだよ)、いろんなことを決めるのが難しくなってきたんだよ。なぜなら、メソッドと関数の呼び出しの基本を、まだ解説してないからなんだ。だから、この記事を先に持ってきたよ。

C 関数と Objective-C メソッド呼び出しの内部

逆アセンブルコードの読み方
たぶん、逆アセンブルされたコードを見たことがない人もいるでしょう。gdb の出力を、さっと調べてみよう。
gdb>x/ 1i $pc
0x1ddc <call_test+12>: stw r0,8(r1)

最初の数 0x1ddc は、コード(たとえば、C 関数とか Objc-C メソッドとかね)がロードされたメモリのアドレス(1)だ。このアドレスに関係したシンボル(関数とか変数とか)があったり、すると、gdb はシンボル名と、アドレスの次に、括弧に入れてオフセットを表示するんだ:<call_test+12>。このアドレスのデータ(通常 4 byte)は、逆アセンブルされて、ニーモニックアセンブラフォーマットで表示されるんだ。stw r0,8(r1)

この記事では、たくさんの逆アセンブルしたコードが出てくるぜ。これは、コードの中でほんとに起こっていること、なんだ。ただ、これは後の議論で使われて、単に「こういう事」が行われているんだ、っていう印象を感じてもらうためだけに使われるんだ。あと、この記事は PowerPC マシンでの Mach 上でのコーディングに、話をしぼっているんだ(つまり、Mac OS X (Server) マシンね)。Intel マシン上では、かなり違った話になる箇所もあるんだ。たとえば、共有ライブラリの呼び出し方とかね。それでも、大部分の話は Intel マシンでも通用するよ。

C の呼び出し

C は Obj-C のベースになっているから、C 関数を呼び出すときに使えるオプションを調べて見よう。

もし inline (0) が定義されている関数だったら、これは簡単で難しいことはないんだ。コンパイラ(-O オプションが設定されているなら)は、幸運なことに、実際の関数コールを避けることができて、呼び出し側のコードに関数のコードを埋め込むんだ。これが C 関数を呼び出す、一番速い方法だ。マクロ(#define ってやつね)を使った古臭い方法も、これといっしょだ。コンパイラの最適化オプションを、もっとアグレッシブなレベルに設定してやったら、小さい関数も自動的にインラインにされるよ。

次の例は、2 つの C 関数がコンパイルされる様子を示しているんだ。ただの関数 foo が、「普通の」やり方でサブルーチンとして呼ばれるのに対して、関数 inline_foo は、コンパイラによってインライン化されるんだ。

0x1dac <foo>: mflr r0 
0x1db0 <foo+4>:	bcl	20,4*cr7+so,0x1db4 <foo+8>  
0x1db4 <foo+8>:	mflr	r12  
0x1db8 <foo+12>:	mtlr	r0   
0x1dbc <foo+16>:	addis	r9,r12,0    
0x1dc0 <foo+20>:	addi	r9,r9,568  
0x1dc4 <foo+24>:	lfd	f0,0(r9)    
0x1dc8 <foo+28>:	fmul	f1,f1,f0   
0x1dcc <foo+32>:	blr                 
0x1dd0 <call_test>:		mflr	r0
0x1dd4 <call_test+4>:	stfd	f31,-8(r1)
0x1dd8 <call_test+8>:	stw	r31,-12(r1)
0x1ddc <call_test+12>:	stw	r0,8(r1)
0x1de0 <call_test+16>:	stwu	r1,-80(r1)
0x1de4 <call_test+20>:	bcl	20,4*cr7+so,0x1de8 <call_test+24>
0x1de8 <call_test+24>:	mflr	r31
0x1dec <call_test+28>:	fmr	f31,f1
0x1df0 <call_test+32>:	addis	r9,r31,0
0x1df4 <call_test+36>:	addi	r9,r9,524
0x1df8 <call_test+40>:	lfd	f0,0(r9)
0x1dfc <call_test+44>:	fmul	f31,f31,f0
0x1e00 <call_test+48>:	bl	0x1dac <foo>
0x1e04 <call_test+52>:	fadd	f1,f31,f1
0x1e08 <call_test+56>:	addi	r1,r1,80
0x1e0c <call_test+60>:	lwz	r0,8(r1)
0x1e10 <call_test+64>:	mtlr	r0
0x1e14 <call_test+68>:	lwz	r31,-12(r1)
0x1e18 <call_test+72>:	lfd	f31,-8(r1)
0x1e1c <call_test+76>:	blr
右側では、ソースコードをコンパイルしたものを、逆アセンブルしたものを示している。

逆アセンブルされたコードの内、インライン化されたコードは、緑色で示されている(<call_test+32> - <call_test+44>)。命令が 4 つだけだ。これと機能はまったく同一で、C 関数のヘッダとフッタで囲まれたやつは、紫色で示してあるんだ(<foo>+16 - <foo>+28)。C 関数の呼び出しや、インライン呼び出しのオーバーヘッドは、青色で示してある。

static inline double  inline_foo( float x)
{
   return( x * 2.1);
}


static double  foo( float x)
{
   return( x * 2.1);
}


double call_test( float x)
{
   return( inline_foo( x) + foo( x));
}

この、単純な例は、これ以上追求することもないでしょう。インラインはすばらしい!

共有ライブラリの関数の呼び出し

共有ライブラリとして提供されている関数って、かなり頻繁に呼び出されてるでしょ。Framework の中にあるすべての関数は、共有ライブラリの中にある、っていうことになるもん。例として、malloc の呼び出しがどうなっているか調べて見よう:

0x1dd4 <call_test2>:		mflr	r0
0x1dd8 <call_test2+4>:	stw	r0,8(r1)
0x1ddc <call_test2+8>:	stwu	r1,-64(r1)
0x1de0 <call_test2+12>:	li	r3,128
0x1de4 <call_test2+16>:	bl	0x1fa4 <dyld_stub_malloc>
0x1de8 <call_test2+20>:	addi	r1,r1,64
0x1dec <call_test2+24>:	lwz	r0,8(r1)
0x1df0 <call_test2+28>:	mtlr	r0
0x1df4 <call_test2+32>:	blr
0x1fa4 <dyld_stub_malloc>:	mflr	r0
0x1fa8 <dyld_stub_malloc+4>:	bcl	20,4*cr7+so,0x1fac <dyld_stub_malloc+8>
0x1fac <dyld_stub_malloc+8>:	mflr	r11
0x1fb0 <dyld_stub_malloc+12>:	addis	r11,r11,0
0x1fb4 <dyld_stub_malloc+16>:	mtlr	r0
0x1fb8 <dyld_stub_malloc+20>:	lwz	r12,112(r11)
0x1fbc <dyld_stub_malloc+24>:	mtctr	r12
0x1fc0 <dyld_stub_malloc+28>:	addi	r11,r11,112
0x1fc4 <dyld_stub_malloc+32>:	bctr
0x5ace3800 <malloc>:		mflr	r0
0x5ace3804 <malloc+4>:	stmw	r30,-8(r1)
0x5ace3808 <malloc+8>:	stw	r0,8(r1)
0x5ace380c <malloc+12>:	stwu	r1,-80(r1)
0x5ace3810 <malloc+16>:	bcl	20,4*cr7+so,0x5ace3814 <malloc+20>
0x5ace3814 <malloc+20>:	mflr	r31
0x5ace3818 <malloc+24>:	mr	r30,r3
0x5ace381c <malloc+28>:	addis	r9,r31,14
0x5ace3820 <malloc+32>:	lwz	r9,3020(r9)
0x5ace3824 <malloc+36>:	cmpwi	r9,0
0x5ace3828 <malloc+40>:	bne	0x5ace3830 <malloc+48>
0x5ace382c <malloc+44>:	bl	0x5ace74a0 <_malloc_initialize>
0x5ace3830 <malloc+48>:	addis	r9,r31,14
0x5ace3834 <malloc+52>:	lwz	r9,3024(r9)
0x5ace3838 <malloc+56>:	lwz	r3,0(r9)
0x5ace383c <malloc+60>:	mr	r4,r30
0x5ace3840 <malloc+64>:	bl	0x5ace3860 <malloc_zone_malloc>
0x5ace3844 <malloc+68>:	addi	r1,r1,80
0x5ace3848 <malloc+72>:	lwz	r0,8(r1)
0x5ace384c <malloc+76>:	mtlr	r0
0x5ace3850 <malloc+80>:	lmw	r30,-8(r1)
0x5ace3854 <malloc+84>:	blr
void  call_test2()
{
   void  *p;

   p = malloc( 128);
}

call_test2 の中の、malloc を呼び出すコードは、青い、たったの、2 行なんだ。アドレス 0x1de0 と 0x1de4 にある。他のコードについて考えてみよう :)

直接 malloc を呼ぶ代わりに、dyld_stub_malloc って名前のコードが呼び出されている。この、君のコードに静的にリンクされた stub コードは、共有ライブラリの malloc を呼び出すための、インタフェースを提供するんだ。その malloc のアドレスは、実行時にのみ決定される。

だから、いくつかの余計な命令(赤いところね)が、malloc に行く前に実行されるんだ (2)

逆アセンブルされたコードを見れば、素人でも、コードがいっぱいあるから、上の例の "foo" みたいに静的にリンクされたものよりも遅くなる、ってことが推論できるよね。

アセンブラの最適化
共有ライブラリの実際の関数のアドレスは、実行時にルックアップテーブルから引っ張ってこられるんだ(dyld_stub_malloc+4 から dyld_stub_malloc_20 の行)。アセンブラでは、rl1rl2 の値を使うことによって、stub を避けることになり、その後に続く malloc の呼び出しを高速化できるんだ。

Obj-C メソッド呼び出しを解剖する

Objective-C の実行環境に、オブジェクトとセレクタを渡して、コードのアドレスを決定させて、適切なパラメータとともに呼び出そう。さて、細かいところでは、いったいどうなっているんだ?最初に気をつけることは、

[p callWith:x and:y];

って書いたものは、こう書くのといっしょなんだ。

objc_msgSend( p, @selector( callWith: and: ), x, y);

ところで、セレクタってなんなのさ?
現在の Apple の実装でのセレクタは、ただの C 文字列のアドレスだ。この文字列は、セレクタの名前を表している。だから、@selector(callWith:and:)0x10210 だったら、アドレス 0x10210 には、'c', 'a', 'l', 'l', 'W', 'i', 't', 'h', ':', 'a', 'n', 'd', ':', '0' があるんだ。

このセレクタの文字列は mach の実行環境がロードしている間は、これが唯一だ。つまり、いろんなフレームワークや、きみの main 関数の同じ名前を持つセレクタは、同じセレクタアドレスを共有しているんだ。

objc_msgSend 関数は、オブジェクトとセレクタから、どのコードを実行するか決定しないといけないんだ。メソッド callWith:and: は、たくさんの数の異なったオブジェクトに実装されていることがあるから、セレクタを調べるだけじゃ、objc_msgSend を実行するには不十分なんだ。必要なのは、オブジェクトのクラスを調べて、そいつか、そいつの親クラスにセレクタの実装が定義されているかを探すことだ。

実際の仕組みは、Objective-C の本によく説明されているよ。だから、ここでは重複することはしない。きみのハードディスクにあるコピーを読むか、 ここを参照してみてくれ。http://www.toodarkpark.org/computers/objc/coreobjc.html#1522

んじゃ、単純な Objective-C のメソッドを調べてみて、その呼び出しを一つずつ追い掛けてみよう。









0x2d18 <-[CallTest3 fooMethod:]>:	addis	r9,r12,0
0x2d1c <-[CallTest3 fooMethod:]+4>:	addi	r9,r9,732
0x2d20 <-[CallTest3 fooMethod:]+8>:	lfd	f0,0(r9)
0x2d24 <-[CallTest3 fooMethod:]+12>:	fmul	f1,f1,f0
0x2d28 <-[CallTest3 fooMethod:]+16>:	blr
- (double) fooMethod:(float) x
{
   return( x * 2.1);
}

面白いことに、fooMethod: 自体は、前の例で見た C 関数よりも、スリムに見えるんだ。リターン命令 blr を除けば、スタック管理や、環境変数セットアップのオーバーヘッドはないんだ。なんでかっていうと、それは stub と objc_msgSend の中で処理されるからだ。

0x2d2c <call_test3>:	mflr	r0
0x2d30 <call_test3+4>:	stw	r31,-4(r1)
0x2d34 <call_test3+8>:	stw	r0,8(r1)
0x2d38 <call_test3+12>:	stwu	r1,-80(r1)
0x2d3c <call_test3+16>:	bcl	20,4*cr7+so,0x2d40 <call_test3+20>
0x2d40 <call_test3+20>:	mflr	r31
0x2d44 <call_test3+24>:	addis	r4,r31,0
0x2d48 <call_test3+28>:	lwz	r4,4800(r4)
0x2d4c <call_test3+32>:	stfs	f1,56(r1)
0x2d50 <call_test3+36>:	lwz	r5,56(r1)
0x2d54 <call_test3+40>:	bl	0x2fc0 <dyld_stub_objc_msgSend>
0x2d58 <call_test3+44>:	addi	r1,r1,80
0x2d5c <call_test3+48>:	lwz	r0,8(r1)
0x2d60 <call_test3+52>:	mtlr	r0
0x2d64 <call_test3+56>:	lwz	r31,-4(r1)
0x2d68 <call_test3+60>:	blr
double   call_test3( CallTest3 *p, float x)
{
   return( [p fooMethod:x]);
}
青いコードは、パラメータ、セレクタ、オブジェクトを設定して(それぞれレジスタ r5r4r3 に入れられる。)、stub 関数を呼ぶんだ。
0x2fc0 <dyld_stub_objc_msgSend>:	mflr	r0
0x2fc4 <dyld_stub_objc_msgSend+4>:	bcl	20,4*cr7+so,0x2fc8 
<dyld_stub_objc_msgSend+8>
0x2fc8 <dyld_stub_objc_msgSend+8>:	mflr	r11
0x2fcc <dyld_stub_objc_msgSend+12>:	addis	r11,r11,0
0x2fd0 <dyld_stub_objc_msgSend+16>:	mtlr	r0
0x2fd4 <dyld_stub_objc_msgSend+20>:	lwz	r12,88(r11)
0x2fd8 <dyld_stub_objc_msgSend+24>:	mtctr	r12
0x2fdc <dyld_stub_objc_msgSend+28>:	addi	r11,r11,88
0x2fe0 <dyld_stub_objc_msgSend+32>:	bctr
objc_msgSend は、共有ライブラリの中にあるから、プロセッサは stub コード(赤いとこね)を通り抜けていって、objc_msgSend の定義に飛ぶんだ。

0x720bb088 <objc_msgSend>:	cmplwi	r3,0
0x720bb08c <objc_msgSend+4>:	beq	0x720bb1f4 <objc_msgSend+364>
0x720bb090 <objc_msgSend+8>:	stw	r8,44(r1)
0x720bb094 <objc_msgSend+12>:	stw	r9,48(r1)
0x720bb098 <objc_msgSend+16>:	stw	r10,52(r1)
0x720bb09c <objc_msgSend+20>:	lwz	r12,0(r3)
0x720bb0a0 <objc_msgSend+24>:	lwz	r12,32(r12)
0x720bb0a4 <objc_msgSend+28>:	lwz	r11,0(r12)
0x720bb0a8 <objc_msgSend+32>:	addi	r9,r12,8
0x720bb0ac <objc_msgSend+36>:	and	r12,r4,r11
0x720bb0b0 <objc_msgSend+40>:	rlwinm	r0,r12,2,0,29
0x720bb0b4 <objc_msgSend+44>:	lwzx	r10,r9,r0
0x720bb0b8 <objc_msgSend+48>:	cmplwi	r10,0
0x720bb0bc <objc_msgSend+52>:	beq	0x720bb0f4 <objc_msgSend+108>
0x720bb0c0 <objc_msgSend+56>:	addi	r12,r12,1
0x720bb0c4 <objc_msgSend+60>:	lwz	r8,0(r10)
0x720bb0c8 <objc_msgSend+64>:	and	r12,r12,r11
0x720bb0cc <objc_msgSend+68>:	lwz	r10,8(r10)
0x720bb0d0 <objc_msgSend+72>:	cmplw	r8,r4
0x720bb0d4 <objc_msgSend+76>:	bne-	0x720bb0b0 <objc_msgSend+40>
0x720bb0d8 <objc_msgSend+80>:	mr	r12,r10
0x720bb0dc <objc_msgSend+84>:	mtctr	r10
0x720bb0e0 <objc_msgSend+88>:	lwz	r8,44(r1)
0x720bb0e4 <objc_msgSend+92>:	lwz	r9,48(r1)
0x720bb0e8 <objc_msgSend+96>:	lwz	r10,52(r1)
0x720bb0ec <objc_msgSend+100>:	li	r11,0
0x720bb0f0 <objc_msgSend+104>:	bctr
0x720bb0f4 <objc_msgSend+108>:	stw	r3,24(r1)
0x720bb0f8 <objc_msgSend+112>:	stw	r4,28(r1)
0x720bb0fc <objc_msgSend+116>:	stw	r5,32(r1)
0x720bb100 <objc_msgSend+120>:	stw	r6,36(r1)
0x720bb104 <objc_msgSend+124>:	stw	r7,40(r1)
0x720bb108 <objc_msgSend+128>:	mflr	r0
0x720bb10c <objc_msgSend+132>:	stw	r0,8(r1)
0x720bb110 <objc_msgSend+136>:	stfd	f13,-8(r1)
0x720bb114 <objc_msgSend+140>:	stfd	f12,-16(r1)
0x720bb118 <objc_msgSend+144>:	stfd	f11,-24(r1)
0x720bb11c <objc_msgSend+148>:	stfd	f10,-32(r1)
0x720bb120 <objc_msgSend+152>:	stfd	f9,-40(r1)
0x720bb124 <objc_msgSend+156>:	stfd	f8,-48(r1)
0x720bb128 <objc_msgSend+160>:	stfd	f7,-56(r1)
0x720bb12c <objc_msgSend+164>:	stfd	f6,-64(r1)
0x720bb130 <objc_msgSend+168>:	stfd	f5,-72(r1)
0x720bb134 <objc_msgSend+172>:	stfd	f4,-80(r1)
0x720bb138 <objc_msgSend+176>:	stfd	f3,-88(r1)
0x720bb13c <objc_msgSend+180>:	stfd	f2,-96(r1)
0x720bb140 <objc_msgSend+184>:	stfd	f1,-104(r1)
0x720bb144 <objc_msgSend+188>:	stwu	r1,-160(r1)
0x720bb148 <objc_msgSend+192>:	lwz	r3,0(r3)
0x720bb14c <objc_msgSend+196>:	mflr	r0
0x720bb150 <objc_msgSend+200>:	bl	0x720bb154 <objc_msgSend+204>
0x720bb154 <objc_msgSend+204>:	mflr	r12
0x720bb158 <objc_msgSend+208>:	mtlr	r0
0x720bb15c <objc_msgSend+212>:	addis	r12,r12,5
0x720bb160 <objc_msgSend+216>:	lwz	r12,-1156(r12)
0x720bb164 <objc_msgSend+220>:	mtctr	r12
0x720bb168 <objc_msgSend+224>:	mflr	r0
0x720bb16c <objc_msgSend+228>:	stw	r0,8(r1)
0x720bb170 <objc_msgSend+232>:	stwu	r1,-56(r1)
0x720bb174 <objc_msgSend+236>:	bctrl
0x720bb178 <objc_msgSend+240>:	addic	r1,r1,56
0x720bb17c <objc_msgSend+244>:	lwz	r0,8(r1)
0x720bb180 <objc_msgSend+248>:	mtlr	r0
0x720bb184 <objc_msgSend+252>:	mr	r12,r3
0x720bb188 <objc_msgSend+256>:	mtctr	r3
0x720bb18c <objc_msgSend+260>:	lwz	r1,0(r1)
0x720bb190 <objc_msgSend+264>:	lwz	r0,8(r1)
0x720bb194 <objc_msgSend+268>:	mtlr	r0
0x720bb198 <objc_msgSend+272>:	lfd	f13,-8(r1)
0x720bb19c <objc_msgSend+276>:	lfd	f12,-16(r1)
0x720bb1a0 <objc_msgSend+280>:	lfd	f11,-24(r1)
0x720bb1a4 <objc_msgSend+284>:	lfd	f10,-32(r1)
0x720bb1a8 <objc_msgSend+288>:	lfd	f9,-40(r1)
0x720bb1ac <objc_msgSend+292>:	lfd	f8,-48(r1)
0x720bb1b0 <objc_msgSend+296>:	lfd	f7,-56(r1)
0x720bb1b4 <objc_msgSend+300>:	lfd	f6,-64(r1)
0x720bb1b8 <objc_msgSend+304>:	lfd	f5,-72(r1)
0x720bb1bc <objc_msgSend+308>:	lfd	f4,-80(r1)
0x720bb1c0 <objc_msgSend+312>:	lfd	f3,-88(r1)
0x720bb1c4 <objc_msgSend+316>:	lfd	f2,-96(r1)
0x720bb1c8 <objc_msgSend+320>:	lfd	f1,-104(r1)
0x720bb1cc <objc_msgSend+324>:	lwz	r3,24(r1)
0x720bb1d0 <objc_msgSend+328>:	lwz	r4,28(r1)
0x720bb1d4 <objc_msgSend+332>:	lwz	r5,32(r1)
0x720bb1d8 <objc_msgSend+336>:	lwz	r6,36(r1)
0x720bb1dc <objc_msgSend+340>:	lwz	r7,40(r1)
0x720bb1e0 <objc_msgSend+344>:	lwz	r8,44(r1)
0x720bb1e4 <objc_msgSend+348>:	lwz	r9,48(r1)
0x720bb1e8 <objc_msgSend+352>:	lwz	r10,52(r1)
0x720bb1ec <objc_msgSend+356>:	li	r11,0
0x720bb1f0 <objc_msgSend+360>:	bctr
0x720bb1f4 <objc_msgSend+364>:	mflr	r0
0x720bb1f8 <objc_msgSend+368>:	bl	0x720bb1fc <objc_msgSend+372>
0x720bb1fc <objc_msgSend+372>:	mflr	r11
0x720bb200 <objc_msgSend+376>:	mtlr	r0
0x720bb204 <objc_msgSend+380>:	addis	r11,r11,5
0x720bb208 <objc_msgSend+384>:	lwz	r11,-1328(r11)
0x720bb20c <objc_msgSend+388>:	lwz	r11,0(r11)
0x720bb210 <objc_msgSend+392>:	cmplwi	r11,0
0x720bb214 <objc_msgSend+396>:	beqlr	
0x720bb218 <objc_msgSend+400>:	mflr	r0
0x720bb21c <objc_msgSend+404>:	stw	r0,8(r1)
0x720bb220 <objc_msgSend+408>:	addi	r1,r1,-64
0x720bb224 <objc_msgSend+412>:	mtctr	r11
0x720bb228 <objc_msgSend+416>:	bctrl
0x720bb22c <objc_msgSend+420>:	addi	r1,r1,64
0x720bb230 <objc_msgSend+424>:	lwz	r0,8(r1)
0x720bb234 <objc_msgSend+428>:	mtlr	r0
0x720bb238 <objc_msgSend+432>:	li	r3,0
0x720bb23c <objc_msgSend+436>:	blr
objc_msgSend の実装の奥深くには、行かないようにしよう。最初の 2 行は nil メッセージの取り扱いだ(オレンジ色)。オブジェクトが nil だったら、objc_msgSend+364 に枝分かれする。これは無視するよ。

黄色っぽい部分(objc_msgSend+8 から objc_msgSend+104)は、キャッシュされてるメソッドを探して、そこに飛ぶ部分だ。

メソッドが初めて呼ばれるときは、objc_msgSend はキャッシュの中にエントリを見つけられない。この場合は、明るい白色の部分のコードが使われる(objc_msgSend+108 以下)。クラス階層を遡って、適当な実装を見つけて、メソッドのキャッシュに入れるんだ。

この探索コードは、比較的に、すごく遅い。ループはするわ、たくさんのサブルーチンに分岐するわで、500 か、それ以上の命令を実行することになるんだ。

メソッドが見つかって、キャッシュに突っ込まれたら、探索時間はすごく短くなる!キャッシュから見つかったエントリを実行するには、カーキ色のコードを数回ループするだけでいいんだ(objc_msgSend+40 以下)。

objc_msgSend の最小のオーバーヘッドは、1 回の呼び出しで 30 命令だ。もう少しかかるときもあるよ。

objc_msgSend を避けて、直接 Objective-C のメソッドを呼ぶ

Objective-C では、メソッドのアドレスを実行時に解決したり、そのアドレスを直接呼ぶ、ってことをできることがあるんだ。methodForSelector: メソッドで、メソッドのアドレスを決定できて、IMP って呼ばれてるやつを取得できるんだ。

IMP imp = [anObject methodForSelector:@selector( fooMethod:)];

IMP の型は Foundation で定義されていて、それは id を返す関数ポインタなんだ。引数は可変長で、最初の 2 つはオブジェクトとセレクタ(ちょうど objc_msgSend といっしょね)なんだ。これは、/System/Library/Frameworks/System.framework/Headers/objc/objc.h からのコピーだ(訳注:Mac OS X 10.1 では、/usr/include/objc/objc.h)。

/*      objc.h
 *      Copyright 1988-1996, NeXT Software, Inc.
 */
#ifndef _OBJC_OBJC_H_
#define _OBJC_OBJC_H_
#import <objc/objc-api.h>               // for OBJC_EXPORT
typedef struct objc_class *Class;
typedef struct objc_object {
        Class isa;
} *id;
typedef struct objc_selector    *SEL;    
typedef id                      (*IMP)(id, SEL, ...); 

この小さな関数は、IMP とオブジェクトを使って、1 つの引数を持つメソッドを呼び出す例だ。気をつけてほしいのは、int や id を返さない関数もたくさんあるけど、その場合は、IMP を正しい型にキャストしてやる必要があるんだ(3)

double   call_test3b( CallTest3 *p, double (*f)( id, SEL, ...), float x)
{
   return( (*f)( p, @selector( fooMethod:), x));
}

で、これはこんな風に呼び出せる(キャストに気をつけてくれ):

call_test3b( p, 
             (double (*)( id, SEL, ...)) 
              [p methodForSelector:@selector( fooMethod:)], 
             2.1);

										

逆アセンブルされたコードによると、関数コールすることによって、3 つ命令が増えている(青いとこ)。もちろん、呼んでいないんだから、stub コードや objc_msgSend のコードは表示されてないよ。

0x2cd4 <call_test3b>:	mflr	r0
0x2cd8 <call_test3b+4>:	stw	r31,-4(r1)
0x2cdc <call_test3b+8>:	stw	r0,8(r1)
0x2ce0 <call_test3b+12>:	stwu	r1,-80(r1)
0x2ce4 <call_test3b+16>:	bcl	20,4*cr7+so,0x2ce8 <call_test3b+20>
0x2ce8 <call_test3b+20>:	mflr	r31
0x2cec <call_test3b+24>:	mr	r0,r4
0x2cf0 <call_test3b+28>:	addis	r4,r31,0
0x2cf4 <call_test3b+32>:	lwz	r4,4888(r4)
0x2cf8 <call_test3b+36>:	stfd	f1,56(r1)
0x2cfc <call_test3b+40>:	lwz	r5,56(r1)
0x2d00 <call_test3b+44>:	lwz	r6,60(r1)
0x2d04 <call_test3b+48>:	mtlr	r0
0x2d08 <call_test3b+52>:	mr	r12,r0
0x2d0c <call_test3b+56>:	blrl
0x2d10 <call_test3b+60>:	addi	r1,r1,80
0x2d14 <call_test3b+64>:	lwz	r0,8(r1)
0x2d18 <call_test3b+68>:	mtlr	r0
0x2d1c <call_test3b+72>:	lwz	r31,-4(r1)
0x2d20 <call_test3b+76>:	blr
これは、もう、よく知ってる fooMethod だよね。
0x2d24 <-[CallTest4 fooMethod:]>:	addis	r9,r12,0
0x2d28 <-[CallTest4 fooMethod:]+4>:	addi	r9,r9,732
0x2d2C <-[CallTest4 fooMethod:]+8>:	lfd	f0,0(r9)
0x2d30 <-[CallTest4 fooMethod:]+12>:	fmul	f1,f1,f0
0x2d34 <-[CallTest4 fooMethod:]+16>:	blr

得たもの:スピード
失ったもの:通常状態じゃないときの安定性

methodForSelector: で、クラスのメソッドのアドレスを得ることができた。どのメソッドの実装になるかは(メソッドはオーバーロードされることを思い出してくれ)、実行時に決定される。クラスに使われるメソッドの実装は、実行の最中にも変わることがあるんだ。

クラスが他のクラスにとって代わられることがあれば、メソッドの実装は変わるだろう。クラスにカテゴリが追加されれば、たとえば NSBundle をロードするとき、実装はたぶん変わるだろう。だから、この直接呼び出し手法は、IMP を使っている間、システムがそれを固定化してくれるときだけ、安全に使えるんだ。そりゃ無理だろ。

だけど、現実に戻ろう。たとえば、NSNotificationCenter を使っているときは、同じ落とし穴にひっかかるはずだ。だって NSNotificationCenter は、メソッドの実装のアドレスを保持しているんであって、セレクタではないからだ。Foundation ができるなら、きみだってできるだろ。それに関係するんだけど、NSBundle の中のクラスやカテゴリが、期待通りに動かないとしても、別に驚きはしないんだ。だから、そいつらには "Schwarzer Peter" を与えよう。methodForSelector: じゃなくて。(訳注:?)

よくつかわれる妥協案は、呼び出し側が生きている間だけ、メソッドの実装をキャッシュしておく、っていうやつだ。次のような例だ。

- (void) operateAnnhilate
{
  SEL  sel;
  IMP  f;
  int  n;
  int  i;
  id   p;

  sel = @selector( objectAtIndex:);
  f = [_array methodForSelector:sel];
  n = [_array count];
  for( i = 0; i < n; i++)
  {
    p = (f)( _array, sel, i);
    [p operate];
    [p annihilate];
  }
}
_array はインスタンス変数で、NSArray の一種だ。operateannhilate のメソッドが実装されているオブジェクトのコンテナとして使われる。

opearteAnnhilate は、ループして、配列の中のすべてのオブジェクトを通って、両方のメソッドを呼び出す。このコードでは、事前に実装のアドレスを解決しておくことによって、NSArrayobjectAtIndex: の呼び出しを最適化してるんだ。

もし、_array の中に、一種類のクラスしかないことが保証されているならば、アドレスを解決しておくことによって、objectAtIndex: だけでなく、operateannhilate も最適化できるんだ。

でもこれはやってない、実装を一般的で、変更可能にしておくためにね。

メソッド呼び出しの代わりにインライン関数を使う

@implementation の中では、インライン関数は private か protected のインスタンス変数にしかアクセスできないんだ。残念なことに、@interface の中では、関数の宣言ができない。だから、これは使えないんだ(くそっ!)。

@interface Foo : NSObject
{
  int someVar;
}
// it ain't compiling folks
static inline int  someComputedValue( id obj, SEL _cmd)
{
   return( ((Foo *) obj)->someVar * STRANGE_CONSTANT);
}

だけど、public なインスタンス変数にはどこからでもアクセスできる。これは使える:

@interface Foo : NSObject
{
@public
   int someVar;
}
@end

// The static declaration ensures that the header can be included in more than one file :)
static inline int someComputedValue( id obj, SEL _cmd, int factor)
{
   return( ((Foo *) obj)->someVar * factor);
}

その代用として(ZNeK ありがとう)、@defs を使えば private 変数にアクセスできる。

@interface Foo : NSObject
{
   int   someVar;
}
@end


static inline int   someComputedValue( id obj, SEL _cmd)
{
   return( ((struct { @defs( Foo) } *) obj)->someVar * STRANGE_CONSTANT);
}

すばらしい!

得たもの:スピード
失ったもの:メソッドの動的バインディング

失ったものは明らかだよね。だって、メソッドを呼んでいるわけじゃなくて、C 関数を呼んでいる、またはインライン化しているだけなんだから。

まとめ

Obj-C のメソッド呼び出しは遅くないけど、通常の C 呼び出しよりは遅い。インライン以外では、最速の呼び出しは、静的な C 関数だ(または、共有ライブラリを使わない関数)。IMP を使うのも同じぐらい速い。

絶望の中で、パフォーマンスを稼ぐには、もうオブジェクト使うのあきらめないといかん!、って思った時は、もう一回考えてくれ。パフォーマンスを稼ぐ C と、便利な Objective-C メソッドを混ぜて使うのは、Objective-C をあきらめるより、ずっといい感じがするぞ。


(0) inline は共通で使われるコンパイラの拡張だけど、C 言語の一部にはなっていないんだ(まだ)。

(1) このアドレスは、思っているほど、ランダムじゃないんだ。大部分の共有ライブラリは、原理的には再配置可能だけど、普通にロードされるときのデフォルトのアドレスを持っているんだ。アプリケーションコードの位置は、リンカによって決められる。もし、きみのバイナリが変更されていないなら、いつも同じ仮想メモリのアドレス上にロードされる。

(2) stub の中のコードのいくつかは、stub の中にないとしたら、呼び出し側でインライン化されてるんだと思うよ。だから厳密にいうと、オーバーヘッドは命令 9 つではない。

(3)オブジェクトのコードが必要としてる。warning を避けるとか、ファシストな同僚を喜ばせるコーディングスタイルのためとかいうよりもね。


Home | Link | Download | Back Number | Speciall Issue

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

HMDT