- Box64のRV64 DynaRecは、1年前に簡単なネイティブLinuxゲームを動かしていた段階から、RISC-V PCでThe Witcher 3を実行できる段階まで進化した
- 進展のきっかけはAMDグラフィックカードの利用が可能になったことで、OpenGLの制約が減り、より多くのx86プログラムをテストしてバグを修正できるようになった点にある
- RV64バックエンドはARM64バックエンドより実装済みのx86命令数が少なく、AVX命令もまだRISC-V側に残された大きな課題となっている
- RISC-Vはビット範囲の抽出・挿入や16バイト原子的命令が不足しており、x86エミュレーションではAArch64やLoongArch64より変換コストが大きい
- The Witcher 3はbox64で実際に動作し、ゲーム内では最大15fps、メインメニューではフルスピードに達する
RISC-VでThe Witcher 3が動くまで
- 1年前のRV64 DynaRecは、Stardew ValleyやWorld of Gooのような比較的動かしやすいネイティブLinuxゲームしか実行できなかった
- 当時のボトルネックは大きく2つあった
- RISC-Vバックエンドにx86_64命令を素早く追加する過程で、DynaRecのバグが多く残っていた
- VisionFive 2とLicheePi 4AのIMG内蔵GPUはOpenGL ESしか対応せず、OpenGLをサポートしていなかった
- gl4esで一部のOpenGL対応を得たことでStardew Valleyのようなゲームは動かせたが、より重いLinuxゲームや一般的なWindowsゲームには十分ではなかった
- SophgoのMilk-V Pioneerは64コアのRISC-V PCで、グラフィックカードを挿せるPCIeスロットを備えている
- もう1人の貢献者xctanは、VisionFive 2にM.2インターフェース経由でAMDグラフィックカードを「接続」する方法を見つけた
- AMDグラフィックカードが使えるようになったことで、テスト可能なx86プログラムの範囲が広がり、RV64 DynaRecのバグ修正とx86命令追加が大量に進んだ
- その結果、The Witcher 3を最初に起動した時点でそのまま動作した
RV64 DynaRecの現在の状態
- x86命令セットは非常に大きく、バックエンドごとに実装規模にも差がある
- ARM64バックエンドは全体で1,600個以上のx86命令を実装している
- RV64バックエンドは約1,000個のx86命令を実装している
- このうち300個以上は新たに対応したAVX命令で、RISC-Vにはまだまったく実装されていない状態だ
- SSE命令の実装でもRISC-Vは性能面で不利だ
- RV64バックエンドはSSE命令をスカラー命令として実装している
- AArch64はNeon拡張、LoongArch64はLSX拡張を使う
- この差のため、ほかの2つのバックエンドより性能がかなり低い
- RISC-Vにはベクトル拡張のRVVがある
- Milk-V PioneerはRVV 0.7.1派生のxtheadvector拡張をサポートする
- SpacemiT K1/M1 SoCは標準化済みのRVV 1.0をサポートする
- このSoCを搭載したBanana Pi F3とMilk-V Jupiterは購入可能だ
- box64には最近、基本的なRVVサポートといくつかの一般的なSSE命令実装が追加された
- RVV対応はまだごく初期段階で、現時点では性能改善には役立っていない
x86エミュレーションで特に不足するRISC-V命令
- x86エミュレーションの観点では、RISC-Vは対応する3つのアーキテクチャの中で表現力が最も低い部類に入る
- AArch64やLoongArch64と比べて便利な命令が足りず、同じ動作をエミュレートするのにより多くの命令が必要になる
- 特に重要な機能が2つ欠けている
- 1つのレジスタから特定のビット範囲を取り出して別のレジスタへ持っていく機能
- 1つのレジスタの一部ビットを別のレジスタの特定範囲へ挿入する機能
- LoongArch64とAArch64にはこれに対応する命令がある
- LoongArch64は
BSTRPICK.DとBSTRINS.Dを使う
- ARM64は
UBFXとBFIオペコードを使う
- RISC-Vには公式拡張にもベンダー拡張にも、これに対応する命令が存在しない
ADD AH, BLの例が示す変換コスト
- x86 ISAは変更されないビットを保持する傾向があり、部分レジスタ操作が重要になる
ADD AH, BLの場合、box64は次の処理を行う必要がある
RBXの最下位バイトを抽出する
RAXの下位から2番目のバイトに加算する
- 結果を
RAXの下位から2番目のバイトへ再挿入する
RAXの残りのバイトはそのまま保持する
- LoongArch64では
BSTRPICK.D、ADD、BSTRINS.Dを使って簡単かつ直感的に実装できる
- RISC-Vでは同じ動作にシフト、マスク、
AND、ORなどを組み合わせる必要があり、10命令を要する
- これは孤立した例ではなく、x86には似た形の命令が多いため、RISC-V実装はより煩雑になる
16バイト原子的命令の制約
- x86にはロックフリー原子演算のためのLOCKプレフィックス命令がある
- box64は主にLR/SCシーケンスでこれをエミュレートする
- LR/SCはLoad-Reserved / Store-Conditionallyの略だ
- たとえば
LOCK ADD [RAX], RCXはLR.D、ADD、SC.D、条件分岐という形で生成される
RAXアドレスがアラインされていない場合はさらに複雑になるが、一般にはこの方式でうまく動作する
- 問題は
LOCK CMPXCHG16Bだ
- この命令は
RDX:RAXをメモリ上の16バイトと比較する
- 条件に応じて
RCX:RBXをそのメモリアドレスへ交換する
- AArch64とLoongArch64には実装に使える16バイト原子的命令がいくつかある
- RISC-Vにはこれに対応する命令がないため、ほかのアーキテクチャほど完全には実装できない
- Unityゲームを含め、多くのプログラムが
LOCK CMPXCHG16Bを使っている
実際の動作結果
- 制約は残っているものの、The Witcher 3はbox64によってRISC-V上で動作する
- 性能はゲーム内で最大15fpsに達する
- メインメニューではフルスピードで動作する
- AAAゲーム実行を目的に設計されていないマシンで得られた結果としては、悪くない水準だ
1件のコメント
Hacker Newsのコメント
チップ方面の仕事をしていない立場として気になるのだが、RISC-V向けソフトウェアを作るとき、ソフトウェアエンジニアは何を変える必要があるのだろうか。
実行ファイルのサイズが大きくなるのでキャッシュ局所性を積極的に最適化すべきなのか、ゲームやWebサーバーのようにCISC/RISCのどちらかにより向いているソフトウェアの種類があるのかも気になる。
ソフトウェアのアプローチとして本質的に変えるべきことは多くなく、x86-64と比べた最大の違いはレジスタが32本あるため、スタックに退避される前に中間値をより多く保持できる点だが、ARMも32本なので似ている。通常は、細かな最適化をしない限り大きく気にすることはない。
さらに細かく言うと、ベクタ拡張(V/RVV)が基本のrv64gc ISAに含まれていないため、ターゲットによってはSIMD最適化の恩恵を受けられない場合があり、popcountや先行/後続ゼロ数のカウントも基本のrv64gcにはなく、Zbbが必要になる。また、
a ? b : cのような分岐なし選択も、基本のrv64gcでは4〜5命令、Zicondで3命令が必要だが、x86-64とaarch64では1命令で可能だ。RISC-Vプロファイルは前の2つの問題をある程度解決する。例えばAndroidは、RVV、Zbb、Zicondなどを要求するrva23を必須としている。しかしLinuxディストリビューションがrva20/rv64gcをターゲットにすると、動的ディスパッチを入れていない事前コンパイル済みコードでは、そうした拡張を実質的に長く使えなくなる。x86-64にも似た問題はあるが、ARMは拡張がはるかに少ないため影響は小さく、SVEが最大の例外だが、まだ広くサポートされているわけではない。
最大の違いは弱いメモリモデルだが、これはARMのような大半の非x86アーキテクチャにもある特性で、そもそもコードが強いメモリモデルに依存すべきではない。
実行コード密度は、歴史的な理由でx86がそれほど良くないため、実行ファイルサイズは思ったほど増えない。圧縮命令拡張のあるRISC-VやThumb拡張のある32ビットARMはかなり高密度だ。
重要なのはCISC対RISCではなく、ベクタ命令と暗号化拡張の有無と品質だ。動画のエンコード/デコードは高い性能のためにベクタ命令へ大きく依存しており、フルディスク暗号化やハッシュ処理はAES、SHA256のような特定アルゴリズムを高速化する専用命令の助けを受けられる。
核心は、ARMの特許から自由になり、学んだ教訓を踏まえて新たな出発をすることに近い。
有名なロシア人がElbrus 8SでAtomic Heartを動かしていたのを思い出す。
Elbrusにはネイティブトランスレータがあり、私の知る限りかなり出来がいい。Atomic Heartは15〜25fps程度で、ある程度プレイ可能だった。
記事は「基本」の説明が少し不足している。Wineのポートのようなものを使って実行したのかと思ったが、実際にはRISC-Vチップ上にx86_64 ISAを何らかの形で実装しているように見える。
その部分がどうなっているのか、もっと説明できる人がいるといいのだが。
エミュレータではあるが、libc、libm、SDL、OpenGLのような一部の「システム」ライブラリはネイティブ版を使うため、大半のアプリケーションと統合して使いやすく、場合によっては性能も驚くほど高くなり得る。Wineもネイティブにコンパイルして実行できる。
驚くべき結果だ。膨大な作業量であり、いくつかの場合にはRISC-Vの限界に達しているようにも見える。
ビットの収集/散布命令は拡張に入れるべきだと思う。
x86 エミュレーションの文脈でサポートしている3つのアーキテクチャのうち、RISC-V が最も表現力に乏しいという点が興味深い。
コンピュータ科学史の授業では、RISC は縮小命令セットコンピュータだと習ったが、最近の RISC-V プロファイル提案や記事を見ると、「機能同等性のために命令があと数個必要だ」という類の話が多い。RISC-V が多くの人にとって他のプラットフォームの便利な代替だというのは理解できるが、これが RISC の夢が死んだという意味なのかも気になる。
RISC-V 仕様を読んだ記憶では、よくある命令シーケンスはフロントエンドで融合できるので、「コンボ」命令を追加しないことにかなり厳格だった。
x86/ARM と比べた RISC-V の不足は、RISC 原理主義のせいというより、仕様が非常に基本的な組み込みチップから始まり、時間とともにアプリケーション CPU 向けの拡張を追加してきたことに由来するように思う。基本の RV32I には整数乗算すらない。残念ながら、ビット操作と SIMD/ベクトル拡張の議論を終わらせるのに時間がかかりすぎ、その結果として今話題になっている機能の空白が生まれた。
ただし、その分だけ高性能のために便利な命令の一部は除外される。
単純なパイプラインには、高性能設計を行うチームのエンジニアリングリソースをあまり消費せず、最適化により多くの時間を使えるようにする利点もある。
RISC は概して 単純化の哲学だが、その度合いはさまざまだ。MIPS は RISC-V と同じくらい単純化されているが、ARM と POWER はより折衷的で、高性能領域で x86 と張り合うのに大きな問題はなさそうだ。
プロセッサ市場には、アプリケーション実行以外にも組み込み、アクセラレータなどさまざまなニッチがある。アプリケーションコアという特定のニッチでは RISC-V にはやや悲観的だが、より広く見れば潜在力は大きく、いくつかの商業的ニッチを支配する可能性もあり、教育・研究ツールとしては優れている。
古典的な RISC の特徴は、データ操作命令の大半がレジスタ上でのみ動作し、メモリ命令はおおむねレジスタへの load/store だけで、そのため多くのレジスタが必要になる点だ。引数渡しのためにスタックを直接操作しなければならないのでスタックも自分で作り、CALL/JSR 命令なしに、命令ポインタレジスタへ直接 load/store する基本命令で実装する。命令エンコーディングは予測可能で、すべての命令サイズが同じ。複数の RISC アーキテクチャには、常に 0 を読み出し、書き込めないレジスタもあり、値を 0 に設定するときに使われた。
この方式はうまくいったが、その後 アウトオブオーダ実行と SIMD によって重要性が下がった。生の命令ストリームは望む結果へ至る経路の宣言に近く、CPU が実際にそのまま実行するという意味ではない。その裏には投機実行、分岐予測、レジスタリネーミングがある。SIMD は幅広いレジスタ空間と、その中のすべての値に作用する命令に近い。結局、アウトオブオーダ実行と SIMD が主導権を握った。
理論上は、元の ソースコードを RISC 向けにコンパイルすればまったく別のバイナリになり、その特定の命令は不要かもしれない。
現実的には、誰かがこれらのゲームを実際に RISC-V 向けにコンパイルすることはなさそうだ。
スクリーンショットでは RAM が 31GB と表示されているが、言及されている開発ボードの最大仕様より明らかに大きい。ここでは別のものを使っているのだろうか?
今なら、RVA22 と RVV 1.0 を実装したより高速なコアを複数備える最近の選択肢のいずれかを使うほうがよい。
これは 86Box なのか? Amstrad PC1512 を買った頃を思い出すのが楽しかった。
500MB のハードカード2枚と 128KB のメモリ拡張を追加して 640KB にしたら、ずっと面白くなった。最初は 360KB フロッピー2台だけで、数年後に 32MB のハードカードを追加した。Borland TurboPascal と Zortech C もあった。楽しい時代だった。
それでも Amstrad PC1512 を使っていた頃は覚えている。
いつか、大きな RISC-V CPU をいくつかと、小さな RISC-V CPU の束で実装した「GPU」を一緒に載せるシステムが出てくるのか気になる。
適切なベクトル機能を付けた形になるのだろうが、ついでの質問として、packed SIMD の代わりに 古典的なベクトル方式が GPU で有用になり得るのかも気になる。
技術的に印象的な Witcher 3 の成果としては Switch 移植版もあり、本当によく動いていた。
最適化でどれだけ多くのことができるか、そして PC で 悪い最適化のせいでどれだけ多くのリソースが無駄になっているかを示している。
同条件の比較ではなく、画面に表示される範囲が大きく違うのに、PC の最適化が悪いと断定するのは難しい。
こうした ISA レベルのフィードバックが RVI 側の人たちに届くとよいと思う
昨日確認してみたところ [1]、記事の例はすでに RISC-V 命令 4 個で実現できる。ただし思いつくのが少し難しい
# a0 = rax, a1 = rbxslli t0, a1, 64-8rori a0, a0, 16add a0, a0, t0rori a0, a0, 64-16[1] https://www.reddit.com/r/RISCV/comments/1f1mnxf/box64_and_ri...
実際、ビットフィールド抽出が欠けているのはあまりにも明白なミスで、RISC-V ISA がどれほど馬鹿げているかを示す、私のお気に入りの例だ。二つ目は、まともなアドレッシングモードがない点だ
より優れた RISC-V 設計の一部は、実際にこのためのカスタム命令を実装している。たとえば Hazard3 の BEXTM がある: https://github.com/Wren6991/Hazard3/blob/stable/doc/hazard3....