В чем разница между unsafeDupablePerformIO и accursedUnutterablePerformIO?

13

Я бродил в Запретном разделе Библиотеки Хаскелла и нашел эти два мерзких заклинания:

{- 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#), однако. У меня есть некоторое общее представление о том, что они делают, но я не понимаю реальных последствий использования одного над другим. Может ли кто-нибудь объяснить мне, в чем разница?

radrow
источник
3
unsafeDupablePerformIOпо какой-то причине безопаснее. Если бы я должен был догадаться, это, вероятно, нужно сделать что-то с использованием inlining и float out runRW#. Ждем того, кто даст правильный ответ на этот вопрос.
lehins

Ответы:

11

Рассмотрим упрощенную библиотеку тестовых строк. У вас может быть тип строки байтов, состоящий из длины и выделенного буфера байтов:

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)

Вы можете быть удивлены, обнаружив, что печатает следующая программа 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могут быть распределены в одном распределении с указателем, совместно используемым двумя байтовыми строками.

Если бы вы удалили вставку из любой из этих функций, то плавание было бы предотвращено, и программа распечатала бы 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использует ее для просмотра первого байта буфера).

К. А. Бур
источник