デスクトップオペレーティングシステムが32ビットから64ビットアドレッシングに移行した時、64ビットアプリはOSの移行に必要不可欠でした。今、iOSはデスクトップ級のアーキテクチャになってきています。iOS 7から、64ビットプロセッサを活用するアプリをビルドすることができます。64ビットプロセッシングをサポートするアプリは、同じデバイスで動作している32ビットアプリと比較して、ほぼ常に良いパフォーマンスを得ることができます。
異なるコードが一緒に動作しなくてはならない時、どのように振る舞うべきかについて、基準となる合意規約に従わなくてはいけません。規約には共通データ型のサイズとフォーマット、一方のコードが他方のコードをコールする場合のインストラクションが含まれます。コンパイラはこれらの規約をベースに実装されているので、協調して動作するバイナリコードを生成することができます。これらの規約をアプリケーションバイナリインターフェイス(ABI)と呼びます。
iOSアプリは、低レベルのアプリケーションバイナリインターフェイスと、Objective-C言語とシステムフレームワークによって規定されるコーディング規約に依存しています。iOS 7から、いくつかのiOSデバイスは、64ビットプロセッサを使用し、32ビットと64ビットのランタイム環境を提供します。多くのアプリにとって、64ビットランタイム環境は、大きく2つの点において32ビットランタイム環境と異なります。
- 64ビットランタイムでは、Cocoa Touchフレームワーク(とObjective-C言語自身)によって使用される多くのデータ型は、サイズが拡大されたり、厳密なメモリアラインメントルールが存在したりします。
- 64ビットランタイムには、関数呼び出しをする際に使用される適切な関数プロトタイプが必要です。
C言語やObjective-C言語では、ビルトインのデータ型のサイズやメモリ上でのアラインメントは定義されていません。代わりに、それぞれのプラットフォームがそれらを定義します。これは、言語規約に定義された制約に則った上で、プラットフォームがハードウェアやオペレーティング・システムに最もマッチする値を使用することができることを意味します。iOS上の64ビットランタイムは、多くのビルトインのデータ型のサイズを変更しています。Cocoa Touchによって使用される多くのデータ型も変更されています。このセクションでは、Objective-Cで一般的に使用されるデータ型の変更について説明します。
32ビットランタイム環境は、ILP32と呼ばれる規約を使用します。そこでは、integerとlong integerとポインタは32ビットの大きさです。64ビットランタイム環境は、LP64という規約を使用します。integerは32ビットですが、long integerとポインタは64ビットの大きさです。
次のテーブルは、Objective-Cにおいて共通で使用されるすべてのinteger型を記載しています。それぞれのエントリーはデータ型のサイズとメモリ上でのアラインメントです。強調されているエントリーは、LP64規約がILP32規約と異なっている部分です。これらサイズに差がある部分が、64ビットランタイム向けにコードがコンパイルされた時にコードの振る舞いが変わる部分です。コンパイラは64ビットランタイム向けにコンパイルする時、__LP64__マクロを定義します。
Integer data type | ILP32 size | ILP32 alignment | LP64 size | LP64 alignment |
---|---|---|---|---|
char | 1 byte | 1 byte | 1 byte | 1 byte |
BOOL, bool | 1 byte | 1 byte | 1 byte | 1 byte |
short | 2 bytes | 2 bytes | 2 bytes | 2 bytes |
int | 4 bytes | 4 bytes | 4 bytes | 4 bytes |
long | 4 bytes | 4 bytes | 8 bytes | 8 bytes |
long long | 8 bytes | 4 bytes | 8 bytes | 8 bytes |
pointer | 4 bytes | 4 bytes | 8 bytes | 8 bytes |
size_t | 4 bytes | 4 bytes | 8 bytes | 8 bytes |
NSInteger | 4 bytes | 4 bytes | 8 bytes | 8 bytes |
CFIndex | 4 bytes | 4 bytes | 8 bytes | 8 bytes |
fpos_t | 8 bytes | 4 bytes | 8 bytes | 8 bytes |
off_t | 8 bytes | 4 bytes | 8 bytes | 8 bytes |
次のテーブルは、iOSとOS Xにおいて共通で使用される浮動小数点型を記載しています。ビルトインのデータ型のサイズは変更されていませんが、64ビットランタイムではCGFloat型はfloatからdoubleに変更されています。これにより、Core Graphicsの型を使用するQuartzなどのフレームワークでは、レンジはより大きくなり正確になります。
Floating-point type | ILP32 size | LP64 size |
---|---|---|
float | 4 bytes | 4 bytes |
double | 8 bytes | 8 bytes |
CGFloat | 4 bytes | 8 bytes |
64ビットARM環境は、リトルエンディアンを使用します。これは、ARMプロセッサを用いたデバイスで動作する32ビットiOSランタイムと同じです。
アプリを32ビットと64ビットの両方のランタイム環境向けにコンパイルするときは、環境によってアプリの振る舞いが異なり、パフォーマンスの差や互換性の問題をもたらすことに留意してください。
-
メモリ負荷の増大
多くの基本的な型のサイズが大きくなるため、64ビット版のアプリは32ビット版よりも多くのメモリを使用します。例えば、リンクリストのようなシンプルなものであっても、64ビットランタイム向けにコンパイルするとメモリ使用量が多くなります。
-
64ビットと32ビットのソフトウエア間でのデータ交換
32ビットと64ビットのソフトウエアをリリースする場合、同じデータを両方のバージョンから操作する必要がある場合があります。例えば、iCloudに保存してあるファイルに対して、ユーザーは32ビットと64ビットの両方のデバイスからアクセスするかもしれません。あるいは、デバイス間でネットワークデータを送信するゲームを作るかもしれません。両方のランタイムにおいて、読み書きされるデータは共通のメモリレイアウトを使用するようにしてください。つまり、全てのデータエレメントのサイズとオフセットが同一となるようにしてください。
-
計算結果が異なる可能性
64ビットintegerは、32ビットintegerよりも非常に大きなレンジの値をサポートします。アプリが32ビットから64ビットに変更されたinteger型を使用する場合、その結果は64ビットバージョンでは異なる可能性があります。特に、32ビットintegerの最大値を超えるような計算をする場合、32ビットintegerではオーバーフローが発生しますが64ビットintegerでは発生しません。
-
大きなサイズのデータ型から小さなデータ型にコピーされた時に値が切り捨てられる可能性
いくつかのアプリの中には、2つのデータ型の間で値を安全に変換するのではなく、2つは同じであると仮定しているものがあります。64ビットランタイムにおいては、データ型についてのこの仮定はもはや正しくない可能性があります。例えば、NSIntegerの値をint型に代入する場合、32ビットランタイムにおいてはどちらの型も32ビットintegerなので動作します。しかし、64ビットランタイムにおいては、これら2つは同じ型ではないため、データが失われる可能性があります。
関数呼び出しが行なわれる時、コンパイラは呼び出し側から呼び出される側に対してパラメータを渡すコードを生成します。例えば、ABIは呼び出し側がパラメータをレジスタに格納するよう指定するかもしれません。または、呼び出し側が値をメモリ上のスタックにプッシュするよう指定するかもしれません。通常は、アセンブラ言語を書いているのではない限り、呼び出し規約はあまり重要ではありません。しかし、64ビットランタイムにおいては、関数呼び出しの方法を気にする必要がある場合があります。可変引数を受け取る関数は、固定のパラメータを受け取る関数と比べて、いくつかの異なる規約を使って呼び出されます。
64ビットランタイム向けのアプリをコーディングする場合、アプリは常に正確な定義に基づいて関数を呼び出す必要があります。
- 全ての関数はプロトタイプを持つ必要があります。
- 関数をキャストする場合、キャストしたバージョンの関数が、オリジナルの関数と同じシグネチャを持つように注意しなければいけません。特に、可変引数をとる関数のポインタを固定引数をとる関数にキャストするなど、異なるパラメータを持つ関数にキャストすることは避けてください。
もしObjective-Cランタイムをダイレクト扱うような低レベルのコードを書いている場合、オブジェクトのisaポインタにはもう直接アクセスすることはできないことに注意してください。代わりに、その情報にアクセスするためのランタイム関数を使用する必要があります。
64ビットARMインストラクションセットは、32ビットのインストラクションセットとくらべて大きく異なっています。もしあなたのアプリがアセンブリ言語のコードを含んでいる場合、新しいインストラクションセットを使用するよう書き換える必要があります。また、iOSの64ビット呼び出し規約はARM標準のものとは異なっていることに注意してください。
概要としては、あなたのコードを64ビットクリーンにするには、次のことに従う必要があります。
- 64ビット長のintegerを32ビット長のintegerに代入しないでください
- 64ビットポインタを32ビットintegerに代入しないでください
- 算術演算時にポインタとlong integerが切り捨てられないようにしてください
- データ型のサイズ変更に伴うアラインメントの問題を修正してください
- 32ビットと64ビットランタイムの間で共有されるメモリ構造が同じレイアウトとなるようにしてください
- アセンブリ言語を書き換え、新しい64bitオペレーションコードとランタイムを使用するようにしてください
- 可変引数型の関数を固定引数の関数にキャストしたり、その逆をしないでください
32ビットと64ビットランタイム環境の両方で動作するアプリを作るには、次のようなステップがあります。
- Xcode 5をインストールしてください。
- プロジェクトを開いてください。
Xcodeはプロジェクトを新しいフォーマットにするよう促します。プロジェクトを最新化すると、64ビット向けのアプリをコンパイルする際に、新しいウォーニングとエラーが表示されるようになります。
- プロジェクトの設定をiOS 7以降をサポートするよう変更します。
iOS 7より前のバージョンをターゲットにしていると、64ビットのプロジェクトをビルドすることができません。
- Build SettingsのArchitecturesを“Standard Architectures (including 64-bit)”に変更してください。
- 64ビットランタイム環境をサポートするようアプリを変更してください。
新しいコンパイラのワーニングとエラーが、このプロセスを容易にします。しかし、コンパイラが全ての仕事をしてくれるわけではありません。コードの調査にこのドキュメントを役立ててください。 6. 実際の64ビットハードウェア上でアプリをテストしてください。
iOSシミュレータも開発中は役に立ちます。しかし、関数呼び出し規約のようないくつかの変更は、アプリがデバイス上で動作する場合のみ確認可能です。 7. メモリパフォーマンスを調整するために、Instrumentsを使用してください。
以降では、Cocoa Touchアプリを64ビットランタイム環境にポーティングする際によく発生する問題について説明します。
ポインタをinteger型にキャストする理由はほとんどありません。ポインタ型を一貫して使用することによって、全ての変数がアドレスを保持するのに十分なサイズを持つようにすることができます。
例えば、次のコードはポインタをint型にキャストしています。これはアドレスを算術演算するためです。32ビットランタイムにおいては、このコードは動作します。int型とポインタは同じサイズだからです。しかし、64ビットランタイムにおいては、ポインタはint型より大きいので、この代入はポインタのデータを失います。この場合は、ポインタをネイティブサイズ分増やせばいいので、単にポインタをインクリメントしてください。
int *c = something passed in as an argument....
int *d = (int *)((int)c + 4); // Incorrect.
int *d = c + 1; // Correct!
もしポインタをinteger型に変換しなければならない場合は、切り捨てられないようにするために、常にuintptr_t型を使用してください。ポインタの値をintegerの算術演算で変更し、ポインタに変換して戻すと、基本的なエイリアシングルールに違反する可能性があるので注意してください。
多くの共通するプログラミングエラーは、コードの中でデータ型を一貫して使用しない場合に発生します。データ型を一貫して使用しないことについて、コンパイラがたくさんの問題を警告してくれますが、これらのパターンについていくつかのバリエーションを理解しておくと役に立ちます。
関数を呼び出すとき、結果を受け取る変数と関数の返り値の方を常に一致させるようにします。もし、返り値の型が受け取る変数の型より大きい場合、値は切り捨てられます。次の例は、この問題を示すシンプルなパターンです。PerformCalculation関数は、long integerを返します。32ビットランタイムにおいては、intとlongはどちらも32ビットです。そのため、コードが正しくなくてもint型への代入はうまく行きます。64ビットランタイムにおいては、代入時に返り値の上位32ビットが失われます。結果はlong integerに代入すべきです。こうすれば、どちらのランタイム上でも動作します。
long PerformCalculation(void);
int x = PerformCalculation(); // incorrect
long y = PerformCalculation(); // correct!
値をパラメータとして渡す場合も同じ問題が発生します。例えば、次の例では、64ビットランタイムにおいて、入力パラメータが切り捨てられます。
int PerformAnotherCalculation(int input);
long i = LONG_MAX;
int x = PerformCalculation(i);
次の例では、64ビットランタイムにおいて、返り値が切り捨てられます。返り値が関数の返り値の型の範囲を超えているからです。
int ReturnMax()
{
return LONG_MAX;
}
これらの例は、全てintとlongが同じであると仮定している事によるものです。ANSI C標準は、このように仮定をしていません。そして64ビットランタイム上で動作する際は、明らかに正しくありません。デフォルトでは、プロジェクトを最新化すると、-Wshorten-64-to-32コンパイラオプションが自動的に有効化されます。そしてコンパイラは値が切り捨てられる多くのケースについて自動的に警告します。プロジェクトを最新化しない場合は、明示的にこのコンパイラオプションを有効化すべきです。より冗長ですがより潜在的なエラーを見つけることができる-Wconversionオプションも含めるといいかもしれません。
Cocoa Touchアプリでは、次のinteger型が検索され、あなたがそれらを正しく使用しているか確認されます。
- long
- NSInteger
- CFIndex
- size_t (sizeofの結果)
両方のランタイム環境において、fpos_tとoff_t型は64ビットのサイズです。これらをint型に代入しないでください。
NSIntegerは、64ビットコードでサイズが変わります。NSInteger型はCocoa Touch全体で使用されています。NSIntegerは、32ビットランタイムにおいては、32ビットです。64ビットランタイムにおいては、64ビットです。そのため、フレームワークメソッドNSInteger型でから情報を受け取る場合は、NSInteger型の変数に格納してください。
NSInteger型がint型と同じサイズであると決して仮定してはいけませんが、いくつかの重要な例があります。
-
NSNumberオブジェクトからの、またはNSNumberオブジェクトに対する変換。
-
NSCoderクラスを使用したデータのエンコードとデコード。
特に、64ビットデバイス上でNSIntegerをエンコードし、その後32ビットデバイス上でデコードすると、値が32ビットintegerの範囲を超えた時にデコードメソッドは例外をスローします。代わりに明示的なinteger型を使用したほうがいいかもしれません。
-
フレームワーク内でNSIntegerとして定義されている定数を使用する場合。特に注意すべきなのは、NSNotFound定数です。64ビットランタイムにおいては、その値はint型の範囲より大きいものとなります。そのため、その値が切り捨てられると、エラーを引き起こす事になります。
CGFloatは、64ビットコードでサイズが変わります。CGFloat型は64ビット浮動小数点に変わります。NSInteger型と同じように、CGFloatがfloatまたはdoubleであると仮定してはいけません。
// Incorrect.
CGFloat value = 200.0;
CFNumberCreate(kCFAllocatorDefault, kCFNumberFloatType, &value);
// Correct!
CGFloat value = 200.0;
CFNumberCreate(kCFAllocatorDefault, kCFNumberCGFloatType, &value);
カスタマーは既に32ビットバージョンのアプリを使用しており、今後何年も使い続けます。常に64ビットintegerを使用するようコードを変換することが良い解決策のように思えるかもしれませんが、そうではありません。64ビットプロセッサは、64ビットintegerの操作を32ビットと同じくらいの速度で行うことができますが、32ビットプロセッサは、64ビットの操作を行うと非常に遅くなります。もしあなたが常に64ビット型を使用したら、あなたの32ビットアプリは現在のバージョンよりもパフォーマンスが悪くなるでしょう。そうではなく、計算して格納するのに必要な値の範囲にマッチするinteger型を使用してください。結果が32ビットintegerに収まるようであれば、32ビットintegerを使用してください。