我在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

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

出问题的是,广泛的内联意味着mallocForeignPtrBytes 1singleton 1中的两个singleton 2调用可以 float 到单个分配中,并且指针在两个字节字符串之间共享。

如果要从任何这些函数中删除内联,则将防止 float ,并且程序将按预期方式打印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(又名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,它实际上是由编译器内联的,但是在分配调用已经被阻止 float 之后,接近编译的结尾。

因此,您可以获得性能的好处,即可以完全内联unsafeDupablePerformIO调用,而不会产生内联带来的不良影响,该内联允许将不同不安全调用中的 public 表达式 float 到 public 单个调用中。

虽然,说实话,这是有代价的。当accursedUnutterablePerformIO正常工作时,它可能会带来稍微更好的性能,因为如果m realWorld#调用可以早于内联而不是内联,则存在更多优化机会。因此,实际的bytestring库在很多地方仍在内部使用accursedUnutterablePerformIO,尤其是在没有分配正在进行的地方(例如,head使用它来窥视缓冲区的第一个字节)。

10-06 13:05