考虑一个简化的字节串库。你可能有一个由长度和分配的字节缓冲区组成的字节串类型:
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 1
和singleton 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
使用它来查看缓冲区的第一个字节)。
unsafeDupablePerformIO
由于某些原因更加安全。如果我猜的话,可能与对runRW#
进行内联和浮动有关。期待有人能给出这个问题的正确答案。 - lehins