借り初めのひみつきち

仮ブログです。

今月の myos

このシリーズ、ほぼ月1になっていたのでタイトルを変更します。

SIMD と Lazy FPU save / restore

CPU の高速化はいくつかの歴史を経て、近年の CPU は「ひとつの命令で多数のデータを処理する」命令をサポートしています。 これを一般に SIMD (Single Instruction, Multiple Data) と言います。 画像処理などは大量のデータに対して同じ手順を繰り返す操作が多いため SIMD 命令が効果的です。

初期の x86 プロセッサには SIMD 命令がありませんでしたが、現代の x86 プロセッサには SSE という拡張機能SIMD に対応しています。 SSE は専用のレジスタを使うので、コンテキストスイッチの際に正しく退避処理を行わないと他のスレッドとレジスタの内容が混ざってしまいます。 SSE は長い x86 の歴史の中では比較的後期に新設された機能であり、通常の処理において大抵の場合 SSE は必須ではありません。

このような経緯のため SSE など x86SIMD 機能はデフォルトで無効になっていて、 OS 側で機能を有効にしないとアプリ側から自由に使えないようになっています。*1

また、大量のデータを扱う性質上、 SIMDレジスタは汎用レジスタに比べて容量も多く退避のコストが上昇します。 基本的な処理ではほとんど使わない上に汎用レジスタより容量の多い SSE レジスタコンテキストスイッチのたびに退避処理を行うと性能低下の恐れがあります。

そこで昔の人は考えました。

コンテキストスイッチのたびに一旦 SIMD ユニットを無効化し、実際に SIMD 命令を実行しようとしたら例外でトラップし、例外が発生するまでレジスタの退避復元処理を遅延させることで SIMD 命令を使わないスレッドにおけるコンテキストスイッチのオーバーヘッドを減らそう。 いわゆる Lazy FPU context switch や Lazy FPU save/restore などと呼ばれる高速化技法です。

しかし、時が経つとこの高速化技法はあまり役に立たなくなってきました。

  • SSE が普及し、昔は汎用命令で処理していた何気ない処理(2つのまとまった 64bit 値をコピーする場合など)にも SSE を使うコンパイラが出てきた。
  • マルチコアが普及してコンテキストスイッチの際に同じ CPU に復元されるとは限らなくなり、レジスタの退避・復元を遅延する効果が薄れた。
  • Lazy FPU save/restore と投機実行を併用したセキュリティ攻撃が発見された。
  • SIMD レジスタの退避・復元処理の速度ペナルティは Intel も認識しており、 AVX 以降では負荷を軽減する新命令が追加された。

現代では Lazy FPU save/restore は使わない方が良いとされています。

ここまでが SIMD を取り巻く歴史となります。

myos では SSE レジスタの退避処理による割り込みやコンテキストスイッチの速度低下への懸念から、当初カーネルのコア部分では SSE を非使用とし、 UI やアプリで必要になったら順次解放していくという方向で開発を進めていました。 しかし、現在のビルド環境ではカーネルのコア部分と UI 部分が同じバイナリに含まれているために ABI を切り替えることが難しく、完全に禁止するか、完全に解放するかという2択の状況が続いていました。

いずれは解放する予定だったし Lazy FPU save/restore のような仕組みを新しく考えるのも面倒になってきたので、試しに全てのコンテキストスイッチで SSE レジスタの退避と復帰処理を実装してみました。

方法は簡単で、元々コンテキストを保存する領域は SSE のレジスタを保存できるように準備していたので、コンテキストスイッチレジスタを切り替える部分に FXSAVEFXRSTOR を追加します。また、新しいスレッドを作成する部分で FPU や SSE の初期値を設定するコードも追加しました。

    fninit
    pxor xmm0, xmm0
    pxor xmm1, xmm1
    pxor xmm2, xmm2
    pxor xmm3, xmm3
    pxor xmm4, xmm4
    pxor xmm5, xmm5
    pxor xmm6, xmm6
    pxor xmm7, xmm7
    pxor xmm8, xmm8
    pxor xmm9, xmm9
    pxor xmm10, xmm10
    pxor xmm11, xmm11
    pxor xmm12, xmm12
    pxor xmm13, xmm13
    pxor xmm14, xmm14
    pxor xmm15, xmm15

そして画像処理で SSE の最適化を有効になるように調整すると、バイナリサイズは肥大化してしまうものの、実機では速度向上が見られました。 懸念していたほどの速度低下も起きず、 Lazy FPU save/restore のような複雑な仕組みも不要になってシンプルになり、画像処理も速くなったのでこれでいくことに決まりました。

これによって機種によってはゲームベンチのFPSが300を超えるようになりました。速いっていいですね。

Universal Serial Bus

myos は一言で言うと moe を Rust で再実装した OS です。 開発言語が違うので単純な比較はできませんが、主要な機能の再実装は完了していると思います。 最後に残った機能は USB でした。

というわけで実装しました。

f:id:neriring16:20210913160425p:plain

USB のクラスドライバは USB バスと通信して結果を変換して他のところに転送するだけのようなものが多く、単純にそのまま実装すると通信用のスレッド数がものすごく増えることになります。 同じコントローラーに対する USB コマンドは同時実行できないので排他処理が必要であること、ドライバの実行時間の大半は待ち時間であることなどを考慮すると、各ドライバがそれぞれスレッドを利用するのはあまり好ましくないので、それぞれのドライバは Rust の async 機能で並列化しています。

また、それぞれのクラスドライバは moe の時代から進化しています。

HID は moe の時代にはキーボードとマウスの最低限の機能だけをサポートした boot プロトコルしか対応していませんでした。 myos では boot プロトコルを廃止してレポートプロトコルの方を使うようになったので boot プロトコルを提供していないトラックパッドや一体型のキーボードなどもだいたい使えるようになりました。 本来の HID 規格はキーボードとマウスだけでなく人間と機器の間でやりとりをするあらゆるデバイスを取り込もうとした超巨大な規格で膨大な種類の Usage が定義されていて全て対応しようとするととても大変ですが、現在の HID ドライバは若干手抜きをしていてキーボードとマウスの入力以外の機能は考慮していません。

USB 2.0 ハブは moe の時代から対応していて起動時に刺さっているとうまくデバイスを認識できない不具合がありましたが修正されました。 USB 3.0 ハブは未対応だったので対応中ですがまだうまく動いていません。

XInput は moe の時代にはとりあえず値が読み出せるだけで使い道がありませんでした。 myos ではゲーム API からアクセスできるようになったので市販の XInput ゲームパッドを接続してゲームを操作することができます。 DirectInput 形式への対応は未定です。

Curiosity

以上により、現在のバージョンをもって moe から myos への移行は完全に完了したと思っています。

いつまでも名前が5月だと締まらないので、 これからは新バージョンのコードネーム「Curiosity」として次のマイルストーンに向けて開発していこうと思います。

*1:ただし、 UEFI から起動した場合は x64 UEFI の用件に SSE が含まれているので有効の状態で起動すると思います