特集

Spectre V2対策による性能低下を緩和する「Retpoline」の効果を確認する

 Microsoftは今年3月1日にリリースした更新プログラムであるKB4482887で、"Retpoline"と呼ばれる性能低下緩和策を有効にしたことを発表した。

 記事でお伝えしたとおり、この更新プログラムを入れることで、むしろ性能が下がってしまうケースもいくつかある、という話なので、無条件に性能が改善されるわけではないが、そもそもRetpolineとは何かということからちょっと説明していきたいと思う。

 まずはSpectre V2について。Spectre(V1/V2)とMeltdownは、2018年1月に公開されたCPUの脆弱性である。概略はこちらに説明があるが、このSpectre V2(Branch Target Injection:CVE-2017-5715)についてもう少し細かく説明しよう。Spectre V2は間接分岐(Indirect Branch)を利用した攻撃である。

間接分岐と投機的実行

 そもそもCPUには、分岐命令(処理の流れを変える命令)として、「無条件分岐」、「条件分岐」、「間接分岐」、「リターン」の4種類がある。

 無条件分岐は名前のとおりダイレクトに分岐するもので、x86で言えば

jmp dest_address	// dest_addressにジャンプ

命令がこれに相当する。

 条件分岐は「ある条件が満たされたら分岐する」もので、

cmp ebx, 8		// EBXレジスタの値を8と比較
jns dest_address // EBXレジスタが8以上ならdest_adderssにジャンプ

といった仕組みだ。リターンは名前のとおり関数コールの最後で実行するret命令で、これが呼ばれるとスタックポインタから分岐先アドレスを取得し、そのアドレスに分岐する。では間接分岐は? というと、条件/無条件分岐によく似ているが、異なるのは「とび先アドレス」がメモリ/レジスタの値として与えられることで、

mov ebx, dest_address	// EBXレジスタにとび先アドレスの値を代入
jmp ebx // EBXレジスタの示すアドレスにジャンプ

というフォーマットを取る。これ、無条件分岐の“jmp dest_address”と何が違うか? というと、無条件分岐の方はとび先アドレスがプログラム中に記載されているから、CPUはこのjmp命令をデコードした段階でとび先アドレスがわかる。ところが間接分岐の場合、このjmp命令をデコードしても、その段階ではとび先アドレスがわからない。これが確定するのは直前のmov命令の処理が終わった段階である。この結果として、この間接分岐では、いわゆる静的な分岐予測が利用できなことになる。

 静的な分岐予測ができないと、当然性能の低下が著しい(分岐先アドレスが確定するまで、その先の命令のプリフェッチができないから、命令パイプラインが空走することになる)が、これを救うのがBTB(Branch Target Buffer:分岐先バッファ)だ。

 間接分岐が複数回行なわれる場合、1発目の間接分岐はどうしようもないが、そのさいに飛び元アドレス(上の例で言えばjmp命令のアドレス)と分岐先アドレス(上の例で言えばdest_address)をBTBに格納しておく。すると2度目に間接分岐がやってきたときは、mov命令を処理して飛び先アドレスが確定する前に、BTBから飛び先アドレスを取得して、その先の命令のプリフェッチが可能になる。これによって命令パイプラインの空走を抑えるという仕組みだ。つまりBTBを利用して、jmp命令の飛び先を確定する「前に」、投機的にdest_addressの先の命令を実行する(投機的実行)ことが可能になる。

 ちなみに、BTBと対になって利用されるものにBHB(Branch History Buffer)がある。これは過去の分岐予測の当たり外れを記録しておくもので、たとえば上の例であればebxにつねにdest_addressが入るので、過去のパターンを参照すれば「次もdest_addressにとぶ」と予測しやすくなるというわけだ。それとBTBだが、アドレスをフルに格納するとテーブルが大きくなりすぎるので、

・アドレス上位32bitは無視し、下位のうち31bitのみ保持
・下位31bitも長すぎるので、そのうち16bitは8bitずつペアにしてXORにして保持する(ので、異なるアドレスでも同じアドレスとして保持されることになる)という制約がある。

Spectre V2の仕組み

 さて、ここからがSpectre V2の話になる。CPUの内部でBTB/BHBは1つしかない。つまり複数のスレッドや複数の権限レベル(カーネルモードで動作しているか、ユーザーモードで動作しているか)を一切区別せず、一意に分岐先アドレスを格納している。そこで、あるスレッドをユーザーモードで動かし、変な分岐先アドレスをBTBに学習させる。このさいに、BHBに「この分岐は有効である」と履歴に残るように、同じ分岐先に何度も(論文によれば29回)繰り返すことが肝心である。こうなると、BTBとBHBは要するにゴミで埋まることになる。

 このゴミで埋まった状態でほかのスレッド、あるいはカーネルコードが動いた場合、当然BTBはミスになるので、時間を掛けて分岐の処理が行なわれることになるが、このさいに今度は当該分岐に関するBTBエントリは正しい値で埋まる。この状態で、もう一度変な分岐先アドレスにジャンプを掛けると、ゴミが残っているBTBエントリの飛び先にはすぐ移動できるが、内容が更新されたBTBエントリでは飛び先に移動するのに時間が掛かることになる。

 ここで先の話に戻ると、BTBの飛び先アドレス(の31bit分)は、16bitを8bitのペアにしてXORして保存している、という話を紹介した。なので、BTBエントリの内容から、ほかのスレッドなりカーネルなりが利用している飛び先アドレスが推察できる(下位15bitは同じで、上位16bit分はXORを掛けて同じ値になるペアのどれかとなる)仕組みだ。

 あくまでもが推測できるだけで、実際にそのアドレスの先の中身とかを見ることはできない(これが可能になったのがMeltdown)ということで脅威度はやや低めではあるが、それでも対策パッチはすぐに用意された。といってもこれが結構性能にインパクトのあるものであった。

 まず最初に登場したのがKB4056892であるが、これをAMDのプロセッサに適用するとPCがブートしなくなるという問題が発生、すぐにこれへの対策を行なったKB4073290がリリースされる。3月にはIntelのマイクロコードアップデートがKB4090007としてMicrosoftから配信された。

 4月にもマイクロコードアップデートが行なわれるとともに、AMDのプロセッサ向けのKB4103723のなかで、IBPB(Indirect Branch Prediction Barrier:これを有効にすると、プロセスなどの切り替え時にBTBをクリアする)を利用可能にする対策が提供された。じつはこの後も新たな脆弱性が続々発見され、それに向けて新しいパッチやらマイクロコードやらは引き続き投入されているのであるが、x86プロセッサ向けの対策としては、Microsoftからは2018年6月で一応対策を終わらせたかたちだ。

 問題は性能への影響で、Microsoftのブログによれば

・Skylake以降のプロセッサ+Windows 10であれば、性能低下は数%以内で、ユーザーはほとんど気づかない。
・Haswell/Broadwell以降のプロセッサ+Windows 10であれば、性能低下に気づくユーザーがいくらか出る
・Haswell/Broadwell以降のプロセッサ+Windows 7/8.1であれば、大多数のユーザーが性能低下に気づく

という曖昧な表現ながら、明確に性能低下が発生することを報告している。ちなみに従来の対策としては上で挙げたIBPBのほかに、

・IBRS(Indirect Branch Restricted Speculation):投機的実行の制限。新しくIBRSレジスタを用意、カーネルモードでは1、ユーザーモードでは0にしておく。間接分岐の投機的実行は、IBRSが1の時のみとする(つまりカーネルモードでしか間接分岐の投機的実行ができなくなる)。当然性能にインパクトがある
・STIBP(Single Thread Indirect Branch Prediction):シングルスレッド動作。要するにHyperThreadingを無効化する。これとIBRSを組み合わせれば、完璧にSpectre V2を防止できる。言うまでもなく大幅(30%~50%位?)の性能低下が見込まれる

などがあったが、性能へのインパクトが大きすぎるため、あまり実施されていない。

Retpolineの仕組み

 さて、ここまではじつは長い前置きで、ここからが本題。2018年1月にGoogle Project ZeroがSpectre/Meltdownの脆弱性を発表した翌日、そのGoogleはRetpolineと呼ばれるバイナリ置換テクニックでSpectre V2の脆弱性を緩和しつつ、かつ性能低下を抑える技法を開発したことをアナウンスしている

 バイナリ置換、というのは要するに命令をほかの物に置き換えてしまうという方策だ。基本的な理屈は、ROP(Return Oriented Programming)と呼ばれる仕組みである。先ほど分岐命令にret(Return)がある、という話をしたが、これを利用する。

 たとえば次のようなプログラムがあるとする。

push addr_1	// ①
call addr_2 // ②
addr_1: xxxxx... // ③
:
:
addr_2: yyyyy..... // ④
:
ret // ⑤

 この場合、処理の流れは①→②→④→⑤→③となる。これはご存知の方が多いと思う。ポイントは⑤のretで、これが実行されると直前にスタックに積まれたアドレス(この場合で言えば、addr_1)にジャンプする、という動作になる。これを逆手にとって

push addr_1	// ①
push addr_2 // ②
push addr_3 // ③
:
ret // ④
addr_1: xxxxx... // ⑤
:
ret // ⑥
addr_2: yyyyy..... // ⑦
:
ret // ⑧
addr_2: zzzzz..... // ⑨
:
ret // ⑩

というプログラムを書くと、処理は①~⑩まで順に行なわれる。つまり、もともとの間接分岐ではjmpに飛び先アドレスを入れて飛ばしていたのに対し、ROPではスタックに飛び先アドレスを入れて飛ばすことになる。

 これがなぜSpectreに利用できるか? というと、ret命令に関してはBTBは動かず、代わりにRSB(Return Stack Buffer)と呼ばれる別のバッファを利用するからだ。こちらも投機実行の対象になっており、過去32回分のリターンアドレス(つまりスタックに積んだ飛び先アドレス)を保持して、その範囲内で投機実行が行なわれる。利用するのがRSBだから、BTBを対象とした攻撃が行なわれても影響がないというわけだ。

 ただ、これは当然ながらプログラムの書き換え(コンパイラの生成するコードを変更する必要がある)が伴うので一朝一夕にはできない。やっと最近、これを利用できる準備が整い始めたという段階だ。

 もっとも、ではソフトウェアが対応すればこれがすぐ利用できるか? というと、もう少し準備が必要である。まず、ファームウェア更新が必要である。これは直接的にはSpectre v2には無関係である。2018年7月、カルフォルニア大の研究者によりSpectreRSB(RSBを利用したSpectre)が発表されたからだ。

 これに対応するために、RSB Stuffing(カーネルモードとユーザーモードの切り替えがあるたびに、RSBに遅延スロットへのアドレスが自動的に挿入されることで、時間計測を行なっても推測ができにくくなる仕組み)という技法が用意されており、これはSkylake以降のCPUでは標準で利用できる。ではそれ以前、Broadwellまでは? というと、ファームウェアの更新が提供されており、これを利用するとRSB Stuffingの対策とあわせてRetpolineが利用可能になる。

 ちなみにSkylake以降のCPUの場合、RSBが空になっている場合の振る舞いがBroadwellまでと異なっている。このため、Retpolineはそのままでは適用できず、enhanced IBRS(IBRSをハードウェア的に実装したもの)に頼るかたちになる。

 それともう1つはソフトウェア側の対処である。要するにOSもRetpolineに対応しなければ意味がない。こちらについては、MicrosoftがWindows 10 1809+KB4482887で対応しているので、あとはこれを利用するだけだ。

 余談になるが、Retpoline自体も副作用がないわけではない。Intelの説明によれば、Retpolineの手法はCET(Control-flow Enforcement Technology)と呼ばれる、Intelが将来のCPUで実装する脆弱性対処の仕組みと干渉する可能性がある、としている。このCETはenhanced IBRSと協調する仕組みになっているので、将来はRetpolineを利用しないことが望ましい、としている。

ベンチマーク環境

 上でもちょっとご紹介したが、従来のSpectre V2対策はそれなりに性能へのインパクトがある。昨年1月に僚誌AKIBA PC HotlineでKB4056892が性能に与える影響を確認しているが、CPUがCoffee Lake-SのCore i7-8700Kであっても、とくにストレージ性能が大きく落ちることが確認できている。これがRetpolineで多少なりともリカバーできるか? というのが今回のテストの目的である。

ケース1ケース2ケース3ケース4ケース5
CPUCore i7-5930K
メモリG.Skill F4-3200C14-8GTZR(DDR4-3200 CL14 8GB)×4
マザーボードASRock X99 Taichi
BIOSVersion 1.40Version 1.80
ビデオカードZOTAC GeForce GTX 770 GeForce Driver 419.35 WHQL
ストレージSamsung 970 Evo 512GB
RAMDisk電机本舗 RAMDA Ver 2 製品版(容量1,050MB)
OSWindows Pro 64bit 1809 Build 17763.379
Spectre V2対応無効化有効化無効化有効化
Retpoline対応無効化無効化

 ということでテストのご紹介を。まずテスト環境は表1のとおりとなっている。ここでケース1~5のシナリオであるが、

・ケース1:BIOS 1.4(enhanced IBRS未対応)、Spectre V2対応パッチ無効化
・ケース2:BIOS 1.4(enhanced IBRS未対応)、Spectre V2対応パッチ有効化
・ケース3:BIOS 1.8(enhanced IBRS対応)、Spectre V2対応パッチ無効化
・ケース4:BIOS 1.8(enhanced IBRS対応)、Spectre V2対応パッチ有効化
・ケース5:BIOS 1.4(enhanced IBRS未対応)、Spectre V2対応及びRetpolin有効化

という順になっている。

 まずBIOSについて。今回利用したASRockの「X99 Taichi」は、今となっては古い製品ながら、きちんとBIOSの更新が行なわれている。Version 1.40は2016年8月10日付のリリースで、まだSpectre/Meltdown騒動の前のものなので、enhanced IBRSには未対応である。これが2018年4月にリリースされたVersion 1.80で、Haswell-Eのマイクロコードを3Cに、Broadwell-Eのマイクロコードを0B00002AにUpdateする措置が行なわれている。こちらではenhanced IBRSが有効になったかたちだ。

X99 Taichi

 次いでOS側。今回利用したWindows 10 1809では標準でSpectre V2パッチとRetpolinパッチが含まれている。ただしインストールした段階では

・Spectre V2パッチ:有効化
・Retpolineパッチ:無効化

という状態になっている。この有効化/無効化はレジストリの操作で可能である。Spectre V2パッチについてはMicrosoftのページで説明があり
有効化の場合は

reg add "HKM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management" /v FeatureSettingsOverride /t REG_DWORD /d 0 /f
reg add "HKM\CurrentControlSet\Control\Session Manager\Memory Management" /v FeatureSettingsOverrideMask /t REG_DWORD /d 3 /f

を、無効化の場合は

reg add "HKM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management" /v FeatureSettingsOverride /t REG_DWORD /d 1 /f
reg add "HKM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management" /v FeatureSettingsOverrideMask /t REG_DWORD /d 3 /f

をそれぞれコマンドプロンプト(管理者で実行)から実行することで有効化/無効化が切り替えられる(操作後はリブートが必要)。

 同様にRetpolineは標準状態では無効だが、やはりコマンドプロンプト(管理者)から

reg add "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management" /v FeatureSettingsOverride /t REG_DWORD /d 0x400
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management" /v FeatureSettingsOverrideMask /t REG_DWORD /d 0x400

を行なって再起動すると有効になる。

 ちなみに今回は、2018年1月の記事に順じ、おもにストレージアクセスの性能を比較してみたいと思う。一応プロセッサ性能も比較対象としたが、ゲームに関しては大きな差はなさそうということで、今回割愛した(それもあってGPUにはGeForce GTX 770を突っ込んである)。ストレージについては、ブートドライブと兼用のSamsung 970 EVO 970(こちらで評価のとおり、現時点ではほぼフラグシップモデル並の性能だから不足はないだろう)のほか、RAMDISKも併用した。逆にHDDあるいはSATA SSDに関しては、先の記事にもあるようにあまり大きな性能差が見られなさそうなので、今回は割愛している。

NVMe SSD(Samsung 970 Evo)

 まずはこちらから。結果はPhoto01~05のようになっており、グラフ1にケース1の場合を100%とした場合の相対性能をまとめてみた。シーケンシャルに関して言えば環境が変わってもほとんど影響がないのだが、4KiBのQ8T8書き込み以降はものすごく影響が激しく出ているのがわかる。ポイントは

・BIOSを更新(ケース1とケース3、あるいはケース2とケース4)しただけで最大20%程度性能が低下する。これはenhanced IBRSの実装ペナルティと考えられる。まぁ20%程度で済むだけマシ、ということだろうか
・Spectre V2パッチを有効にすると、Q8T8とかQ1T1における性能がほぼ半減すると考えてよい。一番厳しいのがQ32T1で、ランダムアクセスでキューを深くするようなケースでは6割ほど性能が落ちている計算になる
・Retpolineを有効にする(ケース4とケース5の比較)と、確かに性能が改善する傾向にある。ただその効果が大きい(たとえば4KiB Q32T1 Read)場合と、効果が薄い(4KiB Q1T1 Read)場合があるのは、Retpolineを使ってもRSBを使い切ってしまうような事態が発生しているのかもしれない

【Photo01】ケース1
【Photo02】ケース2
【Photo03】ケース3
【Photo04】ケース4
【Photo05】ケース5

RAMDISK(RAMDA Ver2 容量1,050MB)

 続いて、よりシビアに性能差が出ると思われるRAMDISKの場合を。Photo06~10にそれぞれの結果を示す。グラフ2に、やはりケース1の場合を100%とした場合の相対性能を示している。こちらでは、より傾向が明確になった、として良いだろう。

 まずBIOSを更新すると20%ほどの性能低下が一律に発生する。そしてSpectre V2パッチを有効化すると、そこから30~40%ほどの性能低下がさらに発生する。これがRetpolineを有効化すると、2%~15%ほど性能を戻す、といった感じだ。

 たしかにRetpolineには効果はある。あるのだが、その効果は大きくないというか、多少マシになる、といった程度なのが改めて確認できたかたちだろうか。

【Photo05】ケース1
【Photo07】ケース2
【Photo08】ケース3
【Photo09】ケース4
【Photo10】ケース5

AIDA64 Extreme 5.99.4900

 何もCPU系のベンチマークを行なわないのも何なので、AIDA64の内蔵ベンチマークも実施してみた。テスト項目はMemory 4項目、CPU(ALU系)5項目、CPU(FPU系)6項目の合計15項目である。結果の単位がそもそも違う上に、結果の値のレンジも2桁の幅があるので、結果そのものは表2にまとめ、グラフ3にはケース1の場合を100%とした場合の相対性能を示している。

ケース1ケース2ケース3ケース4ケース5単位
Memory Read5437554228515765164751667MB/s
Memory Write4913349073448734485544848MB/s
Memory Copy5862058525563115639856341MB/s
Memory Latency56.957.159.459.359.3ns
CPU Queen7668976716632236315963190
CPU PhotoWorxx3019030126291802915229138MPixel/s
CPU Zlib557.9555.5458.4458.5459.7MB/s
CPU AES2974929722244712447024468MB/s
CPU Hash67066700551655175516MB/s
FPU VP881938074662269907103
FPU Julia5182751662414774147741483
FPU Mandel2772327518222442223622241
FPU SinJulia83508341686868686869
FP32 Ray-Trace93959385756375627565KRay/s
FP64 Ray-Trace51945188414841464148KRay/s

 ちなみにMemoryの項目でLatencyのみ逆(値が少ないほど相対性能が高い)にしてある。こちらでは見ておわかかりのとおり、最大の差はBIOSの違いであって、要するにenhanced IBRSが大きく性能に影響する(大体20%ほど)が、逆にSpectre v2パッチの有無とかRetpolineの有効化/無効化はほとんど性能に影響しない(唯一FPUを使ったVP8のみ多少差がある程度)ことが見て取れる。このあたりはAKIBA PC Hotlineの記事とまったく同傾向として良いかと思う。

考察

 ということで簡単ではあるが、Retpolineの効果を確認してみた。その効果は? というと「なくはないが、過大な期待は禁物」というあたりだろうか。まぁ多少性能を戻す程度で、Spectre/Meltdown騒ぎの前まで性能が戻るわけではない。これはまぁ如何ともしがたいことであろう。

 ついでに言えば、このRetpolineが利用できる層はかなり限られる。まず大前提としてSkylake以前(Broadwell世代まで)のCPUを使っており、しかもOSがWindows 10のVersion 1809でないと利用できない(これ以前のOSにはRetpolineへの対応が予定されていない)。

 これに加えて、マザーボードのBIOS更新によってマイクロコード更新が行なわれないと、Retpolineは利用できない。このあたりはマザーボードメーカーの頑張り次第ではあるのだが、たとえばHaswell世代のZ87チップセットを搭載したマザーボードの場合、最新BIOSの日付が2014年だったりすることも珍しくない(Z97だと、enhanced IBRSの対応のために2018年に急遽BIOS Updateが実施されているケースが結構あるのだが)。

 ただ、運よく自分のマザーボードにBIOS更新が用意されているのであれば、Retpolineを有効化するのは悪いことではないと思う。少なくとも使った限りにおいては、とくに副作用的なものはみられなかった。