ハイライト
Xcode 12 は、Mac アプリを arm64 と x86_64 の両方を含むユニバーサル アプリにコンパイルすることをサポートしており、開発者はバイナリの依存関係、低レベルのタイミング コード、同期プリミティブ、およびプラグイン アーキテクチャの一致をチェックする必要があります。
主要内容
Mac が Apple Silicon に移行した後、多くのチームにとって最大の懸念は書き換えです。長年実行されている Mac アプリには、AppKit UI、C コード、バイナリ フレームワーク、コマンド ライン ビルド スクリプト、プラグイン システム、およびパフォーマンス最適化コードが含まれている場合があります。 PowerPC から Intel に移行した過去の経験から、エンディアン、アーキテクチャ ブランチ、および多くの条件付きコンパイルが思い浮かびます。
このセッションの最初のステップは簡単です。Xcode 12 を開き、「My Mac」を選択して、「実行」を押します。講義内の Solar System の例は、プロジェクト設定を変更せずに Apple CPU アーキテクチャ上でコンパイルされ、実行されました。その理由は、Xcode 12、SDK、およびツールチェーンがすでにどの Mac でも arm64 と x86_64 をクロスコンパイルできるためです。高レベルの AppKit コードの場合、移行は通常のビルドから開始されることがよくあります。
問題は古い仮定にあります。コードで使用できるのは、__x86_64__「これは Mac です」という意味、またはおそらくmach_absolute_time()ナノ秒として、または Intel スライスのみに依存する.framework。このコードは長年にわたり Intel Mac 上で実行されており、arm64 のビルド、ネイティブの実行、またはプラグインのロード時にのみ公開されます。
Apple のルートは、移行をチェック可能なステップに分割することです。最初に Intel ビルドがまだ合格していることを確認し、次にネイティブ アーキテクチャに切り替えてコンパイル エラーを修正し、次にリンク時のバイナリ依存関係を処理し、最後に Apple Silicon Mac でテスト、デバッグ、パフォーマンス分析、プラグイン検証を実行します。ユニバーサル アプリはスイッチではなく、チェックリストです。
詳細
Xcode 12 からユニバーサル アプリを構築する
(02:19) Xcode 12 の実行先は、ネイティブ Apple Silicon、Rosetta を選択するか、Apple Silicon と Intel の両方をサポートするユニバーサル アプリを構築できます。この講演では、ツールチェーンは Intel Mac 上でクロスコンパイルできるため、ユニバーサル アプリの構築には開発マシンが Apple Silicon Mac である必要はないことが強調されました。
キーポイント:
- Mach-O ファイルには、CPU アーキテクチャを 1 つだけ含めることも、arm64 と x86_64 の両方を含むユニバーサル バイナリを含めることもできます。
-
lipo -infoスピーチ時の確認に使用します.a、.dylib、.framework、.xcframeworkスキーマ情報コマンド。 - コマンドラインからビルドするときに最初に使用されます
xcodebuild -showdestinations目的地を確認して利用する-destinationテストを実行するアーキテクチャを指定します。 -arch=arm64Apple Siliconネイティブテストパスに対応し、arch=x86_64Apple Silicon Mac 上の Rosetta を介して Intel コードを実行するためのテスト パスに対応します。
(09:04) ビルドの最初の落とし穴はページ サイズです。講演では、Apple Silicon システムのネイティブ ページ サイズは Intel マシンのネイティブ ページ サイズとは異なると述べられました。PAGE_SIZEコンパイル時の定数としては適切ではなくなりました。
キーポイント:
PAGE_MAX_SIZEコンパイル時に使用可能な最大ページ サイズの上限を表します。 -vm_page_size実行時に現在のプロセスによって認識されるページ サイズを返します。- Rosetta は Intel 環境に適合し、変換されたプロセスは引き続き 4 KB ページをサポートするため、この問題はネイティブ パスでテストする必要があります。
CPU 条件の代わりにセマンティック条件を使用する
(10:09) 講義中の関数例GetDefaultTimerClass使用__x86_64__macOS とシミュレータを表します。 Apple Silicon Mac では、この条件は失敗します。 CPU アーキテクチャは命令セットを表し、プラットフォームの条件はシステム環境を表します。この 2 つを混合することはできません。
キーポイント:
TARGET_OS_OSXそしてスイフトの#if os(macOS)「このコードは macOS で実行されます」と表現します。 -TARGET_CPU_X86_64そしてスイフトの#if arch(x86_64)コードが実際に Intel 命令または ABI に依存している場合にのみ使用してください。- Intel アセンブリまたは SSE/AVX 実装のみがある場合は、arm64 に 2 番目の実装を提供するか、代わりに Accelerate や Compression などのシステム フレームワークを使用する必要があります。
非ユニバーサルバイナリ依存関係を修正する
(12:54) 講演中のデモ アプリは arm64 リンク段階で失敗し、プリコンパイルされたバイナリ フレームワークからエラーが発生しました。リンカーレポートignoring file when building for macOS-arm64、プロジェクトが x86_64 のみを含むファイルをリンクしていることを示します。
キーポイント:
- リンク時間
Undefined symbols for architecture arm64下流のバイナリに arm64 スライスが存在しない可能性があります。 -.frameworkディレクトリ自体は Mach-O ファイルではありません。フレームワーク内の実際の実行可能ファイルを確認してください。 - 移行時にスキャンする
.a、.dylib、.frameworkそして.xcframework、次に使用しますlipo -infoプリコンパイルされた各依存関係がユニバーサルであるかどうかを確認します。 - サードパーティのバイナリでは、サプライヤーがユニバーサル ビルドをリリースする必要があります。短期的なトラブルシューティングのために依存関係を一時的に削除することはできますが、リリース前にユニバーサル バージョンに戻す必要があります。
mach_absolute_time をナノ秒として扱わないでください
(21:19) トークの実行時のバグが進行状況バーの更新頻度に表示されます。古いコードmach_absolute_time()ティックをナノ秒として扱い、次の値で割ります。1_000_000_000秒を取得します。この仮定は、異なる CPU アーキテクチャには当てはまりません。
// Don’t assume the time is returned in nanoseconds
func monotonicTimestampInSeconds() -> Double {
let ticks = mach_absolute_time()
let seconds = Double(ticks) / 1_000_000_000
return seconds
}
キーポイント:
mach_absolute_time()ティック単位を返します。ティックが 1 ナノ秒に等しいとは保証されません。- コードはティックをナノ秒として扱うため、一部のアーキテクチャではタイマーが遅くなります。講演では、一般的な現象は約 40 倍遅いと述べました。
- このタイプのバグは通常、コンパイル中には露出されないため、Apple Silicon Mac 上のネイティブ パスを実行する必要があります。
(21:40) 単調なタイムスタンプが本当に必要な場合、講演では代わりにナノ秒を返すタイムスタンプを使用することを推奨しています。clock_gettime_nsec_np(CLOCK_UPTIME_RAW)または、Foundation タイマーと GCD タイマーを使用します。
// Use clock_gettime_nsec_np to read timestamp in nanoseconds
func monotonicTimestampInSeconds() -> Double {
let nanoseconds = clock_gettime_nsec_np(CLOCK_UPTIME_RAW)
let seconds = Double(nanoseconds) / 1_000_000_000
return seconds
}
キーポイント:
clock_gettime_nsec_np(CLOCK_UPTIME_RAW)ナノ秒を直接返すと、後続の除算には明確な単位が付きます。- 関数名は秒のセマンティクスを保持し、呼び出し元は基になる時刻 API が置き換えられたことを知る必要はありません。
- コードベースの検索
mach_absolute_time移行チェックにおける低コストのアクションです。
スピンロックとビジーウェイトをブロッキング同期に置き換えます。
(26:40) Apple Silicon Mac には、パフォーマンス コアとエネルギー効率コアがあります。システムはパフォーマンスと電力に基づいて動的にスケジュールを設定します。この講演では、スピンロックとビジーウェイトがパフォーマンス コアを無駄に占有し、全体の作業完了時間が遅くなる可能性があることを特に思い出させました。
func performWorkUnderSpinlock() {
spinlock_lock()
performWork()
spinlock_unlock()
}
func retrieveNextWorkTask() -> WorkTask {
while true {
let task = queue.sync { taskQueue.pop() }
if let task = task { return task }
else { continue }
}
}
キーポイント:
spinlock_lock()ロックを取得できない場合、CPU を消費し続けます。 -while trueこのループは、キュー内にビジー待機状態のタスクがあるかどうかを繰り返しチェックします。- 非対称 CPU システムでは、このような待機により、実際のタスクの実行に適した P コアが占有される場合があります。
(27:03) 別の方法は、ブロック ロックと条件変数を使用することです。スレッドが続行できない場合は、CPU を放棄し、条件が満たされるまで待ってから再開する必要があります。
func performWorkUnderSpinlock() {
os_unfair_lock_lock()
performWork()
os_unfair_lock_unlock()
}
func retrieveNextWorkTask() -> WorkTask {
condition.lock()
while !taskQueue.hasAnyWork {
condition.wait()
}
let task = taskQueue.pop()
condition.unlock()
return task
}
キーポイント:
os_unfair_lock_lock()これはブロッキング同期プリミティブであり、スピン待機の置き換えに適しています。 -condition.wait()タスクがないときにスレッドをブロックし、CPU を繰り返し占有しないようにします。- 講演では、NSLock、pthread mutex、NSCondition、および pthread 条件変数もリストされ、GCD が利用可能な場合は最初に GCD を使用することを推奨しました。
プラグインはプロセス アーキテクチャと一致する必要があります
(30:16) プラグイン移行の中核ルールは単純です。同じプロセス内のすべてのコードは同じ CPU アーキテクチャを使用する必要があります。インプロセスプラグインの場合dlopenまたはBundle.loadホストがネイティブ arm64 である場合、ロードするには、プラグインにも arm64 スライスが必要です。ホストが Rosetta で実行されている場合、プラグインは Intel コードとしてロードできる必要があります。
void *plugin_module = dlopen("./path/to/plugin.dylib", RTLD_NOW);
if (plugin_module == NULL) {
fprintf(stderr, "loading module failed:\n");
fprintf(stderr, "%s\n", dlerror());
return 0;
}
キーポイント:
dlopen戻るNULLいつ呼び出す必要がありますかdlerror()の場合、エラー メッセージは、ディスク上のファイル スキーマが一致しないことを示します。- ファーストパーティのプラグインはハードコードされたものを削除する必要があります
x86_64Xcode が標準アーキテクチャを使用するようにアーキテクチャを設定します。 - サードパーティのインプロセス プラグインが依然として Intel 専用である場合、ユーザーは Rosetta 経由でしかアプリを強制的に開くことができない可能性があります。
- アウトオブプロセス プラグインは、XPC を通じて独立したプロセスに分離できます。アプリは、アーキテクチャによって区別される最大のプラグイン プロセスを維持できます。
公開する前に 3 種類のクラッシュ ログを読む
(36:39) Xcode Organizer には、アーカイブに含まれる CPU アーキテクチャが表示されます。また、クラッシュ ログには、クラッシュが発生したアーキテクチャと、プロセスが変換された状態にあるかどうかも表示されます。ユニバーサル アプリがリリースされると、開発者は Intel Mac で x86 クラッシュ、Apple Silicon Mac でネイティブ arm64 クラッシュ、Apple Silicon Mac でロゼッタ パスの変換された x86 クラッシュを確認することになります。
キーポイント:
- アーカイブは実際に初めてユニバーサル製品をビルドする可能性があるため、デバッグ ビルドに合格してもアーカイブ ビルドも合格するとは限りません。
- 公開する前に、アーカイブ製品に arm64 および x86_64 が含まれていることを確認し、ネイティブ arm64 テストと Rosetta x86_64 テストをそれぞれ実行します。
- プラグインとバイナリ依存関係を引き続き使用する必要がある
lipo -info特定のパスが Intel 環境でのみ機能することが判明するだけの公開を避けるためにチェックを入れます。 - Thread Sanitizer は、移行後に公開されたデータ競合のトラブルシューティングに適しています。講演では、IntelとApple Siliconのメモリ順序モデルが異なると指摘した。
- 一部のカーネル拡張機能は廃止されているか、Apple Silicon Mac では許可されていないため、カーネル拡張機能および DriverKit 関連ソフトウェアは、さらに macOS 移植ドキュメントを読む必要があります。
重要ポイント
1. 既存の Mac アプリ用の 1 ページのユニバーサル移行ダッシュボードを作成する
- やるべきこと: チーム内でスクリプトを作成し、スキャンします
.a、.dylib、.framework、.xcframework、mach_absolute_time、__x86_64__、PAGE_SIZEそしてハードコーディングされたアーキテクチャ。 - 実行する価値がある理由: セッション移行の問題のほとんどは、静的スキャンによって事前に発見できます (特にバイナリの依存関係やアーキテクチャ条件)。
- 開始方法: シェルまたは SwiftPM コマンド プラグインでラップする
find、lipo -info、rg、各ターゲットに arm64 スライスがあるかどうか、および手動確認が必要なソース コードの場所を出力します。
2. Mac テストを CI で arm64 パスと x86_64 パスに分割します。
- 対処方法: 同じテスト スキームを別々に使用します。
arch=arm64そしてarch=x86_64実行して、どのパスが失敗したかを記録します。 - 実行する価値がある理由: このスピーチでは明らかにネイティブとロゼッタの両方をテストする必要があります。 1 つのアーキテクチャのみを実行すると、移植性のないコードが欠落します。
- 開始方法: Apple Silicon ランナーについて
xcodebuild test2 つの宛先を追加し、パフォーマンス テストの結果をアーキテクチャごとにダッシュボードにアップロードします。
3. プラグイン アプリにアーキテクチャ診断パネルを追加する
- 内容: プラグインのロードに失敗した場合は、
dlerror()、プラグイン パス、ホストの現在のアーキテクチャ、およびlipo -info結果はユーザーに表示されるか、診断ログに書き込まれます。 - 実行する価値がある理由: インプロセス プラグインの失敗は、アーキテクチャの不一致が原因で発生することがよくあります。一般ユーザーには機能障害しか見えず、プラグインを更新するべきか、Rosetta に切り替えるべきかを判断するのは困難です。
- 開始方法:
dlopenまたはBundle.load失敗したブランチは、「プラグイン ベンダーはユニバーサル ビルドを提供する必要があります」というプロンプトとともにエラーを収集し、ユーザーにサポート ドキュメントへの入り口を開きます。
4. 低レベルのパフォーマンス コードの待機戦略を書き直す
- 対処方法: スピン ロック、空のループ キュー、CPU の数に応じたタスクの分割の古い実装を見つけて、それらを GCD、ブロック ロック、または条件変数に変更します。
- 実行する価値がある理由: Apple Silicon Mac の P コアと E コアは動的にスケジュールされ、ビジーウェイトによりパフォーマンス コアが無駄になります。
- 開始方法: まず、最もホットなパスのスピンロックを置き換えてから、Instruments を使用して、ネイティブ arm64 での CPU 使用率とタスク完了時間を比較します。
5. 時間関連コードのアーキテクチャ回帰テストを確立する
- 作業内容: 更新頻度、タイムアウト、スロットリング、手ぶれ補正、ベンチマーク タイミングに関する単体テストまたは UI テストを作成します。
- 実行する価値がある理由:
mach_absolute_time()ティック仮定エラーは、「数十倍遅い」実行時のバグとして現れ、コンパイラは警告しません。 - 開始方法: 検索
mach_absolute_timeを優先して使用してください。clock_gettime_nsec_np(CLOCK_UPTIME_RAW)または、Foundation/GCD タイマーを使用して、arm64 および x86_64 宛先で同じテスト セットを実行します。
関連セッション
- Apple Silicon Mac の新しいシステム アーキテクチャを探索する - まず、SoC、ユニファイド メモリ、Rosetta、セキュア ブートなどのプラットフォームの基盤を理解してから、アプリの移行を決定します。
- Apple Silicon Mac 上の iPad および iPhone アプリ — 既存の Mac アプリのユニバーサル移行と、そのまま Mac に導入される iOS アプリの配布パスを比較します。
- オーディオ ワークグループの紹介 — オーディオ アプリとオーディオ プラグインは、Apple Silicon 上のリアルタイム スレッド コラボレーションに引き続き参入しています。
- Metal アプリを Apple シリコン Mac に導入する - グラフィックスを多用する Mac アプリでは、Apple GPU と Metal の移行の詳細についてさらに理解する必要があります。
- DriverKit を使用して PCI および SCSI ドライバーを最新化する - ドライバーまたはカーネル拡張機能に依存するソフトウェアは、DriverKit ルートを学習する必要があります。
コメント
GitHub Issues · utterances