unsafeDupablePerformIO 和 accursedUnutterablePerformIO 之间有什么区别?

What is the difference between unsafeDupablePerformIO and accursedUnutterablePerformIO?

我在Haskell图书馆的禁区里闲逛,发现了这两个卑鄙的咒语:

{- 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#) 之间。我对他们在做什么有一些基本的了解,但我不明白使用一个而不是另一个的真正后果。有人可以解释一下有什么区别吗?

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

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 monad 中工作并不是那么方便,所以您可能会想做一些不安全的 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

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

这里出了什么问题是广泛的内联意味着 singleton 1singleton 2 中的两个 mallocForeignPtrBytes 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#

用对 myRunRW# m = m realWorld# 的非内联函数调用替换内联 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 使用它来查看缓冲区的第一个字节) .