Qualcommの次世代Snapdragonで、x86アプリが実行可能なWindows 10をサポートするという。当然ながらx86命令が、ARM上でエミュレーションされて動作することになる。気になる性能は、意外と速いという。その速さの秘密を推測してみた。
すでに「昨年(2016年)の話」となってしまったが、Qualcommの新プロセッサ(もちろんのことARMベース)でデスクトップ版のWindows10が実行できるようになるという発表があった(Qualcommのニュースリリース「Qualcomm Collaborates with Microsoft to Support Windows 10 Computing Devices on Next Generation Qualcomm Snapdragon Processors」)。
これは、Microsoft主導による脱PC路線の一環なのだと思う。ARMベースのスマートフォンで、PC版と「同じ」Windows10が走ることに関しては、以前からいろいろと取り沙汰されており、市場的なインパクトやら何やらについてはいろいろと書かれているので、ここでは取り上げない。
その発表の中で技術的ではあるけれども、多くの人が興味を持っていると思われるのが、x86ベースのWin32のコードをARM上のWindowsで実行できるという点である。そのデモのレポートをいくつか読むと「ARMのネイティブのアプリとほとんど遜色ない体感速度であった」と感想を述べている人が多い。
x86をソフトウェアでエミュレーションできる環境は多い。それらの多くは実機速度の10分の1、下手をすると100分の1くらいのものである。それに対して、実機とあまり変わらない体感速度、というのはどうしてなのか。筆者は、Microsoftが内部でどんな手法を採用しているのか、全く知らないのだが、かなり推測が付くつもりでいる。今回はそんな話を書かせていただく。
重要なことは、「ARM上にARMネイティブのWindows 10が載って動いている」という点である。基本的にはx86版と互換性のあるAPI、ライブラリ群がARM上でそろっている、ということだ。
ここでアプリ内部の動作を考えてみよう。ARMコードであれx86コードであれ、UI(ユーザーインタフェース)を構築するにせよ、入出力をするにせよ、アプリ本体のコードは、APIなどに各種パラメーターを与えて呼び出すという監督的な仕事がメインになる。
実際の力仕事は、呼び出されたAPIの先のライブラリ群が担っているわけだ。x86のコードをエミュレーションする場合、アプリ本体コードはx86で書かれているかもしれない。だが、そこから呼び出される本来のAPIの代わりに、該当するARMのネイティブなAPIを呼び出せれば、同じ仕事が行われる。
つなぎのために何らかのラッパレイヤーのようなものを介する必要はあるかもしれない。それでも基本的に同じセットがそろっているはずだから、時間がかかる処理はネイティブなコードに任せることができる。ネイティブのアプリだろうと、エミュレーションのアプリだろうと、ライブラリ部分にかかる時間は同じにできる。もし99%がそのようなライブラリ側で消費されている時間であれば、残り1%のアプリ本体コードが10倍遅くても、全体処理時間は約10%遅いだけで済む。
これに対して、Windows OSやAndroid OS上で昔のMS-DOSもどきを動作させるような場合(そういうソフトウェアは幾つかある)を比較してみよう。このケースではそうは問屋が卸さない。もちろん、エミュレーションしている中でシステムコール、BIOSコールといったものを検出したら相当するネイティブOSのAPIに変換して呼び出すことは可能だ。
しかし、ごくごく低レベルのものに限られる。画面入出力などとなると絶望的である。その昔のプログラムは、ライブラリといってもスタティックリンクのみ(一部オーバレイがあったが)で、画面入出力などはオブジェクトコード内から直接ビデオメモリとハードウェアに読み書きしてしまっていた。
ソフトウェアでエミュレーションしようとすると、そのようなアドレスへの読み書きをいちいち検出して、相当するハードウェアがどのような動作を示すのかを解釈しなければならないわけだ。本来は1命令で済んでいたものが裏側の数百、数千という命令実行を引き起こす。その昔と現在ではクロック速度が3桁違うといっても、決して軽くないオーバーヘッドだ。これを考えるとネイティブな実行環境がある効果は大きい。
命令実行そのものも見てみよう。ライブラリ側よりも自前のコードで多くの処理を行うようなアプリを考えれば、命令実行速度そのものが問われるようになる。
x86の機械語命令コードをエミュレーションしようとする場合、大きく分けて2つの方法がある。1つは、機械語命令コードを1命令ずつ解釈し、それに相当する動作を行うインタープリタ的手法だ。もう1つは、x86の機械語命令コードを相当するARMのネイティブな機械語コードに変換していき、実行できるような塊にまとめてから実行するという手法だ。
どちらも一長一短があるのだが、速度でいったら後者に決まっている。最初に変換時間がかかるものの、終わってしまえば実行速度はネイティブである。多くのアプリの多くの処理時間はループで費やされているから、内側のループをネイティブコード化するだけでも効果は大きい(定石からいえば外側のループはマルチスレッド化である)。中間コードをJIT(Just In Time)コンパイラでネイティブコードに変換して実行するのは、Javaではおなじみで、.NET以来Microsoftもやっていることである。JITみたいなもので、x86からARM変換操作は当然やっているのではないか。
ここでもMS-DOSもどきのエミュレーションみたいなものを考えると、機械語命令の変換といったものがうまく動作しないケースをすぐに思い付く。例えば、昔のx86ゲームソフトでは、自分の命令コードの中の即値データを書き換えてそこに飛び込む、といった荒業を使うものがあった。
プログラムの実行番地によって即値に見えたり、オペコードに見えたり、ということ自体は、インタープリタ的手法では実行可能だ。一方、機械語命令を別な機械語に変換してしまったら、その手合いには対応できない。また、データとして書いたものを実行するには、変換は最初の1回だけという静的な変換方法では対応できない。
あくまでWin32なので、昔のMS-DOSのようなお行儀悪いものはないさ、と思ったのだが、反例を思い付いてしまった。Microsoftのコンパイラではないが、gccを使っても、MinGW(GCCを含むGNU開発環境のツール群をWindows OS上でも利用できるようにしたアプリケーション)のような環境を介してWin32アプリを作ることは一応できる。
gccの場合(確認したのは5.x系のソースであったが)、有名な「トランポリン」という仕組みがあって、ローカル変数の横(当然スタック上)に数命令書き込んでそこに飛び込む場合がある。ただ、こんな荒業が起こるのはCのコードの中で入れ子になった関数定義を書いて、子供の関数ポインターを親の関数のさらに外側で使う、といったかなり特殊なケースに限られる。
こんな荒業が必要な理由は、子の関数が参照可能な親のスタックフレーム位置がコンパイル時には分からず、実行時に確定するためだ。子の関数ポインターを渡された外側の誰かは、そのままだとそんな事情も知らずに適当に関数ポインターを呼び出してしまうので、そのままでは子の関数が親の関数に閉包されている状況を表せないためだと思う。
当然、x86のgccで生成されたコードは、x86機械語命令を実行時に動的に生成してそこに飛び込む。このケースでも最初に1回だけ変換、という単純な処理ではうまくいかないはずだ。まぁ、かなり特殊なので、gccのソースを眺める限り、エンベディッド系のプロセッサの幾つかでは、「トランポリン」を実装していないくらいだ(Linuxをサポートしているメジャーな環境では実装されている)。
そういったもろもろを考え合わせると、メインは機械語から機械語(多分中間コード的なものを介在させるのではないかと思う)への変換としつつ、インタープリタ的な処理ルートも、もちろんダイナミックな変換も利用する、という線だろうか。ダイナミックな変換であれば、より最適化が効かせられるはずなので、速度的にも有利になるはずだ。
こうしてプロセッサ相互の乗り入れが進んでいくと、そのうちx86か、ARMか、といった差を誰も気にしなくなるのかもしれない。Windows OS上のBash on Ubuntu on Windowsなども出てきた(Bash on Ubuntu on Windowsについては、Windows 10 The Latest「Windows 10でUbuntuのシェル「Bash」が動き始める! だが日本語はどうなる?」参照のこと)。Windowsなのか、Linuxなのか、といったOSの差も気にならなくなる。いいことなのだと思う。しかし、バイナリ屋はさらに奥深く、光の届かない海底に深く静かに潜航せざるを得ないだろう。
日本では数少ないx86プロセッサのアーキテクト。某米国半導体メーカーで8bitと16bitの、日本のベンチャー企業でx86互換プロセッサの設計に従事する。その後、出版社の半導体事業部などを経て、現在は某半導体メーカーでヘテロジニアス マルチコアプロセッサを中心とした開発を行っている。
「頭脳放談」
Copyright© Digital Advantage Corp. All Rights Reserved.