2026年6月:改訂しました。
最近のCPUには特定の用途に特化した命令がたくさんあります。例えばSIMD、ハッシュ計算や暗号に役立つもの、その他いろいろです。C言語やC++にはインラインアセンブリーやintrinsics(組み込み関数)があり、そういう命令を活用したコードを書くことができます。
一方、Haskell (GHC) にはそのような仕組みはありません。しかしそれで諦めるのは早い。なんとかしてHaskellからCPUのマイナーな命令を呼び出してみましょう。それも、なるべく効率的に。
まず、Haskellから使えると嬉しいCPU命令をいくつか挙げてみます。
題材:64ビット整数の積の上位と下位
2つの64ビット整数の積を計算し、上位64ビットと下位64ビット(合わせて128ビット)をそれぞれ計算することを考えます。
C言語やHaskellにあるような通常の乗算 (*) :: Word64 -> Word64 -> Word64 では下位64ビットしか計算できません。一方、x86の機械語・アセンブリー言語のレベルでは、乗算の際に上位64ビットも一緒に計算されます。
こういう、「機械語レベルでは容易だが、C言語やHaskellのレベルでは非自明な処理」には、インラインアセンブリーや組み込み関数を使いたいところです。
(実はGHCには timesWord2# :: Word# -> Word# -> (# Word#, Word# #) という組み込み関数があるので、これを使えば一発です。それでもこの題材を選んだのは、「GHCの組み込み関数と比較した場合にどの程度遅くなるか」を検証できるようにするためです。 )
他の題材としては、キャリーなし乗算(有限体を係数とする多項式乗算)もある種の用途では有益でしょう。この記事では詳しくは触れませんが、リポジトリーの方にテスト結果を置いておきます。
C言語では
GCC/Clangには __int128 型があるので、それを使えば一発で計算できます。インラインアセンブリもintrinsicsも必要なかった(ぇ)
unsigned __int128 wideningMul(uint64_t a, uint64_t b)
{
return (unsigned __int128)a * (unsigned __int128)b;
}
あえてインラインアセンブリーで書くとすれば、こんな感じでしょうか:
uint64_t wideningMul_inlasm(uint64_t a, uint64_t b, uint64_t *outHigh)
{
uint64_t lo, hi;
asm("movq %2, %%rax;"
// mulq は %rax とオペランド(この場合は %3)の積を計算し、
// 上位64ビットを %rdx, 下位64ビットを %rax に入れる
"mulq %3;"
"movq %%rax, %0;"
"movq %%rdx, %1;"
: "=r"(lo), "=r"(hi)
: "r"(a), "r"(b)
: "%rax", "%rdx");
*outHigh = hi;
return lo;
}
複数の値をどうやって返すか
さて、この処理では2つの uint64_t を受け取って、128ビット値を返します。標準的なデータ型に直せば、2つの uint64_t です。C言語の構文には多値返却がないので、値の返却方法として次のいずれかを選択することになります:
- 構造体の値返し:
struct uint128 { uint64_t lo, hi; }のような構造体を定義して値で返す。-
unsigned __int128を値で返すのも内部的にはこれに相当します。詳しくはx86_64のABIを確認してください。
-
- ポインターを受け取って渡す:2番目以降の返却値の受け渡しをする場所をポインターとして引数に取る
- 例:先ほど書いた
wideningMul_inlasm関数
- 例:先ほど書いた
前者の例として、C標準の div, ldiv, lldiv 関数は {,l,ll}div_t という構造体を値で返しています。
構造体を値で返すメリットは、ABI次第では、構造体が小さければ値をレジスターに載せたまま返すことができることです。
一方、構造体の値返しのデメリットは、他の言語のC FFIが対応していない場合があることです。実際、現在のGHCのC FFIは構造体を値で受け渡すことに対応していません。
一応、Cの構造体をFFIで値渡し出来るようにしよう、という提案がありますが、動きがありません:
- c structures · Wiki · Glasgow Haskell Compiler / GHC · GitLab
- Support C structures in Haskell FFI (#9700) · Issue · ghc/ghc
C FFIを使う(ポインター使用)
Safe FFI
Haskellにできないことは他の言語の力を借りよう!ということで、HaskellにはFFIがあります。これを使えばC言語で書いた関数を呼び出すことができます。
早速やってみましょう:
#include <stdint.h>
extern uint64_t wideningMul_with_ptr(uint64_t a, uint64_t b, uint64_t *outHigh)
{
unsigned __int128 result = (unsigned __int128)a * (unsigned __int128)b;
*outHigh = (uint64_t)(result >> 64);
return (uint64_t)result;
}
先ほど書いたように、現状のGHCのFFIでは構造体を値渡しできないので、返り値の1つはポインターを経由することにします。
Haskell側のコードはこんな感じです:
foreign import ccall "wideningMul_with_ptr"
c_wideningMul_with_ptr :: Word64 -> Word64 -> Ptr Word64 -> IO Word64
wideningMulWithPtr :: Word64 -> Word64 -> Word128
wideningMulWithPtr !a !b = unsafePerformIO $ do
Marshal.alloca $ \outHigh -> do
lo <- c_wideningMul_with_ptr a b outHigh
hi <- peek outHigh
return $ Word128 hi lo
ポインターを扱うということは、領域を確保したり値を読みだしたりするために IO を行うということです。しかし「64ビット整数の乗算」という処理全体としては純粋だと考えられるので、 unsafePerformIO を使って純粋な関数として書いています1。
(ちなみに、 Word128 型は wide-word パッケージの Data.WideWord.Word128 型を使いました。)
試してみると、
> wideningMulWithPtr 123 456
56088
> 123 * 456
56088
> wideningMulWithPtr (2^63) (2^62) -- CPUの命令を使う
42535295865117307932921825928971026432
> 2^125 -- 多倍長整数で計算する
42535295865117307932921825928971026432
という風に正しく計算できているようです。
C言語のコードを別のファイルに書くのが面倒であれば、
- @tanakh 氏の記事「Haskellにインラインアセンブリを書く」
のように inline-c のようなパッケージを使うのも一つの手段かもしれません。
Unsafe FFI
さて、HaskellのFFIにはsafety levelという概念があります。デフォルトではsafeで、これは「呼び出した外部のコードからHaskellの関数をコールバックしても安全」であることを意味します。
safeの逆はunsafeで、「呼び出した外部のコードからHaskellにコールバックしたら何が起きるかわからんよ」という意味です。
unsafeなFFIはリスクがある分、オーバーヘッドが小さいことが期待できます。
safety levelの指定は、 foreign import 宣言の呼び出し規約の直後に safe または unsafe を書きます:
foreign import ccall unsafe "wideningMul_with_ptr"
c_wideningMul_with_ptr :: Word64 -> Word64 -> Ptr Word64 -> IO Word64
inline-cパッケージを使う場合は、quasiquoter (exp, pure, block) として Language.C.Inline.Unsafe にあるものを使います。
C FFIを使う(2回呼び出す)
先のセクションでは、Cで書いた関数から複数の値を返すためにポインターを経由しました。
しかし、ポインターで値を受け渡しするのは、レジスター渡しに比べて遅い気がします[要出典]。メモリー領域を確保する必要もあります(GHCの alloca 関数はスタックではなくヒープに確保します)。ポインターを使わずに値を受け渡しできれば、それに越したことはありません。
幸い、今回の題材は「64ビット整数の乗算」という、コストが低い演算です。これだったら、上位64ビット、下位64ビットのそれぞれに対して同じ計算をするのもアリではないでしょうか?
// 下位64ビットを計算する
extern uint64_t wideningMul_lo(uint64_t a, uint64_t b)
{
unsigned __int128 result = (unsigned __int128)a * (unsigned __int128)b;
return (uint64_t)result;
}
// 上位64ビットを計算する
extern uint64_t wideningMul_hi(uint64_t a, uint64_t b)
{
unsigned __int128 result = (unsigned __int128)a * (unsigned __int128)b;
return (uint64_t)(result >> 64);
}
foreign import ccall unsafe "wideningMul_lo"
c_wideningMul_lo :: Word64 -> Word64 -> Word64
foreign import ccall unsafe "wideningMul_hi"
c_wideningMul_hi :: Word64 -> Word64 -> Word64
wideningMul2 :: Word64 -> Word64 -> Word128
wideningMul2 !a !b = Word128 (c_wideningMul_hi a b) (c_wideningMul_lo a b)
実際のところ「2回呼び出してレジスター渡しで完結させる」方針が「ポインターで渡す」方針に比べてどうなのかは、後で比較します。
C FFIを使う(SIMDレジスターを使う)
GHCがFFIで扱える型に128ビット整数型はありませんが、128ビット幅のSIMDレジスターは使えます。Word64X2# みたいなやつです。これを使えば、1回の関数呼び出しで128ビットデータをレジスターで受け渡しできます(System V ABIの場合)。
extern __m128i wideningMul_xmm(uint64_t a, uint64_t b)
{
union {
__m128i m128;
unsigned __int128 u128;
} u;
u.u128 = (unsigned __int128)a * (unsigned __int128)b;
return u.m128;
}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}
{-# LANGUAGE UnliftedFFITypes #-}
foreign import ccall unsafe "wideningMul_xmm"
c_wideningMul_xmm :: Word64 -> Word64 -> Word64X2#
wideningMulXMM :: Word64 -> Word64 -> Word128
wideningMulXMM !a !b = case unpackWord64X2# (c_wideningMul_xmm a b) of
(# lo, hi #) -> Word128 (W64# hi) (W64# lo)
汎用的な多値返却の方法ではありませんが、今回はたまたま128ビットで済む題材なので取り上げてみました。
黒魔術:foreign import prim
注意:unsafePerformIOやUnsafe FFIを見て「unsafeこわ」と思った健全な方は、今のうちに帰っパ2しておくことをお勧めします。
先ほど書いたように、現状のGHCでは構造体を値で返すCの関数を扱うことができません。そして、外部の処理から複数の値を返したい場合はポインターを使うか、複数の関数呼び出しを経由する必要があります。しかし、GHCには複数のレジスターを使って外部の処理から値を返す(多値返却)手段が存在します。それが foreign import prim です。
GHCのPrimOpsについて
整数の足し算のような機械語命令に直接対応するような処理は、GHC的にはprimitive operationと呼ばれます。GHCの組み込み関数ということですね。整数の足し算なら
(+#) :: Int# -> Int# -> Int#
という関数が定義されています。昔はghc-primパッケージの GHC.Prim モジュールで(擬似的に)定義されたものがbaseパッケージの GHC.Exts モジュール経由で公開されるのでそれを使う、という形でしたが、baseパッケージとGHCの切り離しの関係で GHC.Exts は凍結されて、最新の組み込み関数はghc-internalパッケージの GHC.Internal.Exts モジュールかghc-experimentalパッケージの GHC.PrimOps モジュール経由で利用できるようです。
まあ、我々は独自の組み込み関数を定義したいので、既存の組み込み関数を利用する方法は詳しく語らなくて良いでしょう。
GHC Wikiのページ (prim ops · Wiki · Glasgow Haskell Compiler / GHC · GitLab) によると、GHCの組み込み関数には、inline PrimOps, out-of-line PrimOps, foreign out-of-line PrimOps (foreign import prim) の3種類があるようです。
- inline PrimOps: その場で命令列に展開される。GHCにハードコードされている。
- out-of-line PrimOps: 専用の呼び出し規約に従う。GHCにハードコードされている。
- foreign out-of-line PrimOps: 専用の呼び出し規約に従う。ライブラリー開発者が
foreign import primによって定義できる。GHC本体に付属するライブラリー向け。
この中では(というか、CPUの特定の命令を叩くためにHaskellで使えるあらゆる方法の中で)inline PrimOpsが一番低コストだと思われますが、命令一つを使いたいがためにGHC本体に手を入れるのはかなり大掛かり3です。というわけで、相対的にforeign out-of-line PrimOpsが使いやすいということになります。
foreign import prim を使う
foreign import prim を使うHaskellコードの例は次のようになります:
{-# LANGUAGE GHCForeignImportPrim, UnliftedFFITypes, MagicHash, UnboxedTuples #-}
foreign import prim "wideningMul_prim"
wideningMul_prim# :: Word# -> Word# -> (# Word#, Word# #)
wideningMul :: Word64 -> Word64 -> Word128
wideningMul (W64# a) (W64# b)
= case wideningMul_prim# a b of
(# lo, hi #) -> Word128 (W64# hi) (W64# lo)
一見すると foreign import の呼び出し規約が ccall から prim に変わっただけですね。安心した!(型名やタプルに # がついているのは低レベルHaskellではよくあることなので今更驚きません。ccall でも # を使いまくるFFIはできますし)
使うGHC拡張ですが、 foreign import prim を使うには GHCForeignImportPrim 拡張4が必要です。また、引数と返り値は基本的にunlifted typesしか使えないので、 UnliftedFFITypes 拡張も必要になります。MagicHash, UnboxedTuples は言わずもがなですね。
GHCForeignImportPrim 拡張についてはUser's Guideに簡単な説明がありますが、呼び出し規約の詳しい説明はありません。
GHC内部の呼び出し規約を触るコードをこれから掲載しますが、これは完全に無保証であり、GHCの構成やマイナーバージョンの違いによって動かなくなる可能性があります。
実際、私自身の手でGHC内部の呼び出し規約を変えたことがあります。これは私がGHCにした貢献の中で初めて取り込まれたものでした。
- Fewer FP registers than available are used for parameter passing on AArch64 (#17953) · Issue · ghc/ghc
- Support auto-detection of MAX_REAL_{FLOAT,DOUBLE}_REG up to 6 (#17953) (!5117) · Merge requests · Glasgow Haskell Compiler / GHC · GitLab
何らかの事情で foreign import prim を活用する場合は、GHC本体の開発状況を常に注視することを推奨します。
foreign out-of-line PrimOpsを実装する
Haskell側で独自のPrimOpsを使う準備はできたので、次は実装を用意します。
GHCのout-of-line PrimOpsはCmmで書くことが想定されているようですが、
- Cmmはよくわからん!
- Cmmからはインラインアセンブリーや環境依存の組み込み関数を使えなさそう!
なので、アセンブリーで直接書くことにします(後述しますが、Cmm, アセンブリーベタ書きの他にLLVM IRをいじる、という第3の道もあります)。
GHCのPrimOps用の呼び出し規約は、x86_64の場合は
- 引数と返り値は基本的にレジスターで渡す。最初が
%rbx, 2番目が%r14, 3番目が%rsi, 4番目が%rdi, ... - 呼び出し側に戻る時は
jmp *(%rbp)する
という感じのようです。レジスターの使い方に関しては rts/include/stg/MachRegs/x86.h を参照してください。
実際のアセンブリーコードは次のようになります:
.globl _wideningMul_prim
_wideningMul_prim:
## 第一引数:%rbx
## 第二引数:%r14
movq %rbx, %rax
mulq %r14 ## %rax と %r14の積を計算して、上位を %rdx, 下位を %rax に入れる
movq %rax, %rbx ## %rbx に最初の返却値 (lo) を入れる
movq %rdx, %r14 ## %r14 に第二の返却値 (hi) を入れる
jmp *(%rbp)
GHC内部の呼び出し規約では、関数(そして継続)は常に末尾呼び出しなので、レジスターはSTGに予約されているものを除き自由に使えます。
なお、どうしてもC言語で書きたかったら、「特定のプロトタイプを持つ関数として書いてClangでコンパイル、-S -emit-llvm で出力されたLLVM IRをいじって呼び出し規約を ghccc5 にする」という方法があるみたいです。詳しくは下記リンクを参照してください。
参考になるかもしれないリンク集:
- Parsing Market Data with Ragel, clang and GHC primops - Ten Cache Misses: clangが吐くLLVM IRに手を加えてGHCの呼び出し規約を使っている
- haskell - foreign import prim call to LLVM - Stack Overflow: 同上
-
haskell - Using
foreign import primwith a C function using STG calling convention - Stack Overflow: 同上 - Almost Inline ASM in Haskell With Foreign Import Prim - Brandon.Si(mmons): LLVMを介さずに直接x86_64のアセンブリーを書いている
foreign out-of-line PrimOpsを実装する(改良)
上述のアセンブリーコードにはいくつかの課題があります。
- 今回実装する関数名は
wideningMul_primですが、アセンブリーでの名前(シンボル名)はアンダースコアがついた_wideningMul_primとしました。頭にアンダースコアがつくかどうかは、一般にはプラットフォーム(OS)に依存します。例えば、macOSならついて、Linuxならつかない、という具合です。 - 「関数から帰る」時に
jmp *(%rbp)としましたが、これはGHCの構成が--disable-tables-next-to-codeの場合に動作しません。
一つ目の問題は、アンダースコアがつく場合は ghcconfig.h に LEADING_UNDERSCORE というマクロが定義されるので、それを利用すれば良いです。アセンブリーソースの拡張子を大文字の .S に変えればプリプロセッサーが使えて、GHC 9.12以降なら #include "ghcconfig.h" できます(これを可能にする改修は私が行いました)。アセンブリーソースのファイル名は小文字から始めないとGHCがHaskellのモジュール名がコマンドラインで与えられたと勘違いするので注意しましょう(Foo.S というファイル名はダメで、foo.S とする必要がある)。
#include "ghcconfig.h"
#if defined(LEADING_UNDERSCORE)
#define SYMBOL(name) _##name
#else
#define SYMBOL(name) name
#endif
.globl SYMBOL(wideningMul_prim)
SYMBOL(wideningMul_prim):
...
二つ目の問題ですが、「関数から帰る」と言っているのは「STGスタック(x86_64では %rbp)の先頭に積まれた継続を起動する」ということで、GHCが --enable-tables-next-to-code (TNTC) で構成されている場合は継続オブジェクトのアドレスがすなわちコードのアドレスですが、そうでない場合は継続オブジェクトの先頭にコードのアドレスが格納されている形になります。この辺のレイアウトについては rts/include/rts/storage/InfoTables.h を参考にしてください。
// --enable-tables-next-to-codeの場合
struct StgInfoTable {
...
// <- ここのアドレスがSTGスタックに積まれている
StgCode code[]; // 機械語
};
// --disable-tables-next-to-codeの場合
struct StgInfoTable {
// <- ここのアドレスがSTGスタックに積まれている
StgFunPtr entry; // 機械語へのポインター
...
};
これも ghcconfig.h の TABLES_NEXT_TO_CODE マクロで判別できます。
...
#if defined(TABLES_NEXT_TO_CODE)
jmp *(%rbp)
#else
movq (%rbp),%rax
jmp *(%rax)
#endif
このほか、GHCの構成にはunregisterisedビルドというのもあり、その場合もここで書いた方法は通用しなくなると思います。
Cの構造体返しと foreign import prim を組み合わせる(サンク方式)
今回のように命令一発で済む場合はPrimOpsの実体をアセンブリ言語でゴリゴリ書いても苦にはなりませんが、もう少し複雑なコードになるとC言語のような高級言語で書きたくなりますよね。
そこで、
- 処理の本体はC言語の関数として書く(構造体の値返しを使う)
- C言語で書いた関数を呼び出すPrimOpsをアセンブリで書く
- アセンブリで書いたやつを
foreign import primでHaskellから呼び出す
という方式を考えます。C言語の呼び出し規約とGHCの呼び出し規約の違いを、アセンブリで書いた少量のコードで吸収するわけです。この、間に挟まるコードをサンクと呼ぶことにします(Haskellでサンクというと遅延評価がらみのアレを連想しがちですが、そのサンクとは違います)。
Cで書いた部分はこんな感じで、
// __int128 is equivalent to struct { uint64_t lo, hi }; on System V/x86_64 ABI
extern unsigned __int128 wideningMul_uint128(uint64_t a, uint64_t b)
{
return (unsigned __int128)a * (unsigned __int128)b;
}
アセンブリー言語で書くサンクはこんな感じです:
#include "ghcconfig.h"
#if defined(LEADING_UNDERSCORE)
#define SYMBOL(name) _##name
#else
#define SYMBOL(name) name
#endif
.globl SYMBOL(wideningMul_thunk)
SYMBOL(wideningMul_thunk):
## GHC:
## 第一引数:%rbx
## 第二引数:%r14
## C:
## 第一引数:%rdi
## 第二引数:%rsi
movq %rbx, %rdi
movq %r14, %rsi
subq $8, %rsp
callq SYMBOL(wideningMul_uint128)
addq $8, %rsp
## C:
## 第一返却値:%rax
## 第二返却値:%rdx
## GHC:
## 第一返却値:%rbx
## 第二返却値:%r14
movq %rax, %rbx
movq %rdx, %r14
#if defined(TABLES_NEXT_TO_CODE)
jmp *(%rbp)
#else
movq (%rbp),%rax
jmp *(%rax)
#endif
Cの関数を呼ぶと、STGで使うレジスターが破壊されないか心配になりますが、x86_64の場合はSTGレジスターはCの呼び出し規約のcallee-savedレジスターを使うように工夫されているので退避の必要はありません(System V ABIとMicrosoft呼び出し規約の両方で)。
スタックポインター %rsp のアラインメントについても確認する必要があります。x86_64のSystem V ABIおよびMicrosoft呼び出し規約の両方で、スタックポインターは callq の実行時点で16の倍数である必要があります。GHCの呼び出し規約では、関数の突入時にはスタックポインターが 16 * n - 8 の形をしていることを仮定できます。つまり、スタックポインターを16の倍数にしてから callq 命令を発行した状態を模倣しているわけですね。最近のGHCではAVXの都合でこれが 64 * n - 8 になっています(私がしました)。詳しくはGHCの rts/StgCRun.c を見てください。
スタックポインターについて結論を書くと、callq 命令の手前で %rsp から8引いておけばCの呼び出し規約の要件を満たすということです。
妄想ですが、Template Haskellでこういう風なサンクを自動生成して、構造体を値渡し(値返却)するCの関数を直接呼び出せるHaskellライブラリーがあったら楽しそうです。誰か作ってくれ〜〜
ベンチマーク
「64ビット整数2つの乗算を128ビット整数として得る」ための実装方法を色々考えました。というわけで比較です。比較対象は以下です:
- Haskellの多倍長演算(
Integer型)を使う - オペランドを
wide-wordパッケージのWord128型に変換してから乗算する-
wide-wordパッケージは内部的にはtimesWord2#を使っている
-
-
GHC.PrimのtimesWord2#を使う- GHCの内部的には
WordMul2と呼ばれる inline PrimOps の一種
- GHCの内部的には
- C FFIを使う
- Safe FFI / Unsafe FFI
- ポインター(unsafePerformIO / unsafeDupablePerformIO / ****PerformIO) / 2回呼び出し / SIMDレジスターを使う
-
foreign import primを使う- アセンブリー言語で実装
- サンク方式
64ビット演算2つの乗算で128ビットを得る演算としてはすでにGHC組み込みの timesWord2# があるので、「inline PrimOpsと(foreign) out-of-line PrimOpsでどのくらい差があるのか」の比較も行えます。
どれが一番早いか予想しておくと、inline PrimOpsである timesWord2# が一番早く、foreign out-of-line PrimOps (foreign import prim) が続くと考えられます。Integer の多倍長演算は一番遅い気がします。
実際のベンチマーク結果は以下です:
テキストで読みたい方はここを見てください。
まとめると
- 最速はGHC組み込みの
timesWord2#関数で、4.0ns程度。内部的にtimesWord2#を使うWord128も同等のパフォーマンスが出ている。 - 次点は
foreign import prim(アセンブリーのみ)で、4.5ns程度。サンク方式や「unsafe FFI + ポインターを使わず2回呼び出す」もがそれに続く(〜5.8ns)。 - unsafe FFIでXMMレジスターを使うのが7.0ns程度。
- その次は「unsafe FFI + ポインター渡し」で、19ns程度。unsafePerformIOの代わりにunsafeDupablePerformIOや名状し難い冒涜的なやつを使ってもほぼ同じ。
- その次が
Integerの多倍長計算を使うやつで、32.5ns程度。 - 最下位は「safe FFI」で、60ns以上。unsafePerformIOの代わりにunsafeDupablePerformIOや名状し難い冒涜的なやつを使ってもほぼ同じ。「ポインター渡し」よりも「2回呼び出し」の方が遅いのは、safe FFIのコストがポインター渡しのコストを上回ったからでしょう。
となります。
使ったソースコードは GitHub に置いてあります。ベンチマークは筆者のZen4搭載マシンのWSL2で実行しました。
雑感・まとめ
GHC組み込みでインラインの命令列に置き換えられるものが最速なのは当然として、 foreign import prim でそれに肉薄するパフォーマンスが出ることがわかりました(今回使ったのはマイクロベンチマークなので、もっと実践的な例では差が広がったりするかもしれませんが)。
意外なのは「unsafe FFI + ポインターを使わず2回呼び出し」が foreign import prim に対して健闘していることです。CのFFIでもやり方次第では foreign import prim に匹敵するということです。パフォーマンスの差がわずかであれば、 foreign import prim の黒魔術よりもCのFFIを選択するという判断はアリでしょう。
逆に、CのFFIを使っていても、不必要にsafeレベルを使っていると、「CPUの便利な命令を使わずに愚直にHaskellで書いた」バージョンよりも遅くなってしまいました。
-
高速化のためにCのFFIを使う場合はsafety levelが不必要にsafeになっていないか確認しろ
- 注意:
unsafeなFFIはGCをブロックするので、実行時間が長い場合はsafeを使いましょう。unsafeは短時間で済む処理向けです。
- 注意:
というのは結構大事な教訓ではないでしょうか。
-
Unsafe Haskellに精通した方なら「この場合なら
unsafeDupablePerformIOが使えるし、なんなら〈検閲削除〉PerformIOの方が…」と思われるかもしれません。安心してください、これらの実験結果も最後に載せておきます。 ↩ -
「帰ってパイソンしとこ……」の略[嘘] ↩
-
GHC本体に取り込んでもらえるなら苦労の甲斐もあるかもしれませんが、ニッチな命令に対応する組み込み関数をGHC本体に取り込んでもらうには相応の説得力が必要です。GHCにAES命令を入れようというIssueが過去にあったようですが、won't fixでcloseされています。 ↩
-
いかにも「GHC専用!外部のライブラリーは使うな!」感がある名前ですね。 ↩
-
ghcccというのはGHCの呼び出し規約のLLVM内での名称です(→LLVMの呼び出し規約のドキュメント)。昔はcc10と呼ばれていたようです。 ↩
