私は Haskell ライブラリの制限区域をさまよっていたところ、次の 2 つのひどい呪文を見つけました。
{- System.IO.Unsafe -}
unsafeDupablePerformIO :: IO a -> a
unsafeDupablePerformIO (IO m) = case runRW# m of (# _, a #) -> a
{- Data.ByteString.Internal -}
accursedUnutterablePerformIO :: IO a -> a
accursedUnutterablePerformIO (IO m) = case m realWorld# of (# _, r #) -> r
runRW#
しかし、実際の違いはとの間だけのようです($ realWorld#)
。 それらが何をしているのかは大体分かっていますが、どちらか一方を使用することの実際の結果は分かりません。 誰か違いを説明してくれませんか?
ベストアンサー1
簡略化されたバイト文字列ライブラリを考えてみましょう。長さと割り当てられたバイトのバッファで構成されるバイト文字列型があるとします。
data BS = BS !Int !(ForeignPtr Word8)
バイト文字列を作成するには、通常、IO アクションを使用する必要があります。
create :: Int -> (Ptr Word8 -> IO ()) -> IO BS
{-# INLINE create #-}
create n f = do
p <- mallocForeignPtrBytes n
withForeignPtr p $ f
return $ BS n p
ただし、IO モナドで作業するのはそれほど便利ではないので、少し安全でない IO を実行したいと思うかもしれません。
unsafeCreate :: Int -> (Ptr Word8 -> IO ()) -> BS
{-# INLINE unsafeCreate #-}
unsafeCreate n f = myUnsafePerformIO $ create n f
ライブラリ内の広範なインライン化を考慮すると、最高のパフォーマンスを得るために、安全でない IO をインライン化するとよいでしょう。
myUnsafePerformIO :: IO a -> a
{-# INLINE myUnsafePerformIO #-}
myUnsafePerformIO (IO m) = case m realWorld# of (# _, r #) -> r
しかし、シングルトン バイト文字列を生成するための便利な関数を追加すると、次のようになります。
singleton :: Word8 -> BS
{-# INLINE singleton #-}
singleton x = unsafeCreate 1 (\p -> poke p x)
次のプログラムが次のように印刷されることに驚くかもしれませんTrue
:
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}
import GHC.IO
import GHC.Prim
import Foreign
data BS = BS !Int !(ForeignPtr Word8)
create :: Int -> (Ptr Word8 -> IO ()) -> IO BS
{-# INLINE create #-}
create n f = do
p <- mallocForeignPtrBytes n
withForeignPtr p $ f
return $ BS n p
unsafeCreate :: Int -> (Ptr Word8 -> IO ()) -> BS
{-# INLINE unsafeCreate #-}
unsafeCreate n f = myUnsafePerformIO $ create n f
myUnsafePerformIO :: IO a -> a
{-# INLINE myUnsafePerformIO #-}
myUnsafePerformIO (IO m) = case m realWorld# of (# _, r #) -> r
singleton :: Word8 -> BS
{-# INLINE singleton #-}
singleton x = unsafeCreate 1 (\p -> poke p x)
main :: IO ()
main = do
let BS _ p = singleton 1
BS _ q = singleton 2
print $ p == q
2 つの異なるシングルトンが 2 つの異なるバッファーを使用することが予想される場合、これは問題になります。
ここで問題になるのは、広範なインライン化によって、 と の 2 つのmallocForeignPtrBytes 1
呼び出しsingleton 1
がsingleton 2
単一の割り当てにフロートアウトされ、2 つのバイト文字列間でポインタが共有されることです。
これらの関数のいずれかからインライン展開を削除すると、フローティングが防止され、プログラムはFalse
期待どおりに印刷されます。 または、 に次の変更を加えることもできますmyUnsafePerformIO
。
myUnsafePerformIO :: IO a -> a
{-# INLINE myUnsafePerformIO #-}
myUnsafePerformIO (IO m) = case myRunRW# m of (# _, r #) -> r
myRunRW# :: forall (r :: RuntimeRep) (o :: TYPE r).
(State# RealWorld -> o) -> o
{-# NOINLINE myRunRW# #-}
myRunRW# m = m realWorld#
インラインm realWorld#
アプリケーションを、 への非インライン関数呼び出しに置き換えますmyRunRW# m = m realWorld#
。これは、インライン化されていない場合に割り当て呼び出しが解除されるのを防ぐことができる最小限のコード チャンクです。
この変更後、プログラムはFalse
期待どおりに印刷されるようになります。
inlinePerformIO
(別名accursedUnutterablePerformIO
)から への切り替えで行われることはこれだけですunsafeDupablePerformIO
。これにより、関数呼び出しがm realWorld#
インライン式から同等の非インライン に変更されますrunRW# m = m realWorld#
。
unsafeDupablePerformIO :: IO a -> a
unsafeDupablePerformIO (IO m) = case runRW# m of (# _, a #) -> a
runRW# :: forall (r :: RuntimeRep) (o :: TYPE r).
(State# RealWorld -> o) -> o
{-# NOINLINE runRW# #-}
runRW# m = m realWorld#
ただし、ビルトインはrunRW#
魔法です。 とマークされていてもNOINLINE
、は実際にはコンパイラによってインライン化されますが、割り当て呼び出しがフローティング状態になるのを既に防いだ後のコンパイルの終わり近くになります。
したがって、unsafeDupablePerformIO
異なる安全でない呼び出し内の共通式を共通の単一の呼び出しにフロートさせることができるインライン化の望ましくない副作用なしに、呼び出しを完全にインライン化することでパフォーマンス上の利点が得られます。
しかし、実を言うと、コストはかかります。 が正しく動作する場合、呼び出しを後ではなく早めにインライン化できれaccursedUnutterablePerformIO
ば、最適化の機会が増えるため、パフォーマンスがわずかに向上する可能性があります。そのため、実際のライブラリは、特に割り当てが行われていない場所 (たとえば、バッファーの最初のバイトを覗くために使用) など、多くの場所で内部的にを使用しています。m realWorld#
bytestring
accursedUnutterablePerformIO
head