unsafeDupablePerformIO 和 accursedUnutterablePerformIO 之间有什么区别?
What is the difference between unsafeDupablePerformIO and accursedUnutterablePerformIO?
我在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
如果您希望两个不同的单例使用两个不同的缓冲区,这就是一个问题。
这里出了什么问题是广泛的内联意味着 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
使用它来查看缓冲区的第一个字节) .
我在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
如果您希望两个不同的单例使用两个不同的缓冲区,这就是一个问题。
这里出了什么问题是广泛的内联意味着 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
使用它来查看缓冲区的第一个字节) .