unsafeDupablePerformIO和accursedUnutterablePerformIO有什么区别?

21

我在 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#)之间存在差异。我对它们正在做的事情有一些基本的想法,但我不知道使用其中一个相对于另一个所产生的真正后果。能有人解释一下它们之间的区别吗?

3
unsafeDupablePerformIO 由于某些原因更加安全。如果我猜的话,可能与对 runRW# 进行内联和浮动有关。期待有人能给出这个问题的正确答案。 - lehins
1个回答

24

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

data BS = BS !Int !(ForeignPtr Word8)
创建一个bytestring,通常需要使用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使用它来查看缓冲区的第一个字节)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接