Haskell GHC のバックエンドにLLVMを使う

こんにちは｜こんばんは。カエルのアイコンで活動しております @kyamaz :frog: です。

## GHCの環境構築

Haskellの環境構築は、GHCのバージョンやバージョン管理＆パッケージ管理の選択（Stackか、GHCup+Cabalか）によって注意が必要です。@mod_poppoさんの次のエントリを参考にして整えてください。

https://zenn.dev/mod_poppo/articles/haskell-setup-2023

https://qiita.com/mod_poppo/items/1abc155b5a5265d7dc45

2024年1月現在では、GHCupで示されるrecommendは次の通り。本稿では、この組み合わせを前提に記載します。

- GHCup 0.1.20.0
- Stack 2.13.1
- cabal 3.10.2.1
- GHC   9.4.8

## GHCのバックエンドの選択

GHCは、複数のコードジェネレータをバックエンドとして使うことができます。GHCのデフォルトのバックエンドは、プログラムを内部中間表現形式 Cmm言語（C--、単純でCライクな言語）にして、それを実行可能コードにコンパイルします。Haskellのソースコードをコンパイルするときに、`ghc`コマンドの引数でバックエンドをスイッチすることができます。現時点では、２つのバックエンドをスイッチできます。

**ネイティブコード生成器（-fasm）**

GHCのデフォルトのバックエンドです。Cmmからアセンブリコードまでコンパイルするネイティブコード生成器です。このネイティブコード生成器は高速なバックエンドであり、通常は優れたパフォーマンスのコードを生成します。また、共有ライブラリのコンパイルをサポートする最も良いコード生成器です。

**LLVMコード生成器（-fllvm）**

LLVMコンパイラを使って実行可能コードを生成するバックエンドです。ネイティブコード生成器と遜色のない性能のコード生成されます。ベクトル等のパッケージを使用する重めの配列を扱うコードのような場合では、より高速なコードが生成されることもあります。ただし、コンパイル時間が大幅に増加するようなトレードオフがあります。

では、実際にコードジェネレータを換えてコンパイルしてみましょう。サンプルは簡単なコードにしたいので、何も結果を出さない次のコードを試します。

```haskell: prog-asm.hs / prog-llvm.hs
main = pure()
```

### ネイティブコード生成器の場合

```shell-session: -fasm オプションでコンパイルする 
$ ghc -fasm prog-asm.hs
[1 of 2] Compiling Main             ( prog-asm.hs, prog-asm.o )
[2 of 2] Linking prog-asm [Objects changed]
```
（`ld: warning: ignoring duplicate libraries: '-lm'
`というワーニングがでますが、本家の[issue#24167](https://gitlab.haskell.org/ghc/ghc/-/issues/24167)にあり、既知の問題ですので気にしないこととします。）

コンパイルを実行すると次のファイルが出力されます。

- prog-asm.hi　（Haskellインターフェース・ファイル）
- prog-asm.o　（生成途中のオブジェクトファイル）
- prog-asm （実行ファイル）

実行しても何も起きませんが、実行ファイルを実行してエラーが発生しないで、正しくコンパイルできていることを確認してください。


### LLVMコード生成器の場合

```shell-session: -fllvm オプションでコンパイルする 
$ ghc -fllvm prog-llvm.hs
[1 of 2] Compiling Main             ( prog-llvm.hs, prog-llvm.o )
[2 of 2] Linking prog-llvm [Objects changed]
```

コンパイルを実行すると次のファイルが出力されます。

- prog-llvm.hi　（Haskellインターフェース・ファイル）
- prog-llvm.o　（オブジェクトファイル）
- prog-llvm （実行ファイル）

こちらも実行ファイルを実行してエラーが発生しないことは確認してください。

### コード生成器の相違点

それぞれのコード生成器が生成したファイルを比較しましょう。Haskellインターフェース・ファイルの`prog-asm.hi`と`prog-llvm.hi`はバイナリ形式ですがファイルサイズは同じです。そこでファイルの中身を比較してみましょう。

```shell-session:hiファイルの比較
$ cmp prog-asm.hi prog-llvm.hi
(何も表示されません。つまりファイルの中身は同じです。)
```

Haskellインターフェース・ファイルは全く同じです。
ちなみに、Haskellインターフェース・ファイルは`ghc --show-iface {hiファイル}`を使うと、テキスト形式で人間可読(っぽいよう)な情報が、標準出力に表示されます。
<details><summary>Haskellインターフェース・ファイルの中身（テキスト形式の情報）</summary>

```shell-session: Haskellインターフェース・ファイル
$ ghc --show-iface prog-llvm.hi
Magic: Wanted 33214052,
       got    33214052
Version: Wanted 9048,
         got    9048
Way: Wanted [],
     got    []
interface Main 9048
  interface hash: e35217ed5c2468129858b883d7596aa5
  ABI hash: e42dfa0006b918e51cb68a342913c29a
  export-list hash: fcc38c538e2dfda1d4f81d0ac9a2f020
  orphan hash: 693e9af84d3dfcc71e640e005bdc5e2e
  flag hash: fefed202e7fe1438e8079f49e3a92182
  opt_hash: c41405d534b8a8266199837271eab888
  hpc_hash: 93b885adfe0da089cdf634904fd59f71
  plugin_hash: ad164012d6b1e14942349d58b1132007
  src_hash: 5dd185c3e68567da422a11bda454ebe7
  sig of: Nothing
  used TH splices: False
  where
exports:
  main
direct module dependencies:
boot module dependencies:
direct package dependencies: base-4.17.2.1
plugin package dependencies:
orphans: GHC.Base GHC.Float GHC.Prim.Ext
family instance modules: Control.Applicative Control.Arrow
                         Data.Functor.Const Data.Functor.Identity Data.Monoid
                         Data.Semigroup.Internal Data.Type.Ord GHC.Generics GHC.IO.Exception
                         GHC.RTS.Flags
import  -/  GHC.Base d99377a12eb91587b4e402ffda48a3fd
import  -/  Prelude aba104de45cee1558a328eb4c1b53045
bf4c3b4119510c9aa6fc9e7ca30acb27
  $trModule :: GHC.Types.Module
  []
22fb446ca82df109d16c99ca2cd3ae25
  main :: GHC.Types.IO ()
  []
trusted: none
require own pkg trusted: False
docs:
  Nothing
extensible fields:

```
</details>

一方でオブジェクトファイルはファイルサイズが異なります。確認のため、`nm`コマンドでそれぞれのオブジェクトファイルの中身を軽く覗いてみましょう。

```shell:prog-asm.oの中身
% nm -n prog-asm.o
                 U _base_GHCziBase_pure_info
                 U _base_GHCziBase_zdfApplicativeIO_closure
                 U _base_GHCziTopHandler_runMainIO_closure
                 U _ghczmprim_GHCziTuple_Z0T_closure
                 U _ghczmprim_GHCziTypes_Module_con_info
                 U _ghczmprim_GHCziTypes_TrNameS_con_info
                 U _newCAF
                 U _stg_SRT_1_info
                 U _stg_SRT_2_info
                 U _stg_ap_p_fast
                 U _stg_ap_p_info
                 U _stg_bh_upd_frame_info
0000000000000000 t ltmp0
0000000000000018 T _Main_main_info
00000000000000b8 T _ZCMain_main_info
0000000000000130 s _LrDT_bytes
0000000000000130 s ltmp1
0000000000000138 s _LrDR_bytes
0000000000000140 d _LuGB_srt
0000000000000140 d ltmp2
0000000000000158 D _Main_main_closure
0000000000000178 d _LuGV_srt
0000000000000198 D _ZCMain_main_closure
00000000000001b8 d _LrDS_closure
00000000000001c8 d _LrDU_closure
00000000000001d8 D _Main_zdtrModule_closure
```

```shell:prog-llvm.oの中身
% nm -n prog-llvm.o
                 U _base_GHCziBase_pure_info
                 U _base_GHCziBase_zdfApplicativeIO_closure
                 U _base_GHCziTopHandler_runMainIO_closure
                 U _ghczmprim_GHCziTuple_Z0T_closure
                 U _ghczmprim_GHCziTypes_Module_con_info
                 U _ghczmprim_GHCziTypes_TrNameS_con_info
                 U _newCAF
                 U _stg_SRT_1_info
                 U _stg_SRT_2_info
                 U _stg_ap_p_fast
                 U _stg_ap_p_info
                 U _stg_bh_upd_frame_info
0000000000000000 t ltmp0
0000000000000000 t ltmp00
0000000000000018 T _Main_main_info
0000000000000018 T _Main_main_info$def
0000000000000088 t ltmp1
00000000000000a0 T _ZCMain_main_info
00000000000000a0 T _ZCMain_main_info$def
00000000000000fc s _rDT_bytes$def
00000000000000fc s ltmp2
0000000000000101 s _rDR_bytes$def
0000000000000110 d __uGz_srt$def
0000000000000110 d ltmp3
0000000000000130 D _Main_main_closure
0000000000000130 d _Main_main_closure$def
0000000000000150 d __uHA_srt$def
0000000000000170 D _ZCMain_main_closure
0000000000000190 d _rDS_closure$def
00000000000001a0 d _rDU_closure$def
00000000000001b0 D _Main_zdtrModule_closure
```

シンボルが多少異なりますが、ほぼ同じように見えます。処理が同じになるので当然かもしれません。

簡単なソースコードで試してみましたので、大きな相違点は確認できませんでしたが、バックエンドを選択的に変更することができました。

## 終わりに
コンパイラとしてGHCの仕組みに踏み込んでみるのも面白そうです。
本稿では、HaskellのコンパイラもLLVMのフレームワークに似て、“フロントエンド”と“バックエンド”に機能分割されて動作することが特徴であることを紹介させて頂きました。