GHC ではスタックとヒープどちらに割り当てるかなどを扱うような段階より,ずっと以前の表層の部分で多くの最適化が行われます.特に worker/wrapper 変換は Haskell のプログラムをより効率の良いプログラムに変換するような最適化で(厳密には, Core-2-Core ですが),単純に Int や Char などの unbox 型をラップした Boxing な型に対して書かれた幾つかの制約を満たす関数をアンラップする関数と unbox 型に対して処理を行う関数に分解することで,wrap/unwrap のオーバーヘッドを無くし不要なヒープ確保を抑えるといったものです.
例えば,
sumInt :: [Int] -> Int
sumInt = go 0
where
go acc [] = acc
go acc (x:xs) = go (acc + x) xs
という関数を考えてみた時,
(+) :: Int -> Int -> Int
は
I# i1 + I# i2 = I# (i1 +# i2)
と定義されますから
(+)
演算をする分だけ unwrap/wrap を繰り返すことになりますが,以下のように変形できればその操作を抑えることができます.
sumInt :: [Int] -> Int
sumInt = go 0
where
go (I# i) = goWork i
goWork acc [] = I# acc
goWork acc (I# x:xs) = goWork (acc +# x) xs
この変形をより一般的に自動で行うのが worker/wrapper 変換による unbox 化です.もちろん,この後の段階で unbox 型の引数はヒープ確保を起こさない形に翻訳される可能性が高いです.ただこの変換は,見ての通りどの部分で
I#
の unwrap/wrap が起きるか分かっていないとその部分を削減できないため,インライン展開がうまく行われるかどうかに強く依存します.
さて,本題の IORef がなぜ unbox されないかですが,厳密には IORef が unbox 化されないというより, IORef を使ったコードは最終的に IORef を操作するランタイム命令 readMutVar# / writeMutVar# までしかインライン展開されないため, unwrap/wrap の処理がインライン展開後も自動的に構文だけからは判断できず, worker/wrapper 変換が適用されないというのが大きいと思います. readMutVar# / writeMutVar# の動作特性を情報として別に持っておいて worker/wrapper 変換を適用できるようにするのは可能ではありますが,かなりヒューリスティックな部分となるのでおそらく実装されていないのだと思います.もちろん,これは STRef でも同じ話になります.