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に対応してそうです。

またコメントで60bitがペイロードで、3bitがtag index、1bitがtagged pointerのフラグだと書いてあります(ARMv8に書いてあるのとはちょっと違いますね

実装をみてみるとこのコメントの通りになってるようです。

次に、デバッガを使って実際にNSNumberでポインタを確認してみます。

@1と@16の場合の違いを見てみると、
@1の場合は 0xb0...0012 になっているのに対し、
@16の場合は 0xb0..0102 となっていることから、
5bit目〜60bit目くらいまでが数値を表しているように考えられます。

また、先頭の 0xb.. の部分についてですが、
objc-internal.hに戻ってみると、64bit目が 1, 61~63bit目がtagとなっています。
NSNumberだとtagの値は 3 なので、ポインタは 0xb から始まることになり、ソースに書いてあることと実際のポインタのビットアサインが一致していますね。

しかし、1~4bit目が 2 になってるのはなんなのか謎が残ります。NSNumberなのでintとかlongとかの型でしょうか。

調べてみると

intだと 2 だったのがlongにすると 3 に変わったのでやはり型情報のようです。残念ながら定義までは見つけることができませんでした。

ポインタのbitアサインをまとめると以下の図のようになります。
スクリーンショット 2015-01-07 午後1.07.01

NSDateも試しに確認してみたところ、Tagged Pointerになっているようでした。

ちなみにretainしたらどうなるのかMRC環境で試してみましたが特に変化はありませんでした。
ソース(NSObject.m)を見てみると確かにretain,release,autoreleaseでは何もしないようになっていました。
#これまで調べたObjective-CのTagged Pointerの使われ方だとヒープにインスタンスの実体自体保存する必要ない(メモリ管理する必要ない)ので当たり前といえば当たり前。

以上のことから、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

isaポインタの検証

まずは、NSStringの実体を見てみましょう。

NSStingのインスタンスのisaポインタと、NSCFStringクラスのポインタを比べてみます。

全く同じですね。

一つしか調べてないですが、isaポインタには32bitの時と比べて特に変わりはないようです。

まとめ

  • 64bitの場合、NSNumber,NSDateなどの一部のObjective-CのクラスではTagged Pointerが利用されていて、ポインタを解釈するだけで値が判別できるため、これらを生成するぶんにはメモリ(ヒープ)を使わないのでメモリにやさしい。
  • Tagged Pointerが利用されている場合はポインタが実体のアドレスを示しているわけではなくなったので、普通のポインタと思って使ってしまうとクラッシュするので良い子は使わないようにしましょう。
  • Tagged Pointer以外の場合はisaポインタは特に変わってない(たぶん)

ただし、これらは今後変わっていくと思いますので、これらの実装に依存したような実装は避けたほうがよいと思います。


  1. なぜいまどきJSONKit使ってるの?とお思いかと思いますが、GreePlatformSDKではiOS4.3もサポートするという事情で使っていましたが、今回の64bit対応でIOS5.1.1以上が必須となったのを期に利用をやめる予定です。