Haskellでインラインアセンブリを模倣する低レベル手法
GHCにはインラインアセンブリ機構が存在しない。しかし、64ビット乗算の上位取得など特殊命令をHaskellから呼び出す方法を模索する試みが注目を集めている。
Haskell/GHCにはC言語のようなインラインアセンブリやintrinsicが存在しない。しかし、CPUの特殊命令(SIMD、暗号・ハッシュ向け命令など)をHaskellから利用したい場面は少なくない。2026年6月30日に公開された技術ブログ「Low-level Haskell: The cursed way to emulate inline assembly in Haskell/GHC」は、この制約を乗り越えるための複数の手法を詳細に比較し、Haskellコミュニティに一石を投じている。
本稿では、同記事の内容を基に、Haskellで低レベルCPU命令を呼び出す方法と、その実用性について考察する。
Haskellが直面する低レベル制御の壁
現代のCPUは、SIMD、ハッシュ、暗号処理など特定用途に特化した多数の命令を備えている。C/C++ではインラインアセンブリやintrinsicを用いてこれらの命令を直接利用できるが、Haskellは高級言語としての抽象性を重視しており、そのような低レベル機構は標準では提供されていない。
筆者が取り上げた具体例は、64ビット整数の乗算において上位64ビットを取得する操作だ。x86アーキテクチャのmulq命令は、128ビットの積を一度に計算し、上位64ビットを%rdx、下位64ビットを%raxに格納する。しかし、Cの通常の乗算演算子は下位64ビットのみを返す。
C言語では、GCC/Clangが提供する__int128型を用いることで、1行で実装できる。
unsigned __int128 wideningMul(uint64_t a, uint64_t b) {
return (unsigned __int128)a * (unsigned __int128)b;
}
インラインアセンブリを用いれば、より明示的に記述できるが、それでもCはこの問題を比較的容易に解決できる。Haskellではどうか。
GHCのプリミティブが持つ可能性
実はGHCには、timesWord2# :: Word# -> Word# -> (# Word#, Word# #)というプリミティブが存在する。これは64ビット乗算の上位・下位両方をタプルで返す、まさに目的に合致したintrinsicである。このプリミティブはGHCのコード生成フェーズで直接CPU命令に変換されるため、オーバーヘッドが極めて小さい。
しかし問題は、このようなプリミティブが網羅的に用意されているわけではない点だ。キャリーレス乗算(有限体上の多項式乗算)など、特定の暗号処理で有用な命令には対応するプリミティブが存在しない。その場合、開発者は別の手段を選ばざるを得ない。
FFIを介したC関数呼び出しの実情
プリミティブが利用できない命令を呼び出す最も一般的な方法は、FFI(Foreign Function Interface)を用いてC言語のラッパー関数を呼び出すことだ。例えばcryptonite-opensslパッケージのように、CライブラリをラップしてHaskellから利用する手法が既に存在する。
FFIのアプローチには、関数呼び出しのオーバーヘッドが生じるという明確な代償がある。C関数を呼び出すたびに、Haskellランタイムは引数をCの呼び出し規約に変換し、呼び出し後は結果をHaskellのデータ構造に戻す処理が必要となる。
短い処理(1〜2命令のアセンブリブロック)であれば、このオーバーヘッドが処理自体よりも大きくなる可能性がある。記事の筆者は、このオーバーヘッドを定量的に測定した結果をリポジトリで公開しており、マクロベンチマークにおいてFFI経由の手法がGHCプリミティブと比較してどの程度劣るかを示している。具体的な数値は元記事のリポジトリに委ねるが、一般的にFFIのコストは数十ナノ秒単位で発生する。
多値返却を巡るコーディングテクニック
本記事の副題にもある「複数の値を関数から返す方法」は、CとHaskellの言語設計の違いを浮き彫りにする。Cでは構造体を値で返すか、ポインタ経由で書き込むしかない。一方Haskellは、BoxedタプルやUnboxedタプル((# #)構文)をネイティブでサポートしており、この点ではむしろ優れている。
ただし、Haskellのタプルはメモリ上でボックス化されるコストが生じる場合がある。timesWord2#のようなプリミティブがUnboxedタプルを返すのは、このコストを回避するためだ。
低レベル最適化の実用性と限界
今回の検証は、HaskellでもCPUの特殊命令を利用可能であることを示す一方で、現実的なトレードオフを浮き彫りにしている。
- GHCプリミティブが存在する場合:最適な選択肢。Cのintrinsicと同等のパフォーマンスが得られる可能性が高い。
- プリミティブが存在しない場合:FFI経由のCラッパーが次善の策。ただし呼び出しオーバーヘッドが無視できないため、処理が十分に長いか、バッチ処理でオーバーヘッドを償却できる場合に限り実用。
- Cコードの自動生成:
cryptonite-opensslのように、コード生成機構を自前で実装する方法もあるが、メンテナンスコストが高い。
これらの手法は、暗号ライブラリやハッシュ関数など、ボトルネックとなる特定の処理に限定して適用するのが現実的だ。Haskellの強みである高い抽象性を犠牲にしてまで、通常のアプリケーションコードに低レベル最適化を導入する価値は疑問視される。
編集部の見解
短期的には、本記事がきっかけとなりGHCに追加のプリミティブが提案される可能性がある。特にRustやZigなど低レベル制御を重視する言語が台頭する中、Haskellの競争力を維持するためには、暗号処理やSIMD演算向けのintrinsic整備が急務と言える。
長期的視点では、Haskellがシステムプログラミング領域に進出するかどうかの分水嶺となる。純粋関数型の利点(安全性、メモリ管理の自動化など)を活かしつつ、低レベル制御を提供するバランスが問われる。一方で、LinuxのCache Aware Scheduling拡張(Linux Cache Aware Scheduling拡張、MySQL最大360%高速化)のような低レベル最適化手法がOSカーネルに導入される動きとも比較されるべきだろう。
編集部の問いとして、GHCにintrinsicを追加することのコスト対効果は本当に妥当か。Haskellの抽象性を損なわずに低レベル制御を提供する設計は可能か。この問題は、関数型言語の将来を考える上で看過できない論点である。
参考
- Low-level Haskell: The cursed way to emulate inline assembly in Haskell/GHC — 2026-06-30公開
- 筆者のGitHubリポジトリ(ベンチマーク結果含む)— 元記事内で参照
よくある質問
- Haskellでインラインアセンブリを使う標準的な方法はあるか
- GHCにはC言語のようなインラインアセンブリ機構は存在しない。代わりに、GHCのプリミティブ(timesWord2#など)を利用するか、FFI経由でCのラッパー関数を呼び出す方法が一般的だ。特定のケースではコード生成ツールを自作するアプローチも検討される。
- 64ビット乗算の上位64ビットをHaskellで取得するには
- GHCが提供するtimesWord2#プリミティブが最も効率的な手段で、Unboxedタプルで上位・下位を返す。FFIでCの__int128型を利用する方法も可能だが、関数呼び出しのオーバーヘッドが生じる。ベンチマークではプリミティブの優位性が確認されている。
- この手法は暗号処理に実用できるか
- 限定的に実用可能。AES-NIやキャリーレス乗算など特定の命令については、FFI経由のCラッパーが現実的な選択肢となる。ただしレイテンシが重要なリアルタイム処理には不向きで、バッチ処理や低頻度の呼び出しに適している。
コメント