HN 日本語サマリー

← 一覧へ戻る
AI・機械学習

バイナリ翻訳とその結果について

On Binary Translation and Its Consequences (chipsandcheese.com)

40 pointsby matt_d16 コメント

要約

この記事では、PC市場におけるx86-64バイナリをaarch64(ARM64)に翻訳する技術、特にWindows 11の新しいバイナリ翻訳エンジン「Prism」に焦点を当てています。バイナリ翻訳はパフォーマンスの低下を伴いますが、Qualcommの最新CPUではその影響が軽減される傾向にあることが、Geekbench 7のベンチマーク結果から示されています。

全文翻訳

バイナリ翻訳とその影響について

PC市場は、CPUデザイナーにとって最も競争の激しい分野の一つです。PCセグメントの消費者は、幅広いアプリケーションで高いパフォーマンスを期待しており、制約された、キュレーションされたユースケースを超えてデバイスが機能することを期待しています。Microsoft自身のSurface RTは、PCユーザーがWindowsで当然動作すると期待するプログラムを実行できなかったため、10年以上前に顕著な失敗をしました。Qualcommなどが最近リリースした64ビットArmコアは、PCのパフォーマンス要件を満たすには十分高速ですが、ソフトウェアの互換性がより困難な障害となっています。PCソフトウェアは伝統的にx86-64向けにビルドされています。開発者が64ビットArm(aarch64)バージョンのプログラムを提供するプロセスは、遅く、段階的です。一部のプログラムは、アクティブな開発下にないため、バイナリ形式でのみ配布された可能性があり、aarch64ポートが得られないかもしれません。したがって、ArmがPC市場で成功する可能性は、x86-64バイナリがaarch64ホストでシームレスに実行されることを保証するためのMicrosoftの取り組みにかかっています。

[画像: Windows 11 on Armデバイスの概念図]

Windows 11の最新バイナリトランスレーターであるPrismは、x86-64命令をaarch64命令に翻訳することでこれを実現します。バイナリ翻訳は、x86-64命令がaarch64命令に単純にマッピングされない場合があるため、困難です。また、プログラムの起動遅延を最小限に抑え、翻訳に過剰なCPU時間を消費しないように、翻訳は高速である必要があります。これらすべては、バイナリ翻訳にはネイティブコードを実行する場合と比較してパフォーマンスのペナルティが伴うことを意味します。

ここでは、Geekbench 7を使用してそのペナルティがどのようなものかを確認します。Geekbench 7にはaarch64版とx86-64版の両方があり、バイナリ翻訳を介して実行した場合のパフォーマンスとネイティブaarch64パフォーマンスを比較する機会を提供します。John Poole(Primate Labsの創設者、Geekbenchのクリエイター)のご厚意によりプロキーを提供していただいたので、個々のワークロードをプロファイルできます。もちろん、1つのベンチマークスイートでは、幅広いアプリケーションの動作を反映することはできませんが、これは良い出発点だと考えています。ハードウェアについては、AsusからサンプリングされたAsus Zenbook A16ラップトップに搭載されているSnapdragon X2 Elite Extreme X2E-96-100と、Microsoft Azureで利用可能なArmインスタンスでテストしています。

Geekbench 7 命令の特性

Geekbench 6と同様に、Geekbench 7には、利用可能な場合にベクトル拡張を活用するいくつかのワークロードがあります。このスイートは、高いIPC(Instructions Per Clock)とCPUバウンドなワークロードに偏っています。IntelのSoftware Development Emulator(SDE)をHaswellの機能セットを公開するように設定してワークロードを実行すると、ワークロードの半分以上がAVXと256ビットベクトル幅を使用していることがわかります。ワークロード自体ではなく、Geekbenchのテストハーネスからの命令が大部分を占めるように、各ワークロードを200イテレーションで実行しています。

[画像: Geekbench 7のワークロードにおけるAVX命令の使用状況]

200イテレーションで、各ワークロードは約数兆命令を実行します。さまざまなaarch64コアからのパフォーマンスカウンターデータは、Geekbench 7のネイティブaarch64バイナリを実行した場合、時折例外はあるものの、命令数はおおむね類似していることを示しています。Qualcommのコアは、多くのテストでNeoverse N1およびN2よりも高いリタイア命令数を報告しており、これはハードウェアパフォーマンスモニタリングに何らかの不正確さがある可能性を示唆しています。

[画像: Neoverse N1/N2とQualcommコアの命令実行数比較]

バイナリ翻訳を介してGeekbench 7のx86-64バージョンを実行すると、PDF Viewerを除くすべてのワークロードで命令数が劇的に増加します。一般的に、Geekbench 7のワークロードは、バイナリ翻訳を介して実行される場合、x86-64命令よりも約2倍のaarch64命令を実行します。

[画像: バイナリ翻訳時の命令数増加率の比較]

Microsoftは、バイナリ翻訳がすべてのaarch64 CPUで同じように機能するわけではないと示唆しています。異なるCPUは異なる命令セット拡張をサポートしており、その一部はPrismがx86-64命令をaarch64命令に、より密接にマッピングできるようにする可能性があります。また、aarch64 CPUは、ISAが要求するもの以上の保証を提供できる可能性があります。例えば、aarch64では1つのコアがプログラムの順序外のストアを観測できるとしても、ストアの順序を維持するなどです。

Prismは、Qualcomm Snapdragonプロセッサに特化して最適化およびチューニングされています。Prism内の一部のパフォーマンス機能は、Snapdragon Xシリーズのみで利用可能なハードウェア機能を必要としますが、PrismはWindows 11 24H2がインストールされた、サポートされているすべてのWindows 11 on Armデバイスで利用可能です。

PrismがQualcommのコアに特化して最適化されている場合でも、命令数の観点からは違いはありません。私がテストした中で最も古いコアであるAmpere AltraのNeoverse N1では、パフォーマンスカウンターは非常に類似した実行命令数を示しています。

[画像: Neoverse N1での命令実行数]

X軸は実行命令数(兆単位)です。

もちろん、類似した命令数でも大きな違いが隠されている可能性があります。最終的に重要なのはパフォーマンスであり、それはコアアーキテクチャと生成される命令の性質の両方に影響されます。

スコアへの影響

Geekbench 7のスコアは、バイナリ翻訳による厳しいペナルティを示しています。Qualcommのコアを含め、すべてのコアが数世代分のパフォーマンス低下を経験しています。しかし、Snapdragon X2 Eliteのコアは、Neoverse N1およびN2よりもはるかに高いベースラインパフォーマンスを持っています。Snapdragon X2 Eliteは、「パフォーマンス」コアの6コアクラスターと、「プライム」コアの2つの6コアクラスターを備えています。「パフォーマンス」コアは6ワイドで、3.6 GHzで動作します。これらは他のチップの効率コアと同じ役割を果たすため、簡単のためE-Coreと呼びます。「プライム」コアは9ワイドで、5 GHzで動作します。これらは最大のパフォーマンスを目指しているため、P-Coreと呼びます。

[画像: Snapdragon X2 Eliteのコア構成]

QualcommのE-Coreは、バイナリ翻訳のペナルティを考慮しても、Neoverse N1を上回るパフォーマンスを発揮します。これはP-CoreがNeoverse N2を上回る場合も同様です。非常に高いクロック速度で動作する、はるかに広いコアに対して、バイナリ翻訳のペナルティがあっても、異論の余地はありません。

[画像: 各コアのGeekbench 7スコア比較(ネイティブ vs バイナリ翻訳)]

Qualcommのコアは、バイナリ翻訳からのペナルティもわずかに低くなっています。これがQualcomm固有のPrismでの最適化によるものなのか、それともQualcommのコアがはるかに高いスループットを持っているためなのかは判断が難しいです。バイナリ翻訳による命令数の増加は、ClaudeのCコンパイラ(CCC)をいじっていた頃を思い出させます。CCCの場合、クリティカルパスから外れた余分な命令が多く生成され、アウトオブオーダーCPUはしばしばそれを吸収できました。最近の高性能コアは通常、コア幅の大部分を使用していないため、余分なコア幅は高い命令数をある程度緩和できます。しかし、ここではそうではないかもしれません。なぜなら、Qualcommの4ワイドE-Coreは、5ワイドのNeoverse N2よりも軽いペナルティで済んでいるからです。

[画像: バイナリ翻訳ペナルティとコア幅/IPCの関係]

個々のワークロードは、バイナリ翻訳下で実行されると、さまざまな程度で影響を受けます。Navigationは、分岐予測とバックエンドメモリレイテンシに大きく依存する低IPCワークロードであり、バイナリ翻訳下で実行されると比較的低いペナルティを受けます。スペクトルの反対側では、Video PlayerはAVX命令を活用する、十分にベクトル化されたワークロードです。このテストでのスコア差は絶対的に大きいです。Photo EditorとPhoto Libraryも同様の状況です。

[画像: 各ワークロードにおけるバイナリ翻訳の影響度比較]

ArmのNeoverse N1およびN2では、バイナリ翻訳下でのワークロード特性はほとんど変わりません。

[画像: Neoverse N1/N2でのワークロード別パフォーマンス比較]

[画像: Qualcommコアでのワークロード別パフォーマンス比較]

コア スループット (IPC)

パフォーマンスカウンターは、Qualcommのコアが未使用のコア幅を活用することで、バイナリ翻訳のオーバーヘッドを軽減していることを示しています。しかし、IPCの増加は命令数オーバーヘッドと比較するとわずかであり、これが大きなバイナリ翻訳ペナルティを説明しています。Snapdragon X2 Elite ExtremeのP-Coreは、Video Playerワークロードで大幅なIPC増加を達成していますが、他の場所での増加は控えめです。

[画像: Qualcomm P-CoreのIPC分析(ネイティブ vs バイナリ翻訳)]

QualcommのE-Coreも同様の状況ですが、より狭いコアであるため、コア幅をより有効に活用できます。Text Processing、HDR、Video Playerでは、バイナリ翻訳下で実行されると、コアが4ワイドの幅制限に達しています。しかし、Text ProcessingとHDRでQualcommのP-Coreに見られる控えめなIPC増加は、ワークロードがすぐに他の制限にぶつかる可能性があるため、E-Coreを広くしてもあまり効果がないことを示唆しています。

[画像: Qualcomm E-CoreのIPC分析(ネイティブ vs バイナリ翻訳)]

Neoverse N2は、低いIPCから始まり、バイナリ翻訳下でx86-64を実行してもIPCがほとんど増加しないという奇妙な結果を示しています。ネイティブワークロードのIPCを見ると、コアは絶対的な意味では良好に機能しますが、Qualcommの最新の効率最適化コアと比較すると見劣りします。Neoverse N2では、バイナリ翻訳の有無にかかわらず、どのワークロードも3 IPCを超えることはありません。

[画像: Neoverse N2のIPC分析(ネイティブ vs バイナリ翻訳)]

Neoverse N1は、このセットの中で最も古いコアであり、予想通り最も苦戦しています。Ampere AltraのN1コアは、サーバー市場への参入を主導し、x86-64の競合他社よりも密度最適化されながら十分なパフォーマンスを提供できたため成功しました。しかし、それは数年前のことです。QualcommのE-Coreも4ワイドであり、N1を大きく引き離しています。

[画像: Neoverse N1のIPC分析(ネイティブ vs バイナリ翻訳)]

QualcommのE-Coreからの比較的良好なパフォーマンスは、コア幅が他のアーキテクチャ特性と比較して相対的にほとんど意味をなさないことを思い出させます。Qualcommは、Neoverse N1よりもはるかに大きなアウトオブオーダーエンジンを最新のE-Coreに搭載しています。これにより、コアはより多くの命令をインフライト状態に保つことができ、個々の命令に対する短時間の遅延の影響を軽減します。バイナリ翻訳からの命令オーバーヘッドは、アウトオブオーダーリソースへの圧力を増加させ、Qualcommのより堅牢なコアはそのオーバーヘッドを吸収する可能性が高くなります。

トップダウン分析

ディスパッチステージのパイプラインスループットを考慮すると、ネイティブで実行されるワークロードとバイナリ翻訳で実行されるワークロードの間で、おおむね類似した状況が見られます。これは、同じ高レベルの作業を行っているはずなので、驚くことではありません。QualcommのP-Coreでは、最も大きな違いは、より多くのパイプラインスロットが使用されていることと、フロントエンドバウンドであることが少なくなっていることです。

[画像: Qualcomm P-Coreのトップダウン分析(ネイティブ vs バイナリ翻訳)]

Qualcommのより狭いE-Coreは、多くのテスト、特にバイナリ翻訳下で実行されるx86-64バージョンで、コア幅の大部分を使用します。狭いコアは供給しやすく、フロントエンドの理由で失われるパイプラインスロットは、より広いP-Coreと比較してはるかに少なくなります。

[画像: Qualcomm E-Coreのトップダウン分析(ネイティブ vs バイナリ翻訳)]

Azureでは、プラットフォームがパイプラインスロットの粒度でアカウンティングを行うトップダウンカウンターをパススルーしないため、詳細なパフォーマンスモニタリングは困難です。しかし、サイクルレベルのアカウンティング(STALL_FRONTENDとSTALL_BACKEND)で大まかにスケッチすると、Neoverse N2はQualcommのE-Coreよりもバックエンドとフロントエンドの両方の理由でより多くのパイプラインスロットを失っていることがわかります。一方、5ワイドパイプラインに供給するのは4ワイドパイプラインよりも困難です。また、Qualcommはより大きなコアプライベートキャッシュと、レイテンシ21サイクルのE-Coreクラスター用のかなりの12MB L2を備えています。Neoverse N2の1MB L2をミスすると、コアは約100サイクルのL3レイテンシにさらされ、これはどのコアにとっても対処が困難です。バイナリ翻訳はこの点でほとんど変化しません。

[画像: Neoverse N2のトップダウン分析(Azureインスタンス)]

バックエンドのストールは、100サイクルを超えるL2ミスレイテンシにも対処しなければならないNeoverse N1ではさらに問題となります。N1は、より小さなアウトオブオーダーエンジンにより、より深刻な影響を受けます。

[画像: Neoverse N1のトップダウン分析(Azureインスタンス)]

QualcommのE-Coreとの比較は、Neoverse N1にとって状況がいかに厳しいかを示しています。Qualcommのコアは、同じコア幅と類似した実行リソースを備えているにもかかわらず、2倍以上の命令をインフライト状態に保つように設計されています。バイナリ翻訳からの追加命令は、アウトオブオーダーリソースへの圧力を増加させ、Qualcommのより堅牢なコアはそのオーバーヘッドを吸収する可能性が高くなります。

[画像: Neoverse N1とQualcomm E-Coreの比較(アウトオブオーダーリソース)]

翻訳されたコード

Prismは、翻訳されたコードをC:\Windows\XtaCacheに保存します。生成されたコードをディスクに書き込むことには2つの機能があります。第一に、Prismはバイナリが起動されるたびにコードを再翻訳するオーバーヘッドを回避できます。第二に、Windowsユーザーが翻訳されたコードを調べて、x86-64バイナリがaarch64ホストでどのように実行できるかを理解できるようにします。これは、ユーザーがハードウェアを理解するのに役立つため重要です。XtaCacheのこの点での有効性は、Microsoftが.jcキャッシュフォーマットを文書化し忘れたため限定的でしたが、幸いにも他の人々が文書化に貢献しています。残念ながら、翻訳されたコードを掘り下げるのは依然として非常に時間のかかる作業です。ここでは、Prismの翻訳がどのようなものかを示すために、いくつかの例を挙げるに留めます。

Geekbench 7のVideo Playerワークロードは、バイナリ翻訳下で実行されるとIPCが大幅に向上し、実行命令数の大幅な増加も経験するため際立っています。SkylakeでVTuneを使用してVideo Playerをプロファイルすると、最もホットな基本ブロックは17命令のループであることがわかります。このループは配列をトラバースし、AVX FMA3命令を使用して融合積和演算を実行します。

[画像: SkylakeでのVideo Playerワークロードのホットブロック]

PrismはこれをNeoverse N1とSnapdragon X2 Eliteの両方で69命令のシーケンスに翻訳します。AVX命令はNEON命令にマッピングされますが、NEONは128ビットのベクトル幅しかありません。Qualcommの最新コアはSVEをサポートしていますが、QualcommのコアはSVEを128ビットベクトル長で実装しているため、使用してもあまり違いはないでしょう。また、固定幅のx86-64命令は、SVEの可変ベクトル長にはうまくマッピングされないでしょう。

[画像: PrismによるAVX命令のNEONへの翻訳例]

命令オーバーヘッドは、複数のNEON命令を使用して256ビットベクトル演算を実行すること、x86-64のより柔軟なアドレッシングモードを再現すること、そして単一の命令でロードと数学演算の両方を指定するのではなく、個別の命令でメモリアクセスを実行する必要があることから生じます。例えば、vbroadcastss ymm8, [r8+r10*4]は、単精度スカラー(32ビット)値をロードし、それをすべてのベクトルレーンにブロードキャストします。Prismは3つのaarch64命令を使用します。1つは値をロードし、もう1つはそれを128ビットNEONレジスタのレーンに複製し、最後の1つは値をymm8の上位128ビットを表す別のNEONレジスタにコピーします。

[画像: Prismによるvbroadcastss命令の翻訳詳細]

融合積和命令はさらに拡張されます。vfmadd231ps ymm7, ymm8, [rax + r9*4 - 0x13c]は、ベース、スケールインデックス、および即時オフセットを持つ、x86-64で最も複雑なアドレッシングモードを使用します。Prismは2つの命令でアドレッシング計算を実行し、インデックスのスケーリングと配列ベースアドレスへの加算の両方を単一の加算(add-with-scale)にまとめることができます。次に、ロードペア(LDP)命令は、2つの128ビットNEONレジスタへの256ビットロードを指定します。Prismは、2つのFMLA命令でFMA演算を実行しますが、奇妙なことにymm7の上位半分を保持するNEONレジスタをスタックにスピルします。これは、ループの8つのFMA命令すべてで再度行われ、キャッシュ帯域幅の要求を倍増させます。スピルされたレジスタは次のループイテレーションの前に変更されないため、スピルと再ロードは完全に不要であり、すでに高い帯域幅要求を持つループでは有害である可能性があります。ループ本体の終盤で、Prismはソフトウェアプリフェッチ命令をaarch64の同等物に忠実に翻訳します。これには、アドレスを生成するための追加のALU命令が含まれます。その後、ループカウンターのインクリメントと制御フロー命令はaarch64の同等物に直接翻訳されますが、もちろんジャンプターゲットアドレスは異なります。

[画像: Prismによるvfmadd231ps命令の翻訳詳細]

人間による翻訳者であれば、Prismの不要なレジスタスピルを避けるでしょう。また、配列の線形トラバースは予測可能なアクセスパターンを作成し、ハードウェアプリフェッチャーがそれを拾い上げる可能性が高いため、プリフェッチ命令を省略することもできます。また、ブロードキャストロードの宛先が変更されないため、ymm8の両半分を単一のレジスタに保持するなど、いくつかの命令を省略することもできます。しかし、Prismは、rax + r9*4がすでに生成されていることを認識し、8つのFMA操作のうち6つについて、減算のみを発行することでアドレス生成を単純化している点は評価に値します。

Geekbench 7のHDRワークロードも調べました。これは、バイナリ翻訳されたバージョンがほとんどIPCを増加させないためです。Skylakeでプロファイルした結果、最もホットなブロックは36命令のループで、スカラーAVX操作を使用しています。これは、PrismがAVXのより広いベクトル幅をNEONにマッピングする必要がないため、翻訳が容易になるはずです。

[画像: SkylakeでのHDRワークロードのホットブロック]

Prismはこれを60以上の命令に翻訳します。翻訳キャッシュからNeoverse N1 VMから取得したとき、完全な翻訳は利用できないようでした。

[画像: PrismによるHDRワークロードの翻訳例]

Prismは、x86 SSEレジスタが同番号のaarch64 NEONレジスタにマッピングされるようにレジスタマッピングを維持しようとすることで、大幅な命令オーバーヘッドを導入します。例えば、Geekbench 7が命令シーケンス内でFP加算(vaddss xmm5, xxm4, [rdi+rbx*4])を行い、その結果をメモリに格納する(vmovss [rdi+rbx*4], xmm5)場合、Prismのaarch64翻訳はFP加算の結果をs18に格納し、その後v5に移動してから格納します。これは、v5をxmm5を表すために使用するためです。追加のオーバーヘッドは、説明のつかないスタックへのストアや、右シフト命令のキャリーフラグを生成しているように見える追加命令から生じます。多くのx86-64命令はフラグを設定しますが、aarch64にはフラグ設定の同等物がないものもあります。Prismは、フラグが使用されているかどうかを確認するために計算能力を費やすよりも、フラグ生成動作を維持することを好むようです。

[画像: Prismによるvaddss/vmovss命令の翻訳詳細]

PrismはQualcommのSnapdragon X2 Eliteに対してもほとんど同じシーケンスを生成しますが、ロードとストアの周りで違いがあります。Qualcommの新しいコアはFEAT_LPCPC2をサポートしており、これによりldaprb(ロードアキア)の代わりにldapurb(ロードアキアと即時オフセット)を使用できます。即時オフセットを使用できると、アドレッシング生成が単純化され、いくつかの追加命令が削減されます。どちらの場合も、Prismはメモリ順序を確保するために、aarch64の基本的なLDRではなく、ロードアキア命令にロードを翻訳します。

[画像: Snapdragon X2 EliteでのPrismコード生成例]

Snapdragon X2 EliteでのPrismコード生成

最終的な言葉

バイナリ翻訳は、Arm on WindowsをPC市場で成功させるための鍵となります。Prismの翻訳はほとんどが直接的です。x86-64命令はその動作が同等のaarch64シーケンスによって忠実に再現されるため、翻訳されたコードを調べれば、どのシーケンスがx86-64命令にマッピングされるかを容易に確認できます。Prismは、計算能力の過剰な消費を避けるためにこのようにしていると推測されます。なぜなら、それは各ブロックを最小限のメモリ内状態を保持して線形パスで実行することで達成可能に見えるからです。しかし、このように直接的な方法で翻訳を行うと、人間が行うことができる最適化を見逃してしまいます。aarch64でx86-64のアドレッシングを再現するには、しばしば追加の命令が必要です。x86-64コンパイラは、ロードと数学演算を組み合わせた命令も利用します。これらはaarch64では2つの命令に分解する必要があり、ロードされた値を保持するための一時的なレジスタをさらに使用する必要があるかもしれません。

ハードウェア側では、高性能コアは未使用のコア幅でデッドコードを吸収できることがよくあります。これは、Prismがデッドコードを生成する場合に発生することがありますが、PrismはClaudeのCコンパイラのように過剰な量のデッドコードを生成するわけではありません。アドレス生成の再現や、Prismのレジスタマッピングを維持するためのレジスタ間での値のコピーによる命令オーバーヘッドは、クリティカルな依存関係チェーンを容易に長くすることができます。アウトオブオーダーエンジンには、それに対処する余地はあまりありません。

バイナリ翻訳の結果に対する究極の答えは、そもそも弱点の少ない高性能なアーキテクチャを持つことです。Qualcommは、最新のSnapdragon X2 Eliteでそれを目指しているようです。5 GHzで動作する9ワイドコアは、軽視できるものではありません。大きな命令キャッシュ(E-Coreで128 KB、P-Coreで192 KB)と十分なアウトオブオーダーリソースは、一般的にパフォーマンスに良い影響を与え、特にバイナリ翻訳からの命令オーバーヘッドを軽減するのに役立ちます。私はまだAsus Zenbook A16 UX3607OAラップトップでさまざまなユースケースを試していますが、ゲームのようないくつかのエッジケースシナリオを除けば、使用感は驚くほど普通です。アプリケーションがバイナリ翻訳を介して実行されなければならない場合でも、より高いパフォーマンスのコアはパフォーマンスをより許容可能にするため、Arm on Windowsの状況は、より多くのアプリケーションがaarch64ポートを取得するにつれて、時間とともに改善されると予想しています。