|
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 の行)。アセンブラでは、rl1 と rl2 の値を使うことによって、stub を避けることになり、その後に続く malloc の呼び出しを高速化できるんだ。 |
Obj-C メソッド呼び出しを解剖する
Objective-C の実行環境に、オブジェクトとセレクタを渡して、コードのアドレスを決定させて、適切なパラメータとともに呼び出そう。さて、細かいところでは、いったいどうなっているんだ?最初に気をつけることは、
って書いたものは、こう書くのといっしょなんだ。
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]);
}
|
青いコードは、パラメータ、セレクタ、オブジェクトを設定して(それぞれレジスタ r5、r4、r3 に入れられる。)、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 の一種だ。operate と annhilate のメソッドが実装されているオブジェクトのコンテナとして使われる。
opearteAnnhilate は、ループして、配列の中のすべてのオブジェクトを通って、両方のメソッドを呼び出す。このコードでは、事前に実装のアドレスを解決しておくことによって、NSArray の objectAtIndex: の呼び出しを最適化してるんだ。
もし、_array の中に、一種類のクラスしかないことが保証されているならば、アドレスを解決しておくことによって、objectAtIndex: だけでなく、operate と annhilate も最適化できるんだ。
でもこれはやってない、実装を一般的で、変更可能にしておくためにね。
メソッド呼び出しの代わりにインライン関数を使う
@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 を避けるとか、ファシストな同僚を喜ばせるコーディングスタイルのためとかいうよりもね。
|