x86アプリをARM上で動かすとき、命令の互換性だけでなく「メモリがどう見えるか」まで合わせないといけない。FEX-Emuは、その中でも見落とされがちな x86-TSO の扱いが、実は性能を大きく左右していると説明している。この記事は、単なるエミュレータの実装話ではない。ARM側の世代差や拡張機能の違いが、同じx86アプリでも体感を変えてしまう現実を、かなり具体的に示している。
FEX-Emuは、Linux上でx86/x86-64アプリを動かすユーザーモードエミュレータだ。その開発チームが今回取り上げたのは、x86エミュレーション全体にのしかかる「x86 Total Store Ordering memory model(x86-TSO)」の問題だった。メモリモデルとは、読み書きの順序や見え方をどう保証するかというルールのことだが、x86はかなり厳格で、書き込みが他のCPUにもすぐ見える前提をプログラマが置きやすい。一方でARMは弱い整合性を前提にしていて、ハードウェアがより効率よく動ける代わりに、順序の保証は薄い。
FEXは最初、ARMv8.0-a環境ではx86のloadをARMのload-acquire、storeをstore-releaseに置き換えていた。これでx86にかなり近い動作は作れるが、実際には必要以上に厳しく、しかもacquire/releaseを大量に使うのはARMの設計思想に合っていない。記事では、ベンチマークを見るとこの古い方法がかなり重いことが示されている。LoadとStoreの基準性能に対し、acquire-loadは5つのCPUテストのうち3つで大きく落ち、AmpereOneではrelease-storeが特に低い。M1でもAcquire/LRCPC loadがベースラインよりかなり低かったという。
そこから先の希望として挙げられるのが、ARMv8.3以降で必須になった LRCPC-load だ。これはRCpcという、x86エミュレーションに向いた新しいメモリモデルに対応するもので、ベンチマークではほぼ通常のloadと同じ性能が出ている。FEXはこの拡張を検出するとacquire-loadをやめ、LRCPC-loadへ切り替える。Apple Siliconについては少し特殊で、CPU機能の切り替えで通常のload/store自体がx86-TSOに寄るよう設計されているため、x86エミュレータ側は普通のload/storeを使えばよい。Asahi Linuxからこの機能を検出すると、FEXもそれを有効にするという。
記事はさらに、ARMの拡張が FEAT_LRCPC、FEAT_LRCPC2、FEAT_LRCPC3 と段階的に増えてきたことも触れている。基本のGPR向けTSO load、即値オフセット付き、vectorやstackベースのTSO load/storeと、穴を埋めるように拡張されてきたが、それでも完全ではない。だからFEXは、最終的にはハードウェア側にTSOモード切替があるのが理想だと見ている。
この記事で面白いのは、性能の話なのに「新しい命令が速いか遅いか」だけで終わっていないところだと思う。むしろ本質は、ARMが長年積み上げてきた効率化の仕組みが、x86を再現するときには障害になりうる、という点にある。ARMは弱い整合性で高速化し、必要な場面だけ明示的な順序制御を足す設計だ。一方のx86は、最初から“プログラマが素朴に考える順序”にかなり寄せている。だからエミュレータは、単に命令を翻訳するだけでは足りず、CPUの世界観ごと借り直す必要がある。
ここで重要なのは、FEXが「かなり厳しく合わせる」方向を選んでいることだ。理屈の上では、x86アプリが本当に必要とする保証だけを細かく満たす最適化もできそうだが、現実はそう単純ではない。実際の記事でも、ARMv8.0-a時代は中間解がなく、やむを得ず過剰に厳しい方法を使っていたと書かれている。これはエミュレータの宿命に近い。安全側に倒すほど互換性は上がるが、速度が落ちる。速さを優先すると、今度はバグが出る。
私は、この種の問題は「ARMが遅い」「x86が古い」で片付けられないと思う。むしろ、どちらも自分の得意分野を最大化した結果、相手を再現するときに歪みが出る。x86のアプリが大量のload/storeを吐くほど、その歪みは顕在化する。だからこそ、FEXがLRCPCやApple SiliconのTSOモードを強く評価しているのは自然だ。エミュレーションでは賢い中間層より、ハードウェアが素直に従ってくれる方が効く。
ベンチマークの話も印象的だ。この記事の数字は、個別のCPUの優劣を語るというより、「どの命令パスを標準として持っているか」で結果が大きく変わることを見せている。通常のload/storeを基準にしたとき、acquire-loadやrelease-storeは一部のCPUで明らかに足を引っ張った。AmpereOneの例はとくに分かりやすい。この記事は、こうした命令を毎回使うのは本来の用途ではなく、x86エミュレーションではそれを“常用”してしまうのがまずいのだと示している。
ここでのポイントは、性能差が「数%の誤差」に収まっていないことだと思う。x86をARM上で動かす場合、エミュレーションのオーバーヘッドはもともとある。その上でメモリモデルの翻訳まで重いと、アプリによっては実用性がかなり削られる。特にゲームやJIT系のように、細かいメモリアクセスが山ほど発生する場面では、こうした差が積み上がる。単発の命令が少し遅いだけではなく、毎秒何百万回も呼ばれる経路が遅いのが問題なのだ。
一方で、Apple Siliconの話は少し違う意味で興味深い。Appleはx86-TSOをハードウェア側で受け止める方針を取ったため、FEXから見れば理想に近い土台になる。もちろん、記事が言うように thread-wide のTSO切り替えには別のコストがあるはずだし、すべての場面で無条件に得というわけではない。それでも、x86エミュレーションという目的に限れば、余計な変換を減らせる価値は大きい。私はここに、エミュレーションの“正しさ”と“速さ”を両立させるには、ソフトだけでなくハードの思想も揃っていないと厳しい、という現実が出ていると感じた。
記事の後半では、さらに厄介な話として非整列アクセスや split-lock に触れている。x86アプリは、メモリアラインメントをあまり気にしないことがある。キャッシュラインをまたぐアクセスや、整列していないatomic操作まで普通にやってくる。ARM側はこうした前提ではないので、エミュレータはここでも変換に苦しむ。しかも split-lock は、Linuxカーネルが検出してゲームを遅くするほど問題視される。つまりこれは、変な一部のアプリの癖ではなく、実際に現場で踏まれる地雷だ。
私は、この部分がこの記事の中でかなり重要だと思う。メモリモデルの話だけでも十分難しいのに、x86はさらに「整列していなくても動く」「境界をまたいでもそれなりに扱える」といった、暗黙の許容範囲を広く持っている。エミュレータはそれを真似しないといけない。しかも、真似しようとすると今度はARMの素直な実装とぶつかる。要するに、x86エミュレーションは“命令を訳す仕事”ではなく、“CPUの甘さまで再現する仕事”に近い。
ここから見えてくるのは、エミュレーションの価値基準が少し変わってきていることだと思う。昔は「動くかどうか」が中心だったが、今は「どれだけネイティブに近い速度で動くか」が問われる。だからメモリの扱いが少しでも重いと、互換性以前に選ばれなくなる。FEXがハードウェアの新機能を細かく追いかけているのは、その競争に勝つためだ。逆に言えば、x86アプリをARMで快適に動かす時代は、こうした地味なメモリの話を避けては通れない。
最後に感じるのは、FEXがかなり一貫して「ハードウェアがx86に寄ってくれるなら、その方がいい」と考えていることだ。これは単なる保守的な態度ではない。load-acquireやstore-releaseを大量に並べるやり方は、理論上は正しくても、実際のCPUにとっては想定外の使い方になりやすい。そこへLRCPCやTSOモードのような機能が入ると、エミュレータはずっと素直に書けるし、性能も取りやすい。
私はこの流れを、ARMがx86に“負けた”と見るべきではないと思う。むしろ、ARM側が高効率な設計を保ったまま、互換性が必要な場面だけ別の顔を持つようになってきた、と見る方が近い。Apple Siliconの設計はその象徴だし、ARMv8.3以降の拡張も同じ方向を向いている。x86エミュレーションは、その進化の恩恵を直接受ける領域だ。FEXの記事は、そこをかなり率直に認めつつ、まだ埋まっていないギャップもはっきり書いている。エミュレータ開発の現場では、こうした「理想に近づいたが、まだ完全ではない」という状態こそが一番リアルなのだと思う。