unsafeDupablePerformIO 和 accursedUnutterablePerformIO 有什么区别?

rad*_*row 17 io haskell unsafe unsafe-perform-io

我在哈斯克尔图书馆的限制区闲逛,发现了这两个邪恶的咒语:

{- 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
Run Code Online (Sandbox Code Playgroud)

然而,实际差异似乎只是在runRW#和之间($ realWorld#)。我对他们在做什么有一些基本的了解,但我没有得到使用一个而不是另一个的真正后果。有人可以解释我有什么区别吗?

K. *_*uhr 18

考虑一个简化的字节串库。您可能有一个字节字符串类型,它由一个长度和一个已分配的字节缓冲区组成:

data BS = BS !Int !(ForeignPtr Word8)
Run Code Online (Sandbox Code Playgroud)

要创建字节串,您通常需要使用 IO 操作:

create :: Int -> (Ptr Word8 -> IO ()) -> IO BS
{-# INLINE create #-}
create n f = do
  p <- mallocForeignPtrBytes n
  withForeignPtr p $ f
  return $ BS n p
Run Code Online (Sandbox Code Playgroud)

然而,在 IO monad 中工作并不是那么方便,所以你可能会想做一些不安全的 IO:

unsafeCreate :: Int -> (Ptr Word8 -> IO ()) -> BS
{-# INLINE unsafeCreate #-}
unsafeCreate n f = myUnsafePerformIO $ create n f
Run Code Online (Sandbox Code Playgroud)

鉴于您的库中有大量内联,最好内联不安全的 IO,以获得最佳性能:

myUnsafePerformIO :: IO a -> a
{-# INLINE myUnsafePerformIO #-}
myUnsafePerformIO (IO m) = case m realWorld# of (# _, r #) -> r
Run Code Online (Sandbox Code Playgroud)

但是,在您添加一个用于生成单例字节串的便捷函数之后:

singleton :: Word8 -> BS
{-# INLINE singleton #-}
singleton x = unsafeCreate 1 (\p -> poke p x)
Run Code Online (Sandbox Code Playgroud)

您可能会惊讶地发现以下程序打印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
Run Code Online (Sandbox Code Playgroud)

如果您希望两个不同的单身人士使用两个不同的缓冲区,这是一个问题。

发生了什么事错在这里的是,大量的内联意味着两个mallocForeignPtrBytes 1在通话singleton 1singleton 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#
Run Code Online (Sandbox Code Playgroud)

用对m realWorld#的非内联函数调用替换内联应用程序myRunRW# m = m realWorld#。这是最小的代码块,如果没有内联,可以防止分配调用被解除。

此更改后,程序将按False预期打印。

这就是从inlinePerformIO(AKA accursedUnutterablePerformIO)切换到unsafeDupablePerformIO所做的所有事情。它将函数调用m realWorld#从内联表达式更改为等效的 noninlined 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#
Run Code Online (Sandbox Code Playgroud)

除了,内置runRW#是魔术。即使它的标记NOINLINE,它实际上由编译器内联,但临近分配调用编译之后结束已经漂浮阻止。

因此,您可以获得unsafeDupablePerformIO完全内联调用的性能优势,而不会产生内联的不良副作用,允许将不同不安全调用中的公共表达式浮动到公共单个调用中。

不过,说实话,这是有代价的。当accursedUnutterablePerformIO工作正常时,它可能会提供稍微更好的性能,因为如果m realWorld#调用可以更早而不是更晚内联,则有更多的优化机会。所以,实际的bytestringaccursedUnutterablePerformIO在很多地方仍然在内部使用,特别是在没有进行分配的地方(例如,head使用它来查看缓冲区的第一个字节)。