単純化されたバイト文字列ライブラリを考えてみましょう。あなたは長さとバイトの割り当てられたバッファで構成されるバイト文字列型を持っているかもしれません:
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バイト文字列の間で共有ポインタで、1回の割当てに出て浮上させることができます。
これらの関数のいずれかからインライン展開を削除すると、浮動が防止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
(AKA 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
それを使用してバッファーの最初のバイトを確認します)。
unsafeDupablePerformIO
何らかの理由で安全です。私が推測しなければならなかった場合、それはおそらくインライン化とフローティングから何かをしなければならないでしょうrunRW#
。この質問に適切な答えを与える誰かを楽しみにしています。