unsafeDupablePerformIO和accursedUnutterablePerformIO有什么区别?


13

我在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#)。我对他们在做什么有一些基本的想法,但是我并没有真正理解使用它们之间的真正后果。有人可以告诉我有什么区别吗?


3
unsafeDupablePerformIO由于某种原因更安全。如果我不得不猜测,可能必须对内联和浮出水面做些什么runRW#。期待有人对此问题给出正确的答案。
lehins

Answers:


11

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

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调用in 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使用它来窥视缓冲区的第一个字节)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.