FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

LLVMのPassの作り方について学ぶ(ドキュメントを読む3)

LLVMについて、バックエンドの部分はある程度勉強したけど、そういえばPassの作り方をまじめに勉強したことが無かった。

LLVMと言えばPassだろう!ということでPassの作り方について勉強することにした。これはそのうちLLVMの拡張や最適化をしたいときに役に立つはずだ。

いくつかのPassについて。CallGraphSCCPassクラス、FunctionPassクラス、LoopPassクラス。

llvm.org


LLVMのPassを書く

CallGraphSCCPassクラス

CallGraphSCCPassはpassがプログラムの呼び出しグラフを下から上にする必要があるときに使用する(呼び出し先から呼び出し元へ)。 CallGraphSCCPassCallGraphを作成するメカニズムを提供するが、システムがCallGraphSCCPassの最適化実行も許す。 あなたのpassが以下のアウトラインに示す要件に合致し、FunctionPassの要件に合致しないならば、CallGraphSCCPassから派生させるべきです。

TODO: SCCとは何か、Tarjanのアルゴリズム、B-U meanについて説明する。

CallGraphSCCPassサブクラスについてその役割を明確にすると:

  1. 現在のSCCおよびSCCの直接呼び出し元と直接呼び出し先以外の関数を検査したり変更したりすることはできません。
  2. 現在のCallGraphオブジェクトを保存し、プログラムに加えられた変更を反映して更新することが求められる。
  3. SCC の内容を変更することはできても、現在のモジュールに SCC を追加したり削除したりすることはできない。
  4. 現在のモジュールにグローバル変数を追加・削除することができる。
  5. runOnSCC の起動に関わらず、状態を維持することができる(グローバルデータを含む)。

Note: SCCって何? --> "Strongly Connected Component"

SCCを1つ以上のノードで管理しなければならないため、CallGraphSCCPassを実装するのは、いくつかの場合において非常にトリッキーです。 以下で説明するすべてのバーチャルメソッドでは、プログラムを変更した場合にはtrueを、そうでない場合はfalseを返す必要があります。

doInitialization(CallGraph &)メソッド

virtual bool doInitialization(CallGraph &CG);

doInitiazilationメソッドはCallGraphSCCPassが許可していないことのほとんどを許可するメソッドである。関数の追加や削除、関数のポインタの取得などが行えます。doInitializatoinメソッドは処理されるSCCに依存しない単純な初期化などに使用されます。doInitilizationメソッドは他のpassの実行とオーバラップしないようにスケジュールされます(従ってこのメソッドは非常に高速であるべきです)。

runOnSCCメソッド

virtual bool runOnSCC(CallGraphSCC &SCC) = 0;

runOnSCCメソッドはpassの最も興味のある動作を行う場所です。モジュールが変換により変換された場合はtrueを返し、そうでない場合はfalseを返すべきです。

doFinalization(CallGraph &)メソッド

virtual bool doFinalization(CallGraph &CG);

doFinalizationメソッドは、passのフレームワークコンパイルされるプログラムのすべてのSCCに対してrunOnSCCを呼び終わった後に呼び出されます。

FunctionPassクラス

ModulePassサブクラスと対比して、FunctionPassサブクラスは予測可能な、システムによって予測可能な動作を持っている。すべてのFunctionPassはプログラム中の関数を独立に実行する。FunctionPassは特定の順序で動作する必要はなく、FunctionPassは外部の関数を変更しない。

明確化するために、FunctionPassサブクラスは以下を許可しない:

  • 現在処理されているFunction以外の関数を検査や変更すること
  • 現在のModuleから関数を追加もしくは削除すること
  • 現在のModuleからグローバル変数を追加または削除すること
  • runOnFunctionの起動の状態を操作すること(グローバルデータを含む)

FunctionPassの実装は通常は単純です("Hello World"passの例を参照のこと)。FunctionPassは3つのバーチャルメソッドをオーバロードする。すべてのメソッドは、プログラムを変更した場合にはtrueを返す必要があり、そうでない場合はfalseを返す。

doInitialization(Module &) メソッド

virtual bool doInitialization(Module &M);

このdoInitializationメソッドはFunctionPassが許可しない殆どの操作を行うことができます。関数の追加削除や、関数のポインタの取得などが行えます。このdoInitializationメソッドは処理する関数に依存しないタイプの様々なことを簡単に初期化するために設計されています。 doInitializationメソッドは他のpassとオーバラップして呼び出されるようにはスケジュールされていません(従って非常に高速であるべきです)。

このメソッドの使い方の良い例としてLowerAllocationpassがあります。 このpassはmallocfree命令をプラットフォームに依存するmalloc()free()関数呼び出しに置き換えます。 doInitializationメソッドではmallocfree関数への参照を取得し、必要ならばモジュールにプロトタイプを追加します。

runOnFunctionメソッド

virtual bool runOnFunction(Function &F) = 0;

runOnFunctionメソッドはpassによる変換や解析処理を行うために実装する必要があります。 通常は、関数が変更されたらtrueを返します。

doFinalization(Module &)メソッド

virtual bool doFinalization(Module &M);

doFinalizationメソッドはあまり使われませんが、passのフレームワークが、コンパイルされるプログラムに対してすべてのrunOnFunctionの呼び終えた後に呼び出されます。

LoopPassクラス

LoopPassクラスは関数内の独立したループに対して適用されます。LoopPassはネストされた順番に実行され、つまり最も上位のループが最後に実行されます。

LoopPassサブクラスはLPPassManagerインタフェースを通じてループネストをアップデートすることが許可されています。loop passを実装することは通常は単純です。LoopPassは3つバーチャルメソッドをオーバロードする必要があります。すべてのこれらのメソッドは、プログラムを変更した場合はtrueを返し、そうでない場合はfalseを返すべきです。

LoopPassサブクラスは、同じ関数の解析を実行するメインのループパスパイプラインの一部として実行するように意図されています。これを簡単にするために、LoopUtils.hにはgeTLoopAnalysisUsage関数が提供されています。これはサブクラスのgeTAnalysisUsageのオーバライドの中で呼び出すことが出来、一貫性と正しい動作を行うために必要です。同様に、INITIALIZE_PASS_DEPENDENCY(LoopPass)はいくつかの関数の解析を初期化します。

doInitialization(Loop *, LPPassManager &)メソッド

virtual bool doInitialization(Loop *, LPPassManager &LPM);

doInitializationメソッドは処理する関数に依存しない簡単な初期化を実行します。doInitializationメソッドは他のpassと実行がオーバラップしないようにスケジュールされます(従って、この関数は素早く処理を終える必要があります)。LPPassManagerインタフェースはFuncitonModuleレベルの解析情報にアクセスするために使用します。

runOnLoopメソッド

virtual bool runOnLoop(Loop *, LPPassManager &LPM) = 0;

runOnLoopメソッドは、サブクラス内、変換及び解析を行うために実装しなければならないメソッドです。通常、関数が変更された場合にはtrueを返す必要があります。LPPassManagerインタフェースはループネストをアップデートするために必要です。

doFinalizationメソッド

virtual bool doFinalization();

doFinalizationメソッドはあまり使われることのないメソッドですが、passフレームワークが、コンパイルされるプログラム内のすべてのループに対してrunOnLoopの呼び出しを終了すると呼び出されます。

RegionPassクラス

RegionPassLoopPassと似ていますが、関数内のエントリから終了までを実行します。RegionPassはその領域をネスト順に実行し、もっとも外側の領域が最後に処理されます。

RegionPassサブクラスはRGPassManagerインタフェースを使って領域ツリーを更新することが許されています。RegionPass内の3つのバーチャルメソッドをオーバロードして独自のリージョンpassを実装することができます。これらのメソッドは、プログラムを変更した場合にはtrueを返す必要があります。

doInitialization(Region *, RGPassManager &)メソッド

virtual bool doInitialization(Region *, RGPassManager &RGM);

doInitializationメソッドは処理される関数に依存しない簡単な初期化などを行うためのものです。doInitializationメソッドは他のpassの実行とオーバラップしないようにスケジュールされます(したがって高速に動作する必要があります)。RPPassManagerインタフェースはFunctionもしくはModuleレベルの解析情報にアクセスするために必要となります。

runOnRegionメソッド

virtual bool runOnRegion(Region *, RGPassManager &RGM) = 0;

runOnRegionメソッドは、変換や解析などの実行するために必要な関数です。通常、その領域を変更した場合にはtrueを返します。RGPassManagerは領域ツリーをアップデートするために必要です。

doFinalization()メソッド

virtual bool doFinalization();

doFinalizationメソッドはあまり使われることのないメソッドで、コンパイルされるプログラムのすべての領域に対してrunOnRegionが実行された後に呼び出されます。

MachineFunctionPassクラス

MachineFunctionPassLLVMコードジェネレータの一部で、プログラム中のマシンに依存する表現に対して実行されるものです。

コードジェネレータpassはTargetMachine::addPassToEmitFileにより特別に登録され初期化されます。従ってこれらは一般的にoptbugpointコマンドによって実行することはできません。

MachineFunctionPassFunctionPassでもあります。したがってFunctionPassのすべての制約はMachineFunctionPassにも当てはまります。MachineFunctionPassは以下のことが許可されていません:

  1. LLVM IRInstruction, BasciBlock, Argument, Function, GlobalVariables, GlobalAlias, Moduleの作成または変更
  2. 現在処理しているMachineFunction以外のMachineFunctionの変更
  3. runOnMachineFunctionの起動を促すステートの調整 (グローバルデータも含む)

runOnMachineFunction(MachineFunction &MF) メソッド

virtual bool runOnMachineFunction(MachineFunction &MF) = 0;

runOnMachineFunctionMachineFunctionPassのメインのエントリポイントと考えることができます; つまり、このメソッドをオーバライドしてMachineFunctionPassを実装することができます。

TerosHDL for Visual Studio Code を試してみる

ネットで回ってきたVisual Studio Code用のVerilog/VHDLの総合開発環境プラグインTerosHDLが気になったので少し試してみた。

github.com

インストールはVisual Studio Codeプラグイン環境で簡単にインストールすることができた。 私はWSL2の環境(とリポジトリはWSL2上に存在しているので)にVerilatorとVUnitをインストールしておく。

pip install vunit_hdl

インストールできるまではいいのだが、どうも思い通りに動いてくれない。ステートマシンを試しに作ってみて表示させてみようとしたのだが上手く認識してくれない。

f:id:msyksphinz:20210920190227p:plain

Module declaration map はちゃんと出てくれるようだ。

f:id:msyksphinz:20210920190719p:plain

Dependencies viewerも動いてくれない。警告表示で"Install Python3 and configure the binary path in TerosHDL plugin configuration. Install VUnit: sudo pip3 install vunit_hdl"と出るのだが、ちゃんとWSL2上にこれらはインストールしてるんだけどなあ?何がダメなんだろう?

f:id:msyksphinz:20210920190938p:plain

LLVMのPassの作り方について学ぶ(ドキュメントを読む2)

LLVMについて、バックエンドの部分はある程度勉強したけど、そういえばPassの作り方をまじめに勉強したことが無かった。

LLVMと言えばPassだろう!ということでPassの作り方について勉強することにした。これはそのうちLLVMの拡張や最適化をしたいときに役に立つはずだ。

今回は少し進んで、LLVMのPassのクラスについて。いくつかのPassを構成するクラスがあり、最適なものを選択する必要があるということ。

llvm.org


LLVMのPassを書く

passのクラスと必要事項

新しいpassをデザインするときにまず考えなければならないのは、そのpassがどのクラスのサブクラスなのかということです。 "Hello World"の例ではFunctionPassを使用しましたが、なぜこのpassを使ったのか、他にどのようなpassがあるのかについては特に議論しませんでした。 ここではどのようなクラスが利用なのか、最も一般的なものから専門的なものまで紹介します。

Passのスーパークラスを選択する際、可能ならばその要求に適した最も専門的なクラスを選択すべきです。 これにより、LLVM Passインフラストラクチャはpassの実行方法を最適化するために必要な情報を提供し、 結果としてコンパイラが不必要に遅くならないようにします。

ImmutablePassクラス

もっとも一般的なタイプのpassはImmutablePassクラスです。 このpass型は実行する必要のない、状態を変更しない、更新する必要のないpassに対して使用します。 これは変換もしくは解析の普通の型ではありませんが、現在のコンパイラのコンフィグレーションについての情報を提供することができます。

このpassクラスは殆ど使用されることはありませんが、コンパイルされている現在のターゲットマシンについての情報や、 いくつかの変換に影響を与える他の静的な情報を提供するために重要です。

ImmutablePassは他の変換を決して無効化せず、また決して無効化されず、また決して「実行」されることもありません。

ModulePassクラス

ModulePassクラスはすべてのスーパークラスのもっとも一般的なものです。ModulePassから派生したものは、 このpassはプログラム全体を使用することを意味し、単体として、predictable順ではなく関数の本体を参照し、関数を追加または削除します。 ModulePassサブクラスについて何も知らないため、このpassの実行では最適化は実行できません。

モジュールpassは、任意のモジュールまたはimmutable passを使用しないとき、getAnalysisインタフェースgetAnalysis<DominatorTree>(llvm::Function *)を使用する関数レベルのpass(例: dominatorなど)を使用することができ、関数に対して全体的な解析の結果を提供します。 これは解析の行われた関数に対してのみ行われます。例えば、dominatorの場合はDominatorTreeの関数定義に対してのみ問い合わせるべきであり、宣言に対しては問い合わせるべきではありません。

正しいModulePassサブクラスを実装するためには、ModulePassから派生してrunOnModuleメソッドを以下のようにして実装する必要があります:

runOnModuleメソッド

virtual bool runOnModule(Module &M) = 0;

runOnModuleメソッドはpass乗で面白い動作をします。 変換処理によりモジュールを変更した場合、trueを返す必要があり、そうでない場合はfalseを返します。

LLVMのPassの作り方について学ぶ(ドキュメントを読む1)

LLVMについて、バックエンドの部分はある程度勉強したけど、そういえばPassの作り方をまじめに勉強したことが無かった。

LLVMと言えばPassだろう!ということでPassの作り方について勉強することにした。これはそのうちLLVMの拡張や最適化をしたいときに役に立つはずだ。

参考にしたのはWriting LLVM Passというドキュメント。読み進めていくとこのPass Managerは古いそうなのだが、まあまずは古い方のドキュメントを読んで、新しい方の差分を確認していけばよかろう。

llvm.org


LLVMのPassを書く

Passとは何か?

LLVMのPassフレームワークLLVMシステムで重要な部分です。 LLVM Passはコンパイラ内に存在する面白い部分殆どに存在しています。 Passは、コンパイラを構成する変換や最適化を行い、これらの変換で使用される解析結果を構築し、そして何よりもコンパイラコードの構造化技術です。

LLVMのすべてのPassはPassクラスのサブクラスで、仮想メソッドをPassクラスから継承してオーバライドすることによって機能を実装します。 Passがどのように動作するかに依存して、ModulePass, CallGraphSCCPass, FunctionPass, LoopPass, RegionPassクラスのどれから継承するかを選択する必要があります。 それぞれのクラスがpassがどのように動作するかの情報のシステムを提供し、どのように他のpassと組み合わせられるのかについての情報を提供します。 LLVM Passフレームワークの大きな特徴の一つに、あなたのpassの要件(どのクラスから継承されたか、を示す)をもとに効率的なpassの実行方法のスケジューリングがあります。

まずはどのようにpassを構築するのか、コードをセットアップしてコンパイルし、ロードして実行することですべてを見ていきましょう。 基本を押さえた後で、より高度な機能について議論します。

注意: このドキュメントでは、レガシーなpassマネージャを扱っています。 LLVMは最適化パイプラインのために、新しいpassマネージャをデフォルトで使用しています(codegenパイプラインはまだレガシーパスマネージャを使用しています)。 詳細については、Writing an LLVM PassUsing the New Pass Managerを参照のこと。 optでレガシーpassマネージャを使用するには、すべてのoptの呼び出しに-enable-new-pm=0フラグを渡してください。

クイックスタート: hello worldを書く

ここでは、"hello world" passを書く手順について説明します。 "Hello" passは単純にコンパイルされたプログラムのうち、外部関数ではないものの名前をプリントアウトするだけのものです。 これはプログラムを全く変更せずに、単純に調査するだけです。 このpassのソースコードとファイルはLLVMのソースツリー内のlib/Transform/Helloディレクトリに置かれています。

ビルド環境を構築する

まず、LLVMを校正してビルドします。次にLLVMのソースベースに対して新しいディレクトうぃろ作成する必要があります。 この例では、lib/Transforms/Helloに新しいディレクトリを作成したものとします。 最後に、新しいpassのためのソースコードコンパイルするためのビルドスクリプトを用意する必要があります。 このために、以下のコードをコピーしてCMakeLists.txtとして保存してください。

add_llvm_library( LLVMHello MODULE
  Hello.cpp

  PLUGIN_TOOL
  opt
  )

そして以下のコードをlib/Transforms/CMakeLists.txtに挿入します。

add_subdirectory(Hello)

(すでにHelloディレクトリが作成されており、"Hello"サンプルpassが実装されている場合、すぐにそれを試すことができます。 この場合はCMakeLists.txtファイルを変更する必要はありません。 すべてを一からから作りたい場合、別の名前を使用して試すことができます)

ビルドスクリプトは当該ディレクトリのHello.cppを指定しており、これがコンパイルされて共有オブジェクト$(LEVEL)/lib/LLVMHello.soとしてリンクされます。 このオブジェクトはopt-loadオプションによって動的んいロードされます。 使用しているオペレーティングシステム.so以外の接尾子を使っている場合(WindowsMacOSなど)、適切な拡張子が使用されます。

ビルドスクリプトの構築が完了したら、次にpassを書いていきます。

必須の基本コード

ここまでで新しいpassをコンパイルするための準備が整いました。次にコードを書いていきます。まず以下から始めましょう。

#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

Pass.hは私たちは今Passを書いているので必要なもの、 何かをプリントするのでFunctionが必要です。

次に以下を追加します。

using namespace llvm;

これは、インクルードファイルに機能はllvm名前空間に存在しているからです。

次に、

namespace {

これは無名の名前空間から始まるという意味です。無名の名前空間C++ではstaticキーワードでC言語から(グローバルスコープで)使用できます。 無名の名前空間で定義したものは、このファイルでのみ参照できます。 もしこの機能にあまり詳しくなければ、まともなC++の本を読んで参考にしてください。

次に、私たちのpassを宣言します:

static Hello : public FunctionPass {

これはFunctionPassのサブクラスとして"Hello"クラスを提起します。 ビルトインpassのサブクラスの違いは以降で説明しますが、 今はFunctionPassは関数を一度に処理するということだけ知っておいてください。

static char ID;
Hello() : FunctionPass(ID) {}

これはLLVM内でpassを識別するための識別子を定義します。 これによりLLVMC++の高価なランタイム情報を使用することが回避できます。

  bool runOnFunction(Function &F) override {
    errs() << "Hello: ";
    errs().write_escaped(F.getName()) << '\n';
    return false;
  }
}; // end of struct Hello
}  // end of anonymous namespace

runOnFunction()メソッドを定義します。 これは私たちのやりたいことを実現する関数で、各関数の名前をメッセージとして出力します。

ここでpassのIDを初期化します。LLVMはpassを識別するためにIDアドレスを使用しますので、この初期化はあまり重要ではありません。

static RegisterPass<Hello> X("hello", "Hello World Pass",
                             false /* Only looks at CFG */,
                             false /* Analysis Pass */);

最後に、Helloクラスを登録しますし、 コマンドライン引数として"hello"を使えるようにし、名前を"Hello World Pass"とします。 最後の2つの引数はこのクラスの動作を示しています: もしpassがCFG中を探索し、それを変更しない場合、3つ目の引数をtrueに設定します; もしそのpassが解析pass、例えばdominator tree passの場合、4番目の引数をtrueに設定します。

もしpassを既存のパイプラインの1ステップとして登録したい場合は、いくつかの拡張ポイントが提供されます。 例えば、PassManagerBuilder::EP_EarlyAsPossble は私たちのpassを最適化の前に適用し、 PassManagerBuilder::EP_FullLinkTimeOptimizationLastではリンク時最適化の後に挿入します。

static llvm::RegisterStandardPasses Y(
    llvm::PassManagerBuilder::EP_EarlyAsPossible,
    [](const llvm::PassManagerBuilder &Builder,
       llvm::legacy::PassManagerBase &PM) { PM.add(new Hello()); });

全体として、.cppファイルは以下のようになります。

#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"

#include "llvm/IR/LegacyPassManager.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"

using namespace llvm;

namespace {
struct Hello : public FunctionPass {
  static char ID;
  Hello() : FunctionPass(ID) {}

  bool runOnFunction(Function &F) override {
    errs() << "Hello: ";
    errs().write_escaped(F.getName()) << '\n';
    return false;
  }
}; // end of struct Hello
}  // end of anonymous namespace

char Hello::ID = 0;
static RegisterPass<Hello> X("hello", "Hello World Pass",
                             false /* Only looks at CFG */,
                             false /* Analysis Pass */);

static RegisterStandardPasses Y(
    PassManagerBuilder::EP_EarlyAsPossible,
    [](const PassManagerBuilder &Builder,
       legacy::PassManagerBase &PM) { PM.add(new Hello()); });

すべてを組み合わせて、ビルドディレクトリのトップで単にgmakeコマンドを入力すればlib/LLVMHello.soファイルが生成されているはずです。 このファイルに含まれているものすべてが無名名前空間に含まれています - これはpass自体が自身を内蔵しているユニットであり、 外部のインタフェースを必要としない(しかし必要とあれば持つこともできます)ということを意味します。

optを使ったpassの実行

私たちは新しく作った、輝かしい共有オブジェクトを手に入れました。optコマンドを使ってLLVM実行中にpassを渡すことができます。 RegisterPassを使ってpassを登録したので、optツールによってそれにアクセスすることができます。

テストのために、Getting Started with the LLVM System の最後に使った例を使って "Hello World"をLLVMコンパイルしてみましょう。 ビットコードファイル(hello.bc)のプログラム全体を以下のようにして変換します(もちろん、どのようなビットコードでも問題ありません)。

$ opt -load lib/LLVMHello.so -hello < hello.bc > /dev/null
Hello: __main
Hello: puts
Hello: main

-loadオプションによってoptがどのような共有オブジェクトをpassとしてロードすべきなのかを指定しており、 これにより-hello引数が有効となります。(これが[passの登録]の必要な理由です)。 Hello passはどのような興味深い方法でもプログラムを全く変更しないので、私たちはoptの結果を単純に捨てています(結果を/dev/nullに渡しています)。

他にどのような文字列を登録したのか、opt-helpオプションで見てみましょう:

$ opt -load lib/LLVMHello.so -help
OVERVIEW: llvm .bc -> .bc modular optimizer and analysis printer

USAGE: opt [subcommand] [options] <input bitcode file>

OPTIONS:
  Optimizations available:
...
    -guard-widening           - Widen guards
    -gvn                      - Global Value Numbering
    -gvn-hoist                - Early GVN Hoisting of Expressions
    -hello                    - Hello World Pass
    -indvars                  - Induction Variable Simplification
    -inferattrs               - Infer set function attributes
...

passの名前に、あなたのpassの情報文字列が追加され、optのいくつかのドキュメントに追加されています。 これで動作するpassを手に入れることができましたので、次によりクールな変換用のpassの実装に移ることができます。 一度動作を確認しテストすると、このpassがどれくらいの速度なのかをチェックできると便利です。 PassManagerは便利なコマンドラインオプション(-time-passes)を提供しており、他のpassと一緒にあなたのpassが どれくらいの実行時間だったのかを知ることができます。例えば:

$ opt -load lib/LLVMHello.so -hello -time-passes < hello.bc > /dev/null
Hello: __main
Hello: puts
Hello: main
===-------------------------------------------------------------------------===
                      ... Pass execution timing report ...
===-------------------------------------------------------------------------===
  Total Execution Time: 0.0007 seconds (0.0005 wall clock)

   ---User Time---   --User+System--   ---Wall Time---  --- Name ---
   0.0004 ( 55.3%)   0.0004 ( 55.3%)   0.0004 ( 75.7%)  Bitcode Writer
   0.0003 ( 44.7%)   0.0003 ( 44.7%)   0.0001 ( 13.6%)  Hello World Pass
   0.0000 (  0.0%)   0.0000 (  0.0%)   0.0001 ( 10.7%)  Module Verifier
   0.0007 (100.0%)   0.0007 (100.0%)   0.0005 (100.0%)  Total

見てわかる通り私たちの実装はかなり高速です。他のpassはoptによって自動的に挿入されたもので、 あなたのpassによってLLVMから出力されたものが有効でLLVMにより正しく構成されているもの確認し、 何も壊されていないことを確認するためのものです。

ここまでで、passの基本とpassの裏側にあるメカニズムについてみてきました。 これらがどのように動作するのかと、どのように使用すればよいのかについてより詳細に見ていきましょう。

自作RISC-V OoOコアでDhrystoneが完走した

ちまちまと自作RISC-Vコアを実装している。riscv-testsがかなり通っているので、Dhrystoneを実行することにした。

実はDhrystoneは少し前に動かしていたのだけれども、L1Dキャッシュのデータリプレースの部分になかなか難しいバグがあり、難しくなっていた部分を一気に書き換えてバグを全面的に潰した。 その結果、Dhrystoneが完走できるようになった。

riscv-testsにはすでにDhrystoneなどのベンチマークが備わっているが、現在の実装では上手く動かすことが出来ない。 いくつかの原因があって、

  • FPUをサポートしていないので、GCCコンパイルオプションを変更する必要がある
  • tohost / fromhostを正しく実装していないので、printf()の実装をいくつか変更する必要がある

まずはriscv-tests/benchmarks/の中で、FPUが必須のベンチマークプログラムを外して、コンパイルオプションを変更する。

diff --git a/benchmarks/Makefile b/benchmarks/Makefile
index c9469e2..44316be 100644
--- a/benchmarks/Makefile
+++ b/benchmarks/Makefile
@@ -24,20 +24,21 @@ bmarks = \
        towers \
        vvadd \
        multiply \
-       mm \
        dhrystone \
-       spmv \
-       mt-vvadd \
-       mt-matmul \
        pmp \

+#      mm \
+#      spmv \
+#      mt-vvadd \
+#      mt-matmul \
+
 #--------------------------------------------------------------------
 # Build rules
 #--------------------------------------------------------------------

 RISCV_PREFIX ?= riscv$(XLEN)-unknown-elf-
 RISCV_GCC ?= $(RISCV_PREFIX)gcc
-RISCV_GCC_OPTS ?= -DPREALLOCATE=1 -mcmodel=medany -static -std=gnu99 -O2 -ffast-math -fno-common -fno-builtin-printf -fno-tree-loop-distribute-patterns
+RISCV_GCC_OPTS ?= -march=rv$(XLEN)imc -mabi=lp$(XLEN) -DPREALLOCATE=1 -mcmodel=medany -static -std=gnu99 -O2 -ffast-math -fno-common -fno-builtin-printf -fno-tree-loop-distribute-patterns
 RISCV_LINK ?= $(RISCV_GCC) -T $(src_dir)/common/test.ld $(incs)
 RISCV_LINK_OPTS ?= -static -nostdlib -nostartfiles -lm -lgcc -T $(src_dir)/common/test.ld
 RISCV_OBJDUMP ?= $(RISCV_PREFIX)objdump --disassemble-all --disassemble-zeroes --section=.text --section=.text.startup --section=.text.init --section=.data

次に、syscalls.cを変更してputchar()を実行した後にfromhostの状態変更を待たずにすぐに戻るように変更する。

diff --git a/benchmarks/common/syscalls.c b/benchmarks/common/syscalls.c
index 39547b3..f3f8836 100644
--- a/benchmarks/common/syscalls.c
+++ b/benchmarks/common/syscalls.c
@@ -25,8 +25,8 @@ static uintptr_t syscall(uintptr_t which, uint64_t arg0, uint64_t arg1, uint64_t
   __sync_synchronize();

   tohost = (uintptr_t)magic_mem;
-  while (fromhost == 0)
-    ;
+  // while (fromhost == 0)
+  //   ;
   fromhost = 0;

   __sync_synchronize();

この変更したことで、Dhrystoneが完走できた。

1906722 : 201307 : PC=[000000008000264e] (20,02) 0017e793 ori     a5, a5, 1
GPR[15](178) <= 0000000000000001
1906722 : 201308 : PC=[0000000080002652] (20,04) fffff717 auipc   a4, 0xfffff
GPR[14](60) <= 0000000080001652
1906722 : 201309 : PC=[0000000080002656] (20,08) 9af73723 sd      a5, -1618(a4)
MW8(0x0000000080001000)=>0000000000000001
1906722 : 201310 : PC=[000000008000265a] (20,10) 0000a001 c.j     pc + 0
GPR[00](0) <= 0000000000000000
1906738 : L1D Stq Store : 80001000(00) : ______________________________________________________00000000_00000001
===============================
SIMULATION FINISH : PASS
===============================
f:id:msyksphinz:20210921235632p:plain

ただし分岐予測が全く実装できていないため、性能については全くダメ... これは今後の実装課題だな。

RISC-V Vector Extension v1.0がリリースされた

これまでのものはv1.0のRC1だったりRC2だったりするけど、いよいよv1.0正式バージョンについてのPublic Review版がリリースされた。

一応差分をチェックしてみたが、本文内の曖昧の所を明文化したようなところが多い。 以下にまとめたのはRC1→RC2からのさらに変更点なので、v1.0はRC2からの以下のさらに変更点が加えられたことになる。

github.com


  • いくつかの文章の言い回しの改善。デザインに依存する項目について追加。
  • vpopc.mvcpop.mに名前変更。スカラ命令との統一性のため。エイリアスとして古い名前はアセンブラに残す。
  • マスク論理命令、vmsbf.m, vmsif.m, vmsof.m マスク操作命令では結果によてすべてのマスクレジスタへの書き込みを許す (つねにTail-agnosticとなる)
  • ベクトル長拡張 "Zvl*"を追加
  • mstatus.VSフィールドへの書き込み操作を明記し、mstatus.SDとの相互作用について追記。
  • ハイパーバイザー化における動作について定義を追加。
  • vtypeにおける不正な値のチェックについて明確化。
  • 呼び出し規約についての項目を追加。例題を理解するためのプレースホルダとしてのみ設置し、RISC-VのpsABIはこれらの項目を含むように拡張される。
  • vxsatビットのはCSRの0ビット目であり、上位のビットは0が書き込まれるべきであることを追加。
  • vxrmフィールドはCSRの下位の2ビットであり、上位のビットは0が書き込まれるべきであることを追加。
  • 1つの要素が複数のベクトルレジスタへの書き込みを行う場合(ELEN > VLEN)の場合のオプションについて説明を簡素化し、この時点ではこの方式は提案されていない。
  • 実装ではEEWの幅が標準的なロードとストア命令においてサポートされていない場合では実装が不正命令例外を発生するべきであるということを追加。
  • セグメントロードストア命令のためのZvlsseg拡張名を削除し、標準的なすべてのベクトル拡張に搭載する必要がある。
  • 全体レジスタロードストア命令においてnfはNFIELDと同様にエンコードされることを明確化。
  • レジスタオーバラップの制約を違反した命令は予約済みであることを明文化。
  • Zfinx/Zdinx/Zhinx拡張がベクトル命令に搭載された場合の浮動小数点スカラ値の扱いについて注記を追加。
  • 以前のアセンブラニーモニックであるvmandnotvmornotvmandnvmornに変更され、同じ動きをする。古いvmandnotvmornotニーモニックアセンブラエイリアスとして残される。
  • 標準ベクトル拡張をサポートした実装ではmisa.vが設定されることを明文化。

殆どの項目は大きな変更がある訳では無いが、Zvl*についてはVLENの最低長を示しているらしい。これそんなに必要あるかな?

Extension Minimum VLEN
Zvl32b 32
Zvl64b 64
Zvl128b 128
Zvl256b 256
Zvl512b 512
Zvl1024b 1024

リングフィットアドベンチャー本編をクリアした

春辺りからリングフィットアドベンチャーを毎日コツコツやっていたのだが、ついに本編を全クリアした。長かった...

全クリアまで100日以上かかっている。地道にやっていったがついにコンプリートだ。

f:id:msyksphinz:20210920222928j:plain

元々のモチベーションは、コロナで外で運動とかできなくなったということ。普段は結構マラソンと化していたのだが、 コロナであまりできなくなってしまった。 そこでちょうど電気屋さんでNintendo Switchを手に入れて、物は試しと思ってリングフィットアドベンチャーを購入してみたのだった。

ちなみにSwitchはリングフィット以外で全く活躍していない(ちょっと桃鉄をやってみたくらい)。ひたすらリングフィット専用マシンとなっていた。

このゲームは運動負荷を設定できるのだが、たしか極めて標準的(19だったかな?)な設定でやっていた気がする。まあ結果的にこれくらいでちょうど良かったかも。

ゲーム自体の感想としては、定期的に細かく運動できるし、1日に10~20分程度でできるので非常にやりやすい。 私は退社してからお風呂に入るまで10~20分くらいを毎日やっていた。まあ本当に毎日続くものではなくて、夏場にひどい風邪をひいたときは2週間くらいはストップというのもあるけど、 結果的に再開してコンプリートまで行けた。

で、実際のところの効果だが、100日以上やって本当に効果があったかというと、それは分からない。 そもそも始める前に体重とか計っていなかったので完了したときにどれだけ体重が変化したのかは分からない。 それは少し後悔。

これからもちょくちょくやっていきたいと思う(まだ全ステージのコースを100%クリアしたわけじゃないし)。 それと次はフィットボクシングをやってみたいかな。後は(だいぶ後発だが)マリオカート