IntelのLinuxアプリケーションをRISC-Vでも動かしたい

# 概要

このプロジェクトの目標は、Linux アプリケーションで一般的に使用される Intel MMX、SSE、および AVX の組み込み関数と同等の機能を提供し、それら (または同等の機能) をRISC-Vプラットフォームで利用できるようにすること。これらのX86組み込み関数は、IntelおよびMicrosoftコンパイラで開発されたが、その後GCCコンパイラに移植された。GCC実装は、インライン関数を含むヘッダーセットである。これらのインライン関数は、Intel/Microsoft方言の組み込み関数名から対応するGCC Intel組み込み関数、またはC言語のベクトル拡張構文を介して直接マッピングする実装を提供する。

# 背景

コンピュータの進歩の方向性としては、微細化技術がわかりやすいので注目されやすいが、実は別の方向性も注目されつつある。それは具体的にはCISCからRISCへの移行である。

CISCとは、Complex Instruction Set Computerの略で、複雑命令セットコンピュータである。

CISCの代表として挙げられるのがIntel x86-64であるが、可変長命令であり、1979年に作られたオブジェクトコードが現代の最新のIntel x86-64プロセッサでも実行できる。それはx86-64の強みでもあるのだが、同時に弱みでもある。

CISCが開発された当初は、アセンブラでプログラミングされることが多く、アセンブリプログラムを容易にしたいという目的から、命令セットが次第に複雑化される傾向があった。CISCの命令セットには、例えば、データ構造へのアクセスを支援するための複雑なアドレッシング・モードや、レジスタを退避して、引数をスタック上に引き出す呼び出し命令が含まれる。（A.Vエイホ、「コンパイラ　第二版」）

コンパイラの最適化によって、よりシンプルな命令セットを使うことができるようになる、その結果できたものがRISC-VやARMなどのRISCプロセッサである。

RISCとは、Reduced Instruction Set Computerの略で、縮小命令セットコンピュータのことである。RISC-V、ARM、OpenPOWERなどがそれである。現在はスマホやタブレット、スパコンなどに使われることが多い。

プロセッサ設計において、後方互換性は神聖不可侵である。それはRISC-VなどのRISCでも同じであるが、RISC-Vは比較的新しいアーキテクチャである。背負っているものが少ない。

1979年以降に作られたオブジェクトコードがすべて実行できるということは、プロセッサの複雑性がどんどん増していくことを意味する。x86-64においては、内部でCISC命令をμOpと言われるRISC命令に変換しており、そのオーバーヘッドが存在する。x86-64を積んだコンピュータが熱くなりやすいのはそのせいである。


x86-64は現時点において、サーバやスパコンやPC市場において大きなシェアを握っている。x86-64のソフトウェア資産は膨大であり、世界はそれを手放すことが出来ない。


x86-64の非効率さによる電力などのロスは膨大である。ダイサイズは無駄に大きくなり、熱くなったCPUを冷却する装置なども必要となり、それも電力を消費する。それがデータセンターの収益性を圧迫し、地球温暖化を加速している。

# IntelのIntrinsic関数をRISC-Vにポートする
では、僕たちはそれを放置するしかないのだろうか。そこで提案される方法が、Intel x86-64のIntrinsic関数を他のRISCなどにポートすることである。
今回提案する方法は、x86-64のバイナリコードを直接RISCのバイナリコードに変換する方法ではない。


x86-64で一般的に使用されているSIMD(Single Instruction Multiple Data)演算をC言語からアクセスするための関数がIntel x86-64のIntrinsic関数である。以下のサイトでリストされている。

https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html

SIMDとは、一つの命令発行で複数のデータを同時に処理できるCPU命令である。SIMD用の回路がCPU内に存在する。カテゴリとしては、MMX、SSE、AVXなどが存在する。マルチメディア処理やネットワーク処理、暗号処理、文字列処理などに使用される。ここで重要なのは「Intel x86-64の」Intrinsic関数であり、RISC-VやOpenPOWERなどでは別のIntrinsic関数が存在する。

よって、Intel x86-64のIntrinsic関数が記述されたC言語のソースコードはIntel依存であり、RISCでは動かすことが出来ない。そして、そのIntel依存の膨大なソフトウェア資産があるため、非効率なx86-64から脱却することが出来ない。

#  提案

ではどうすればいいか。答えは Intel x86-64 Intrinsic関数をRISC系のCPU用にポートすることにある。具体的には以下のようなラップ構造を取る。以下の例は、Intel x86-64から他のアーキテクチャに汎用的にポートしたものである。

```C
extern __inline __m128d __attribute__((__gnu_inline__, __always_inline__,__artificial__))
_mm_add_pd (__m128d __A, __m128d __B)
{
   return (__m128d) ((__v2df)__A + (__v2df)__B);
}
```

以下の場所に記述されているようだ。

https://github.com/gcc-mirror/gcc/blob/master/gcc/config/rs6000/emmintrin.h#L267

_mm_add_pdはIntel x86-64のIntrinsic関数である。それは比較的「ゆるっと」ポートしてあるもので、パフォーマンスを最適化しようとしたら更に複雑なソースコードになる可能性がある。

例えば、`_mm_cmpeq_sd`の例を取ってみよう。

```c
extern __inline __m128d __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_cmpeq_sd (__m128d __A, __m128d __B)
{
  return (__m128d)__builtin_ia32_cmpeqsd ((__v2df)__A, (__v2df)__B);
}
```
上のものはあらゆるアーキテクチャ用にポートしたものとなる。以下の場所に定義されている。

https://github.com/gcc-mirror/gcc/blob/master/gcc/config/i386/emmintrin.h#L432

で、OpenPOWERに特化してポートしたものが以下のものとなる。パフォーマンスは改善している可能性がある。

```c
extern __inline  __m128d  __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_cmpeq_sd(__m128d  __A, __m128d  __B)
{
  __v2df __a, __b, __c;
  /* PowerISA VSX does not allow partial (for just lower double)
     results. So to insure we don't generate spurious exceptions
     (from the upper double values) we splat the lower double
     before we do the operation. */
  __a = vec_splats (__A[0]);
  __b = vec_splats (__B[0]);
  __c = (__v2df) vec_cmpeq(__a, __b);
  /* Then we merge the lower double result with the original upper
     double from __A.  */
  return (__m128d) _mm_setr_pd (__c[0], __A[1]);
}
```

以下の場所に定義されている。

https://github.com/gcc-mirror/gcc/blob/master/gcc/config/rs6000/emmintrin.h#L465

このようなラップ構造の利点は、アプリケーション・ソフトウェア本体のコードを改変せず、Intel Intrinsicをexternとinline関数によって置き換えるだけでRISC-VやOpenPOWER対応ができることである。
このようなラップ構造でポートされているのは現時点ではIntelからOpenPOWERであるが、同じ方法でIntelからRISC-Vへもポートできると思われる

では、もう一つの疑問が浮かび上がってくる

このポートしたソースコードは本当に正しいのだろうか。

２−３個試してみてもわからない。偶然結果が合致しただけかもしれない。

今構想しているのは、IntelからRISC-Vにポートしたソースコードの正しさを検証するテストフレームワークである。

例えば、上のソースコードにおいて、ランダムに生成した値をIntel IntrinsicとそれをRISC-Vにポートしたものの__Aと__Bにランダムに生成した同じ値を入れ、帰ってくる値がIntelとRISC-Vで同じかどうか検証する。

IntelとRISC-Vをネットワークで接続する必要はない。まずはIntelで、Intrinsic関数の入力値と戻り値のセットを大量に集めたファイルを作り、RISC-Vにファイルを移動させて、RISC-VのIntrinsic関数をポートしたものに入力して、入力値と戻り値がIntelで生成したものと同じであるかどうかをチェックする。

これには、__m128dの値をテキストに変える（シリアライズする）機構が必要である。なぜかというと、ランダムに生成したIntel Intrinsicに対する入力値と、その出力値のペアの膨大なセットをファイルに保存して、IntelからRISC-Vに移動などして、再びチェックを走らせて、Intel IntrinsicとそれをRISC-Vにポートしたものが等価であるかどうかチェックする必要があるからである。

# GCCにコミットしてDejaGnuに追加
ポートしたソースコードをすべてのアプリケーションに実装するためには、GCCにコミットして、DejaGnuに追加する必要がある。
DejaGnuとは、GCCの開発（GCCでの開発ではない）に使用されるテストスイートである。GCCの開発の基本戦略は、ソースコードを変更してDejaGnuテストスイートを追加することのようだ。テストを削除することはあまりやっていないようだ。
例えば、IntelのIntrinsicをDejaGnuでテストするテストケースに、Intel系でテストするテストケースを、RISC-V系でテストするテストケースに書き換えて追加する必要がある。

以下の例は、IntelからOpenPOWERにポートするときのDejaGnuの書き換えである。

```
/* { dg-do run } */
/* { dg-options "-O2 -msse2" } */
/* { dg-require-effective-target sse2 } */
```
上のものは、Intel系でテストするときのテストケースのヘッダーであるが、中身を変えることなく、以下のように変更してテストケースとして追加する必要がある。

```
/* { dg-do run } */
/* { dg-options "-O3 -mpower8-vector" } */
/* { dg-require-effective-target lp64 } */
/* { dg-require-effective-target p8vector_hw { target powerpc*-*-* } } */
```

これはOpenPOWERにポートするときのヘッダーなので、RISC-Vにポートするときはまた違ったヘッダーにする必要がある。

なおIntelからOpenPOWERへのIntel Intrinsic関数のポートは作業中のようであるが、IntelからRISC-Vへのポートは作業されていないようである。

最後にこれをテーマにした勉強会を毎週土曜日１６時からやってます。無料ですので気軽にご参加ください。

「計算機最適化勉強会(オンライン）　connpass」で検索！！
https://performanceoptimization.connpass.com/

参考文献

コンパイラ 第2版: 原理・技法・ツール (Information&Computing 38)
https://amzn.asia/d/4e115P2

Vector Intrinsics Porting Guide
https://openpowerfoundation.org/specifications/vectorintrinsicportingguide/

下の画像は、IntelからOpenPOWERやRISC-Vのポートに使うHPCのテストフレームワークのマスコットです。生成AIで作りました。

![Akari3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53172/c016a5dd-5aef-4f38-9405-b752000d8c2a.png)