我在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 1
和singleton 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
使用它来窥视缓冲区的第一个字节)。