🐉

GHCのLLVMバックエンドの使い方

2025/01/06に公開

この記事はHaskellの環境構築2023の一部として記載した内容を単体の記事として独立させたものです。

English version: How to use GHC's LLVM backend


GHCはネイティブコードを生成できるコンパイラーですが、ネイティブコードの生成方法としてはGHC自身のNCG (Native Code Generator) バックエンドと、LLVMを使うLLVMバックエンド、それからC言語のソースを経由するvia Cバックエンドの3種類を持っています。

普段、x86やAArch64向けのコードを生成する際はNCGバックエンドが利用されますが、何らかの事情でLLVMバックエンドを使いたいこともあるかと思います。この記事では、GHCでLLVMバックエンドを使えるようにする方法を解説します。

NCGバックエンドと比較した、LLVMバックエンドの特徴は以下になります:

  • 多くのアーキテクチャーに対応している(執筆時点ではARM/AArch64/LoongArch/RISC-V/System Z/x86/x86-64)。
  • SIMDプリミティブに対応している(NCGはGHC 9.12からx86-64向けで一部のSIMDプリミティブが使えるようになったが、まだ発展途上)。
  • LLVMの最適化を利用できる。

ターゲットによっては、LLVMバックエンドの利用が必須であることもあります。特に、GHC <= 9.0でAArch64向けのコードを生成するにはLLVMバックエンドが必要となります。また、GHC <= 9.10でRISC-V向けのコードを生成する場合もLLVMバックエンドが必要となります。

なお、「LLVMのインストール」と「GHCにLLVMの位置を教える」はWindowsには当てはまらないので、Windowsの場合は「Windowsの場合」に飛んでください。

LLVMのインストール

一般に、(Windows版を除いて)LLVMはGHCには同梱されておらず、システムのパッケージマネージャー等でGHCとは別にLLVMをインストールする必要があります。

LLVMにはバージョンによって変更が加えられていくので、特定のGHCには特定のバージョンのLLVMが必要です。対応関係をまとめた表は次のようになります:

GHC LLVM
GHC 8.10.7 LLVM >= 9 && <= 12
GHC 9.0.2 LLVM >= 9 && <= 12
GHC 9.2.8 LLVM >= 9 && <= 12
GHC 9.4.8 LLVM >= 10 && <= 13(ただし、-mavx を使う場合はLLVM <= 12が必要)
GHC 9.6 LLVM >= 11 && <= 15(ただし、-mavx を使う場合はLLVM <= 12が必要)
GHC 9.8 LLVM >= 11 && <= 15(ただし、-mavx を使う場合はLLVM <= 12が必要)
GHC 9.10 LLVM >= 13 && <= 15
GHC 9.12 LLVM >= 13 && <= 19

(注:GHC 9.8以下で -mavx を使う場合はGHC #23870という問題があるので、LLVM 12を使ってください。)

ここではGHC 9.12.1 + LLVM 15の組み合わせをインストールしてみます。

Ubuntuなどのaptを使うシステムでは、

$ sudo apt install llvm-15 clang-15

を実行すれば良いでしょう。

macOSでHomebrewを使っている場合は

$ brew install llvm@15

を実行すれば良いでしょう。

macOSでMacPortsを使っている場合は

$ sudo port install llvm-15 clang-15

を実行すれば良いでしょう。

LLVMが提供するライブラリーや各種コマンド群のうち、GHCに必要なのは最適化器である opt コマンド、アセンブリーを出力する llc コマンド、それからGHC 9.10以降ではアセンブラーの役割を果たす clang コマンドです。これらがどこに配置されたか確認しましょう。

Ubuntuの場合は、コマンド名にサフィックス -15 がつきました:

$ which opt-15 llc-15 clang-15
/usr/bin/opt-15
/usr/bin/llc-15
/usr/bin/clang-15

あるいは、/usr/lib/llvm-15/bin 以下にサフィックスのつかないコマンドが配置されています:

$ ls /usr/lib/llvm-15/bin/{opt,llc,clang}
/usr/lib/llvm-15/bin/clang
/usr/lib/llvm-15/bin/llc
/usr/lib/llvm-15/bin/opt

Homebrewの場合は、$(brew --prefix llvm@15)/bin 以下に各種コマンドが配置されました:

$ ls $(brew --prefix llvm@15)/bin/{opt,llc,clang}
/opt/homebrew/opt/llvm@15/bin/clang
/opt/homebrew/opt/llvm@15/bin/llc
/opt/homebrew/opt/llvm@15/bin/opt

MacPortsの場合は、コマンド名にサフィックス -mp-15 がつきました:

$ which opt-mp-15 llc-mp-15 clang-mp-15
/opt/local/bin/opt-mp-15
/opt/local/bin/llc-mp-15
/opt/local/bin/clang-mp-15

あるいは、/opt/local/libexec/llvm-15/bin にサフィックスのつかないコマンドが配置されています:

$ ls /opt/local/libexec/llvm-15/bin/{opt,llc,clang}
/opt/local/libexec/llvm-15/bin/clang
/opt/local/libexec/llvm-15/bin/llc
/opt/local/libexec/llvm-15/bin/opt

サフィックスのつかないコマンドのあるディレクトリーにPATHを通しておくとGHCからも見つけやすくなって便利ですが、異なるGHCは異なるバージョンのLLVMを要求することがあるため、複数のLLVMを共存させるためにはサフィックスのつかないコマンドにPATHを通すことはお勧めしません。

GHCにLLVMの位置を教える

GHCのLLVMバックエンドを使うには、GHCにLLVMのコマンドの位置を教える必要があります。方法としては、GHCの実行時に毎回教える方法と、GHCのインストール時に指定する方法があります。

毎回指定する方法

毎回教える場合は、-pgmloopt コマンドの位置を、-pgmlcllc コマンドの位置を、GHC 9.10以降では -pgmlasclang の位置を教えます。例えば、HomebrewでインストールしたLLVMをGHC 9.12.1から使う場合は次のようになるでしょう:

$ ghc-9.12.1 -fllvm -pgmlo $(brew --prefix llvm@15)/bin/opt \
    -pgmlc $(brew --prefix llvm@15)/bin/llc \
    -pgmlas $(brew --prefix llvm@15)/bin/clang Main.hs

GHCのインストール時に指定する方法

GHCのインストール時にLLVMの位置を教えておけば、毎回 -pgmlo 等を指定する必要がなくなります。

まず、LLVMのツール群にPATHが通っており、コマンド名が

opt-15 opt-15.0 opt15 opt
llc-15 llc-15.0 llc15 llc
clang-15 clang-15.0 clang15 clang

の形をしていれば、GHCの configure コマンドによって自動的にLLVMのコマンド群が発見され、名前が記録されます。

GHCのインストール時点でLLVMが入っていなかった場合は、LLVMのインストール後に改めてGHCをセットアップする必要があります。GHCupであれば ghcup install ghc 9.12.1 --force という風に、Stackであれば stack setup 9.12.1 --reinstall とすれば良いでしょう。

そうでない場合、つまりHomebrewやMacPortsを使っており、PATHが通っていなかったりサフィックスが変則的な場合は、GHCの configure コマンドに手動で opt / llc / clang の位置を教えてやる必要があります。具体的には、GHCのインストール時に OPT, LLC, LLVMAS の各環境変数を指定します。

GHCupの場合は、次のような手順になります:

$ # Homebrewを使う場合
$ env OPT=$(brew --prefix llvm@15)/bin/opt \
      LLC=$(brew --prefix llvm@15)/bin/llc \
      LLVMAS=$(brew --prefix llvm@15)/bin/clang \
      ghcup install ghc 9.12.1 --force

$ # MacPortsを使う場合
$ env OPT=opt-mp-15 LLC=llc-mp-15 LLVMAS=clang-mp-15 \
      ghcup install ghc 9.12.1 --force

Stackを使ってGHCをインストールした場合は、次のような手順になります:

$ # Homebrewを使う場合
$ env OPT=$(brew --prefix llvm@15)/bin/opt \
      LLC=$(brew --prefix llvm@15)/bin/llc \
      LLVMAS=$(brew --prefix llvm@15)/bin/clang \
      stack setup 9.12.1 --reinstall

$ # MacPortsを使う場合
$ env OPT=opt-mp-15 LLC=llc-mp-15 LLVMAS=clang-mp-15 \
      stack setup 9.12.1 --reinstall

tarballから直接インストールする場合は、次のような手順になります:

$ curl -LO https://downloads.haskell.org/~ghc/9.12.1/ghc-9.12.1-aarch64-apple-darwin.tar.xz
$ tar xf ghc-9.12.1-aarch64-apple-darwin.tar.xz
$ cd ghc-9.12.1-aarch64-apple-darwin

$ # Homebrewを使う場合
$ env OPT=$(brew --prefix llvm@15)/bin/opt \
      LLC=$(brew --prefix llvm@15)/bin/llc \
      LLVMAS=$(brew --prefix llvm@15)/bin/clang \
      ./configure --prefix=/opt/ghc-9.12.1

$ # MacPortsを使う場合
$ env OPT=opt-mp-15 LLC=llc-mp-15 LLVMAS=clang-mp-15 \
      ./configure --prefix=/opt/ghc-9.12.1

$ sudo make install

GHCのセットアップ時に検出あるいは指定された opt, llc, clang の名前は、lib/settings というファイルに記録されます。見てみましょう:

$ # GHCupの場合
$ grep LLVM ~/.ghcup/ghc/9.12.1/lib/ghc-9.12.1/lib/settings
,("LLVM target", "arm64-apple-darwin")
,("LLVM llc command", "/opt/homebrew/opt/llvm@15/bin/llc")
,("LLVM opt command", "/opt/homebrew/opt/llvm@15/bin/opt")
,("LLVM llvm-as command", "/opt/homebrew/opt/llvm@15/bin/clang")

$ # Stackの場合
$ grep LLVM ~/.stack/programs/aarch64-osx/ghc-9.12.1/lib/ghc-9.12.1/lib/settings
,("LLVM target", "arm64-apple-darwin")
,("LLVM llc command", "/opt/homebrew/opt/llvm@15/bin/llc")
,("LLVM opt command", "/opt/homebrew/opt/llvm@15/bin/opt")
,("LLVM llvm-as command", "/opt/homebrew/opt/llvm@15/bin/clang")

注意して欲しいのは、セットアップ時に一時的に PATH=$(brew --prefix llvm@15)/bin:$PATH という風に opt 等のコマンドへのパスを通した場合は、lib/settings には(コマンドへのフルパスではなく)opt という名前しか記録されないことです。したがって、PATHを通すよりは、OPT 等の環境変数を使って指定するべきです。

Windowsの場合

Windowsでは、GHC 9.4以降でGHC自身にLLVMが付属するようになりました。そして、GHC 9.12以降ではGHC自身に付属するLLVMが自動で利用できるようになります。

一方、GHC <= 9.10ではLLVMバックエンドで浮動小数点数を使おうとした際にリンクエラーが発生するので実質使い物になりません。詳しくはGHC #22487を参照してください。

LLVMバックエンドを使う

うまくセットアップできていれば、GHCに -fllvm オプションを渡すことでLLVMバックエンドを利用できます:

$ ghc -fllvm hello.hs

もちろん、LLVMのコマンドを「毎回指定する方法」を選んだ場合は、-pgmlo などのオプションが必要です。

GitHub ActionsでLLVMバックエンドを使う

GitHub ActionsでLLVMバックエンドを有効にしたいこともあるかと思います。ここまでの知識を活用すれば、GitHub ActionsでLLVMバックエンドを有効化することは難しくないと思います。実際に、GitHub ActionsでLLVMバックエンドを有効にする例をghc-llvm-backend-test/.github/workflows/build.yamlに用意しました。

注意点としては、runner imageがすでにGHCを含んでいる場合があることです。そういう場合は、GHCupに --force オプションを渡す必要があります。現状のhaskell-actions/setupでは --force オプションを渡すことはできないので、GHCupを直接インストールするか、haskell/ghcup-setupを使うと良いかもしれません。

LLVMバックエンドに関する既知の問題

LLVMバックエンドに関する既知の問題をいくつか挙げておきます。すでに言及したものもあります。

まとめ

GHCのLLVMバックエンドをガンガン使っていきましょう。

Discussion