19

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

4

1 回答 1

19

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

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

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

这里的问题是广泛的内联意味着两个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#

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使用它来查看缓冲区的第一个字节)。

于 2020-04-04T06:19:03.303 回答