GHCへの私の貢献2024

この記事はHaskell Advent Calendar 2024の24日目の記事です。


私はここ数年、Haskellの主要な処理系であるGHCに趣味で貢献しています。この記事では、今年(2024年)行なった貢献を紹介します。バグ報告のみ(修正は他の人)のものも含みますが、その場合はその旨を書いています。

同様の記事を去年(2023年)も書きました:GHCへの私の貢献2023

GHC 9.12に入ったものは「GHC 9.12の新機能」でも触れています。

こういう記事を書く目的は、一つにはOSSへの貢献の対価として名誉が欲しいというものがあります。そのほかに、自分用の備忘録という目的もあります(あのバグはどのバージョンで修正されたんだっけ、ということがたまにあります)。あとは、単純にGHCの開発状況やホットな話題を皆さんに知って欲しいというのもあります(私の趣味全開なので、非常に偏った視点ではありますが)。

GHCへの貢献方法は去年の記事を読んでください。

貢献内容

FMAプリミティブ周り(1月〜2月)

2023年10月にリリースされたGHC 9.8.1ではFMA (fused multiply-add) のプリミティブ関数が追加されました(GHC 9.8の新機能)。しかし、リリースされたばかりの状態では色々バグが見つかりました。本当はalpha/betaとかRCの段階で気づいて報告しろよと言われたらそうなのですが。

一つ目:#24311: GHC should produce FMA instructions even if `-mfma` is not set on AArch64 · Issues · Glasgow Haskell Compiler / GHC · GitLab(1月)

x86と違ってAArch64には最初からFMA命令があるにもかかわらず、-mfma オプションが設定されないとFMA命令が使われないようになっていたので、指摘しました(実装は他の人です)。多分PowerPCにも同様のことが言えますが、私はPowerPCの環境を持っていないので触れていません(QEMUとかを使えばテストできるかもしれませんが)。

GHC 9.8.2/9.10.1に反映されています。

二つ目:#24496: x86_64: FMA primop generates wrong result · Issues · Glasgow Haskell Compiler / GHC · GitLab(2月)

x86でのFMAプリミティブが出力するコードがバグっていたので報告しました(修正は他の人です)。

GHC 9.8.3/9.10.1で修正されています。

FMAプリミティブについては去年も問題を報告していました。難産だったと言えそうです。

なお、プリミティブのままでは引数の型が Float# とかで使いづらいです。FMAを使いたい場合は、私が作っているfp-ieeeというパッケージを使うと普通の FloatDouble 型で使えます。古いGHCではlibcの fma 関数を呼び出したり、自前で実装したりしているので、古いGHCでFMAを使いたい方も安心です。

プリプロセスされるアセンブリソースのインクルードパス(5月〜6月)

GHCが受け付ける入力ファイルのうち、数種類はCプリプロセッサーにより前処理されます。具体的には、CPP拡張を有効にしたHaskellソース、.c/.cppのようなC系のソースファイル、.cmmファイル、そして拡張子が大文字のアセンブリソース.Sです。

Cプリプロセッサーによる処理の例は、#include です。このうち、アセンブリソースだけが他と異なるインクルードパスで処理されていて、#include <ghcconfig.h>のようなことができなくなっていました。これを解消しました。

#include <ghcconfig.h>できると何が嬉しいのかというと、Cのシンボルがアンダースコアから始まるかどうかを教えてくれるマクロLEADING_UNDERSCOREが利用できるようになります。私が作っているrounded-hwパッケージでこれを使いたい場面があったので、自分のための改良です。

この変更はGHC 9.12.1に入っています。

拡張命令フラグの間の含意(6月、10月)

x86の命令セット拡張は、SSE→SSE2→SSE3→SSSE3→SSE4.1→SSE4.2→AVX→AVX2→AVX-512というふうに進化してきました。ここに挙げたものは包含関係があり、新しい命令セット拡張を実装したハードウェアでは以前の命令セット拡張も利用できます。例えば、AVXが使えるハードウェアではSSE4.2が使えるはずだ、という具合です。

GHCには、これらのオプションのうちどれをコード生成で使えるかをコンパイラーに教えるフラグとして、-msse4.2-mavx といったものがあります。包含関係を考えると、-mavx を指定してコンパイルする際にはSSE4.2の機能も使えて良いはずですが、これまでのGHCではそうはなっていませんでした。

そこで、この辺を改善する変更を私が実装しました。

ただ、12月現在はCIの問題に引っかかっており、保留中です。(私が解決しないと放置されたままになるやつか?)

MacでLLVMのバージョン判定がうまくいかない(6月)

GHC 9.10でLLVMのバージョン判定の正規表現が変更され、変更後のものがGNU拡張を使っていたのでMac(や、おそらくBSD系)でバージョン判定に失敗していました。LLVMをインストールしていても「利用できない」と判定されてしまうのです。

私は報告と原因究明を行いました(修正は他の人)。

この問題はGHC 9.12.1で修正されました。イシューの方にbackport needed: 9.10タグが付いているので、9.10.2にもバックポートされるはずです。GHC 9.8以前には影響しません。

WindowsでLLVMバックエンドを使えるようにする(8月〜9月)

Haskellの環境構築2023」に「WindowsでLLVMの opt.exellc.exe を入手するのは厄介だ」というようなことを書きました(記事の最新版からは削除されています)。当時のLLVM公式のバイナリーには opt.exellc.exe が含まれていなかったのです。しかし、GHC 9.4以降は実はGHCにMSYS2由来の opt.exellc.exe が付属します。これを使えば良いのでは?と思ってやってみたら、謎のリンクエラーに阻まれました(2022年ごろ)。

この件はしばらく放置されていましたが、最近、なんやかんやでそのリンクエラーが修正されました(調査は私がしました)。

リンクエラーが修正されたので、私の方でGHC付属の opt.exellc.exe を活用するパッチを書いて、マージされました。

いずれもGHC 9.12.1に入りました。つまり、GHC 9.12.1ではWindowsであれば何もしなくても -fllvm が使える状態になります。

プリミティブ文字列リテラルと文字列リテラルの脱糖の仕様のドキュメント化(9月)

Haskell標準の文字列型 String は文字 Char のリストとして定義されていますが、文字列リテラル "abc" はリストリテラル ['a','b','c'] とは異なる脱糖のされ方をします。また、MagicHash拡張を有効にすると Addr# 型の文字列リテラル "abc"# が使えるようになります。この辺の話は前に「Haskellの文字列リテラルはGHCでどのようにコンパイルされるか」(2020年)に書きました。

しかし、GHCのユーザーガイド(User’s Guide)はその辺の記述が薄い状態でした。

私は英語がネイティブではないので、調べた内容をイシューに書いておけば英語が堪能な誰かがユーザーガイドを更新してくれるかもしれないと期待していました。しかし、そういう誰かは4年間一人も現れなかったので、私が重い腰を上げて自分でユーザーガイドを更新することにしました。私が書いた英語が不自然にならないか心配だったので、ChatGPT等にチェックさせました。いい時代になったものです。

この変更はGHC 9.12.1に反映されました。

floatRange関数のドキュメントの改善(9月)

floatRange 関数は、浮動小数点型の指数部の範囲を返す関数です。これが意図しない値を返すというイシューが立っていました。

ただ、これは指数部の流儀の違いによるもので、ドキュメントを明確にすれば(関数の動作を変えることなく)解決するように思われました。既存のコードが壊れると嫌なので挙動は変えたくありません。そこで、ドキュメントを改善するパッチを投げました。

GHC 9.12には今のところバックポートされていないので、このままいけばGHC 9.14に反映されます。

LLVMでのFloat型と定数畳み込みの問題の調査(10月)

GHCのイシューからLLVM関連のものを漁っていたら、LLVMバックエンドで浮動小数点周りの定数畳み込み(?)の動作が違う、というものがありました。失敗しているテストケースは元々私が追加したものでした。

これの原因究明と、修正を行いました。

GHC 9.12には今のところバックポートされていないので、修正が反映されるのはGHC 9.14になりそうです。

浮動小数点数のmin/maxについての問題提起(10月、11月)

GHC 9.12で minFloat#/maxFloat#/minDouble#/maxDouble# およびそのSIMD版のプリミティブが追加されました。しかし、「浮動小数点数の min / max」に書いた通り、浮動小数点数のmin/max演算にはいくつか流儀があります。そして、追加されたプリミティブは「ターゲットにあるmin/max命令(あるいは関数)を深く考えずに利用する」という風になっていたので、環境ごとに符号付きの0やNaNに関する動作が違うということになってしまいました(環境というよりはバックエンド依存なので、同じアーキテクチャーでも -fllvm の有無によって変わったりします)。

というわけで、問題提起しました。どういう風に解決するかはまだわかりません。「仕様が一貫したプリミティブを追加する」という方向性であれば私が実装することになるのかなあと思います。つまり私のやる気次第です。

11月にGHCのSIMDプリミティブのドキュメントを改善するパッチを投げたときに、ついでに「min/max系プリミティブの挙動が違うかもしれない」ことを明記するようにしました。

LLVMと -msse4.2(10月)

LLVMバックエンドで -msse4.2 を指定したところ、LLVMへの +sse42 オプションが認識されないというような警告が出ました。これはLLVMのオプション名が +sse4.2 に変わっていたためです。

これを修正しても、popcnt命令が有効になりませんでした。実際のところ、CPUの機能(cpuid)的にはSSE4.2とpopcntは別個なのですが、GCCやClang等のメジャーなコンパイラーは -msse4.2 でpopcnt命令も有効にします。そこで、GHCに -msse4.2 が与えられた時はLLVMに +popcnt オプションも渡すように提案しました。

GHC 9.8.3/9.12.1に反映されています。backport needed: 9.10になっているので、9.10.2が出るなら反映されるはずです。

MultilineStrings拡張とCRLF(10月)

GHC 9.12の新機能を試すか〜〜と思って試していたら、改行がCRLF(Windows流)のファイルでMultilineStrings拡張を使った時の挙動が意外なものでした。これは提案者・実装者の見落としだと思われたので、報告しました。GHC 9.12.1-rc1で修正されました。

GHC 9.12のリリースノートでのRISC-V NCGの言及(10月)

GHC 9.12のアルファ版のリリースノートを見ていたら、RISC-V NCGの言及が抜けていることに気づきました。AArch64 NCGが追加された時や、SPARC NCGが削除された時はリリースノートに記載があったので、RISC-V NCGへの言及を載せた方が良いと思い、イシューを立てました。正式リリースでは直っています。

LLVMバックエンドを使っていてもマクロ __GLASGOW_HASKELL_LLVM__ が定義されない(12月)

LLVMバックエンドを使っているか検出するマクロが動いていませんでした。LLVMの llc コマンドのバージョン出力(llc -version)の形式が変わったことの影響のようです。

昨日見つけた、採れたてほやほやの新鮮なバグです。本記事公開時点ではまだ「needs triage」がついています。

SIMDプリミティブ周り(11月、12月)

去年「Haskell/GHCのSIMDについて考える」という記事を書きました。最近「HaskellでEDSLを作る:SIMD編」という記事も書きました。私の性格として「CPUの機能を活用できると……楽しい!」という気持ちがあるので、HaskellでもSIMDを活用したいわけです。ちなみに、私はSIMDで高速化したい具体的な計算対象を抱えているわけではありません(なんじゃそりゃ)。

しかし、「GHCへの私の貢献2023」に書いたように、私の力ではレジスター割り付け/スタックへの退避の問題を解決できず、NCGでのSIMD対応は棚上げとなっていました。

それが、今年になって @sheaf さん(Haskellコンサル集団であるWell-Typedの人)がレジスター割り付けの問題に取り組んで、解決しました。つまり、GHC 9.12ではx86 NCGバックエンドでもSIMDが一部使えるようになりました。

とはいえ、その時点で使えるようになったのは FloatX4#DoubleX2# くらい(Int64X2# も一部使えたかも?)で、それも packFloatX4# などのコード生成に -msse4 が必要(SSE4.1の命令を使う)でした。GHCがデフォルトで仮定するSSE命令はSSE2なので、デフォルトでは packFloatX4# のような基本的な関数を使えないということになります。

これは不便なので、SSE2までの機能だけで packFloatX4# を実装するパッチを私が書きました(GHC 9.12.1にバックポート済み)。ちなみに、LLVMバックエンドでは以前からSSE2だけで packFloatX4# が使えていました。

その際、 packFloatX4# のテストを書いたらコード生成器のバグを踏んだので報告しました:

SIMDプリミティブのドキュメントを改善しました:

Int64X2以外の整数ベクトルのpack/insert/broadcast/unpackの実装に着手しました(執筆時点で未マージ):

SIMDベクトルをMutableByteArrayに書き込むプリミティブがsegvを起こしていたので報告しました(レジスター割り付けの問題なので、私は報告のみ):

LLVMバックエンドがSIMD絡みで不正なLLVM IRを出力することがあったのを修正しました(残念ながらGHC 9.12.1の正式リリースには間に合いませんでした):

HaskellでSIMDを使い倒すという野望の実現に向けて、今後も気が向いた時にSIMDプリミティブの改善を進めていきたいと思っています。今後のロードマップはこんな感じかと思います:

  • 整数ベクトルの四則演算の対応
  • AArch64 NCGでのSIMD対応
  • プリミティブの追加(abs, sqrt, ビット演算あたり)
  • AVX/AVX-512対応(vzeroupper 周りが面倒そう)

雑多な話

同時にいくつのイシューを回せるか

OSSでの協業では、人によって活動する時間帯が異なったりするので、コミュニケーションに時間がかかりがちです。また、CIを回すのにも時間がかかります。

時間がかかるのであれば並列化して複数のイシューを同時に進めればいいのですが、あまり抱えすぎると私のコミュニケーション容量のような資源が枯渇してしまいます。私の場合、同時に3個以上のイシューを抱えるのはきついようです。

本当はここに書いていないGHCへの貢献ネタがいくつかあるのですが、私のキャパシティーの問題があるので、気が向いた時に小出しにしています。(イシューを思いついた時点で立てておくべきなのかもしれませんが)

GHCの開発体制に足りないもの

GHCを見ていると、メジャーバージョンが変わった最初のリリース(x.y.1)にはバグが多いように思います。GHC 9.0.1には fromIntegral に重大なバグがあったというのは「GHCに初めてコントリビュートした/最近のGHC動向」に書きました。GHC 9.8.1で追加されたFMAプリミティブに色々バグがあったのは上の方に書きました(まあFMAプリミティブの件は新機能であり使う人は多くはないと思われるので、そこまで深刻な問題ではないかもしれません)。

こういうのは品質保証(QA)と言うんでしょうか?GHCには品質保証が足りないのでは、と思ってしまいます。

一方、新しいバージョンが安定しないからといって枯れたバージョンを使おうとすると、そのメジャーバージョンは保守が終わっていたりします。例えば、執筆時点ではGHCupのrecommendedはGHC 9.4.8となっていますが、GHC 9.4系のリリースはそれ(9.4.8)が最後です。

他の言語処理系はどうしているんでしょうか?何か手順が違うのでしょうか?それとも、GHCへ新機能が追加されるペースが早すぎるのでしょうか?あるいは、メジャーバージョンの保守の期間が短すぎるのでしょうか?

(気になって調べたところ、Pythonは最初のアルファ版が出てから正式版までに1年間かけるようです:PEP 602 – Annual Release Cycle for Python | peps.python.org 言語の進化に対するスピード感が違うのでそのまま真似ることはできないと思いますが。)

単純に、Haskellには人と金が足りないということなのかもしれません。GHCの開発に参加している(GHC開発者を雇っている)企業はいくつかありますが、それでは不足なのか。GHCだけでなく、周辺ツールも全体的にマンパワーがカツカツな印象があります。

とはいえ、私の力ではGHCの品質を底上げしたり、Haskellのエコシステムに時間と金をじゃぶじゃぶ注ぎ込むことはできません。じゃあどうするか、と考えて思いつくことを挙げてみます:

  • Haskellお役立ち記事をたくさん書いて、日本でのHaskellユーザーを増やす(普及活動)。できればHaskellを採用する企業が増えてほしい。Haskellを採用する企業が増えたら、Haskellコンサルみたいなところの経営が潤ってマンパワーが増えるかもしれない。(夢を見過ぎか?)
  • GHCの初期リリースが安定しない問題については、アルファ版の段階で「新機能紹介記事」を書いて、できるだけ多くの人に試してもらう。バグが発覚すれば正式リリースまでに修正できるかもしれない。

まあ、私が今やっているようなことですね。書いていて思ったのは、これらの活動を英語でもやった方がいいのでは?ということですが、毎回英語で記事を書くのは私には荷が重いです。

(とはいえ、「LLVMバックエンドを誰も使っていない感」を考慮すると、「LLVMバックエンドを有効にする方法」についての英語の記事を書くべきかもしれません(日本語の情報は「Haskellの環境構築2023」にある)。)

最後に

私のGHCへの貢献は、(仕事ではなく)ボランティアでやっています。そんな私を金銭的に支援したい方には、GitHub Sponsorsという手があります。現在、1名の方に支援していただいています(ありがとうございます)。あるいは、「だめぽラボ」の同人誌を購入するという手もあります。

本記事の執筆に際してこのブログの過去記事を読み返していると、GHCに初めてコントリビュートした/最近のGHC動向(2021年3月)という記事を見つけました。GHCのGitLabを眺めるのが日常になった自分からすると、4年前の自分がまだGHCに貢献していなかったというのはなかなか信じられません。

Spread the love