64bit環境におけるObjective-Cのポインタ
はじめに
こんにちは、GREE Platform部の柳村(@yana_3)です。
iOSエンジニアのみなさまにおかれましてはXcode6以降の使用と64bit対応が必須になりますが、対応すすんでいますか?
GREE Platformでは、64bit対応の検証をする中でポインタ周りでJSONKit1がクラッシュするという事態が発生し、そこから64bit時のポインタについて調べたのですが、
あまりこの内容に関して詳しく記載されているところがなかったようなので共有したいと思います。
ただ普通にiOSで開発するぶんには全く役に立たない内容になっておりますのであらかじめご了承くださいmm
調べるきっかけ
64bit環境でのみJSONKitで
0xb000000000000012
という明らかにメモリ外っぽいアドレスをたたいてEXC_BAD_ACCESSでクラッシュするという問題が64bit対応の検証中に発生しました。
このアドレスの正体を突き止めると @1 つまり [NSNumber numberWithInt:1] でした。
これがなぜこんなアドレスになっているのか調べると、Objective-Cでは64bit環境においてはTagged Pointer(タグ付きポインタ)が使われているのではというところに行き着きました。
このTagged PointerがObjective-Cでどのように実際使われているのか調査していきます。
Tagged Pointer
Tagged Pointerとは、ざっくり説明すると、仮想メモリのアドレス空間を表現するのに64bitフルで必要ではないので、ポインタの余ったbitに型情報など付加的な情報を含めることで有効活用するという技術です。
参考にARM v8の資料の中にあるARMv8 Architecture Technical Previewを見ると、
アドレス空間は最大48bitで、上位8bitはTagged Pointerとして設定できソフトウェア側で判別すると書いてあります。
・Supporting up to 48 bits of VA space for each TTBR
・Upper 8 bits of address can be configured for Tagged Pointers
- Meaning interpreted by software
実際iOSでどうなってるのか見ていきます
Tagged Pointerの検証
検証環境:Xcode 6.1、iPhone6
Appleのソースを調べたらobjc-internal.hにそれっぽいものがあったのでこれを頼りに検証していきます。
ここを見たところ、NSString,NSNumber,NSIndexPath,NSManagedObjectID,NSDateがTagged Pointerに対応してそうです。
1 2 3 4 5 6 7 8 9 10 11 |
{ OBJC_TAG_NSAtom = 0, OBJC_TAG_1 = 1, OBJC_TAG_NSString = 2, OBJC_TAG_NSNumber = 3, OBJC_TAG_NSIndexPath = 4, OBJC_TAG_NSManagedObjectID = 5, OBJC_TAG_NSDate = 6, OBJC_TAG_7 = 7 }; |
またコメントで60bitがペイロードで、3bitがtag index、1bitがtagged pointerのフラグだと書いてあります(ARMv8に書いてあるのとはちょっと違いますね
1 2 3 4 5 6 7 8 |
// Tagged pointer layout and usage is subject to change // on different OS versions. The current layout is: // (MSB) // 60 bits payload // 3 bits tag index // 1 bit 1 for tagged pointer objects, 0 for ordinary objects // (LSB) |
実装をみてみるとこのコメントの通りになってるようです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// tagged pointer marker is MSB static inline void * _objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value) { // assert(_objc_taggedPointersEnabled()); // assert((unsigned int)tag < 8); // assert(((value << 4) >> 4) == value); return (void*)((1UL << 63) | ((uintptr_t)tag << 60) | (value & ~(0xFUL << 60))); } static inline bool _objc_isTaggedPointer(const void *ptr) { return (intptr_t)ptr < 0; // a.k.a. ptr & 0x8000000000000000 } static inline objc_tag_index_t _objc_getTaggedPointerTag(const void *ptr) { // assert(_objc_isTaggedPointer(ptr)); return (objc_tag_index_t)(((uintptr_t)ptr >> 60) & 0x7); } static inline uintptr_t _objc_getTaggedPointerValue(const void *ptr) { // assert(_objc_isTaggedPointer(ptr)); return (uintptr_t)ptr & 0x0fffffffffffffff; } static inline intptr_t _objc_getTaggedPointerSignedValue(const void *ptr) { // assert(_objc_isTaggedPointer(ptr)); return ((intptr_t)ptr << 4) >> 4; } |
次に、デバッガを使って実際にNSNumberでポインタを確認してみます。
1 2 3 4 5 6 7 |
(lldb) expr NSNumber* $num1 = @1 (lldb) p $num1 (NSNumber *) $num1 = 0xb000000000000012 (int)1 (lldb) expr NSNumber* $num16 = @16 (lldb) p $num16 (NSNumber *) $num16 = 0xb000000000000102 (int)16 |
@1と@16の場合の違いを見てみると、
@1の場合は 0xb0...0012 になっているのに対し、
@16の場合は 0xb0..0102 となっていることから、
5bit目〜60bit目くらいまでが数値を表しているように考えられます。
また、先頭の 0xb.. の部分についてですが、
objc-internal.hに戻ってみると、64bit目が 1, 61~63bit目がtagとなっています。
NSNumberだとtagの値は 3 なので、ポインタは 0xb から始まることになり、ソースに書いてあることと実際のポインタのビットアサインが一致していますね。
1 2 3 4 5 6 7 |
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value) { // assert(_objc_taggedPointersEnabled()); // assert((unsigned int)tag < 8); // assert(((value << 4) >> 4) == value); return (void*)((1UL << 63) | ((uintptr_t)tag << 60) | (value & ~(0xFUL << 60))); } |
しかし、1~4bit目が 2 になってるのはなんなのか謎が残ります。NSNumberなのでintとかlongとかの型でしょうか。
調べてみると
1 2 3 4 |
(lldb) expr NSNumber* $longNum = @(1UL) (lldb) p $longNum (NSNumber *) $longNum = 0xb000000000000013 (long)1 |
intだと 2 だったのがlongにすると 3 に変わったのでやはり型情報のようです。残念ながら定義までは見つけることができませんでした。
ポインタのbitアサインをまとめると以下の図のようになります。
NSDateも試しに確認してみたところ、Tagged Pointerになっているようでした。
1 2 3 4 |
(lldb) expression NSDate* $date1=[NSDate dateWithTimeIntervalSinceReferenceDate:1] (lldb) p $date1 (NSDate *) $date1 = 0xe3ff000000000000 2001-01-01 09:00:01 JST |
ちなみにretainしたらどうなるのかMRC環境で試してみましたが特に変化はありませんでした。
ソース(NSObject.m)を見てみると確かにretain,release,autoreleaseでは何もしないようになっていました。
#これまで調べたObjective-CのTagged Pointerの使われ方だとヒープにインスタンスの実体自体保存する必要ない(メモリ管理する必要ない)ので当たり前といえば当たり前。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
__attribute__((aligned(16))) id objc_retain(id obj) { if (!obj) return obj; if (obj->isTaggedPointer()) return obj; return obj->retain(); } __attribute__((aligned(16))) void objc_release(id obj) { if (!obj) return; if (obj->isTaggedPointer()) return; return obj->release(); } __attribute__((aligned(16))) id objc_autorelease(id obj) { if (!obj) return obj; if (obj->isTaggedPointer()) return obj; return obj->autorelease(); } |
以上のことから、NSNumber,NSDateなどがTagged Pointerに対応していて、60bitがペイロードで、3bitがtag index、1bitがtagged pointerのフラグとして使われていることがわかりました。
ところで、64bit対応のバイブルである64bit移行ガイドを読みかえしてみますと、
"オブジェクトのisaフィールドに直接アクセスするコードは、64ビットランタイム上では正常に動作しません。isaフィールドに、ポインタそのものは保持しないようになりました。代わりに、ポインタに結びついたあるデータを収容し、余ったビットで他のランタイム情報を保持するようになっています。この最適化により、メモリ効率と処理性能が向上しています。"
と書いてあります。
しかしながら、これまで調べた感じだと、64bitのTagged Pointerに対応したクラスの場合、そもそもポインタが実体のアドレスを示していないので、obj->isaのようにisaフィールドにアクセス自体できないので上記のような内容とは異なるように思えます。。
となると、Tagged Pointerに対応していないクラスのisaポインタも何か変わっているということでしょうか。
次にTagged Pointerに対応していないクラスインスタンスのisaポインタを見てみます。
isaポインタ
isaポインタは、ざっくりいうとクラスオブジェクトを指すポインタで、Objective-Cのインスタンスはobjc_object構造体をベースにしているので全てのインスタンスは先頭にisaポインタを保持しています。
詳しくはここでは割愛しますので、objc.hの実装やその他解説サイトや本を御覧ください。
objc.h
1 2 3 4 5 6 7 8 9 |
/// A pointer to an instance of a class. typedef struct objc_object *id; /// Represents an instance of a class. struct objc_object { Class isa OBJC_ISA_AVAILABILITY; }; /// An opaque type that represents an Objective-C class. typedef struct objc_class *Class; |
isaポインタの検証
まずは、NSStringの実体を見てみましょう。
1 2 3 4 5 6 7 8 |
(lldb) expr NSString* $str1 = @"hoge" (lldb) p *$str1 (NSString) $0 = { NSObject = { isa = __NSCFString } } |
NSStingのインスタンスのisaポインタと、NSCFStringクラスのポインタを比べてみます。
1 2 3 4 |
(lldb) p/x $str1->isa (Class) $1 = 0x00000001067f14b8 __NSCFString (lldb) p/x [__NSCFString class] (Class) $2 = 0x00000001067f14b8 __NSCFString |
全く同じですね。
一つしか調べてないですが、isaポインタには32bitの時と比べて特に変わりはないようです。
まとめ
- 64bitの場合、NSNumber,NSDateなどの一部のObjective-CのクラスではTagged Pointerが利用されていて、ポインタを解釈するだけで値が判別できるため、これらを生成するぶんにはメモリ(ヒープ)を使わないのでメモリにやさしい。
- Tagged Pointerが利用されている場合はポインタが実体のアドレスを示しているわけではなくなったので、普通のポインタと思って使ってしまうとクラッシュするので良い子は使わないようにしましょう。
- Tagged Pointer以外の場合はisaポインタは特に変わってない(たぶん)
ただし、これらは今後変わっていくと思いますので、これらの実装に依存したような実装は避けたほうがよいと思います。
- なぜいまどきJSONKit使ってるの?とお思いかと思いますが、GreePlatformSDKではiOS4.3もサポートするという事情で使っていましたが、今回の64bit対応でIOS5.1.1以上が必須となったのを期に利用をやめる予定です。 ↩