こんにちは|こんばんは。カエルのアイコンで活動しております @kyamaz です。
GHCの環境構築
Haskellの環境構築は、GHCのバージョンやバージョン管理&パッケージ管理の選択(Stackか、GHCup+Cabalか)によって注意が必要です。@mod_poppoさんの次のエントリを参考にして整えてください。
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
コマンドの引数でバックエンドをスイッチすることができます。現時点では、2つのバックエンドをスイッチできます。
ネイティブコード生成器(-fasm)
GHCのデフォルトのバックエンドです。Cmmからアセンブリコードまでコンパイルするネイティブコード生成器です。このネイティブコード生成器は高速なバックエンドであり、通常は優れたパフォーマンスのコードを生成します。また、共有ライブラリのコンパイルをサポートする最も良いコード生成器です。
LLVMコード生成器(-fllvm)
LLVMコンパイラを使って実行可能コードを生成するバックエンドです。ネイティブコード生成器と遜色のない性能のコード生成されます。ベクトル等のパッケージを使用する重めの配列を扱うコードのような場合では、より高速なコードが生成されることもあります。ただし、コンパイル時間が大幅に増加するようなトレードオフがあります。
では、実際にコードジェネレータを換えてコンパイルしてみましょう。サンプルは簡単なコードにしたいので、何も結果を出さない次のコードを試します。
main = pure()
ネイティブコード生成器の場合
$ 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にあり、既知の問題ですので気にしないこととします。)
コンパイルを実行すると次のファイルが出力されます。
- prog-asm.hi (Haskellインターフェース・ファイル)
- prog-asm.o (生成途中のオブジェクトファイル)
- prog-asm (実行ファイル)
実行しても何も起きませんが、実行ファイルを実行してエラーが発生しないで、正しくコンパイルできていることを確認してください。
LLVMコード生成器の場合
$ 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
はバイナリ形式ですがファイルサイズは同じです。そこでファイルの中身を比較してみましょう。
$ cmp prog-asm.hi prog-llvm.hi
(何も表示されません。つまりファイルの中身は同じです。)
Haskellインターフェース・ファイルは全く同じです。
ちなみに、Haskellインターフェース・ファイルはghc --show-iface {hiファイル}
を使うと、テキスト形式で人間可読(っぽいよう)な情報が、標準出力に表示されます。
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:
一方でオブジェクトファイルはファイルサイズが異なります。確認のため、nm
コマンドでそれぞれのオブジェクトファイルの中身を軽く覗いてみましょう。
% 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
% 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のフレームワークに似て、“フロントエンド”と“バックエンド”に機能分割されて動作することが特徴であることを紹介させて頂きました。