В чем разница между unsafeDupablePerformIO и accursedUnutterablePerformIO? - PullRequest
13 голосов
/ 04 апреля 2020

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

1 Ответ

11 голосов
/ 04 апреля 2020

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

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# равен magi c. Несмотря на то, что он помечен NOINLINE, он на самом деле встроен компилятором, но ближе к концу компиляции после того, как вызовы выделения уже были заблокированы.

Итак, вы получаете выигрыш в производительности, связанный с тем, что вызов unsafeDupablePerformIO полностью встроен без нежелательного побочного эффекта от того, что вставка, позволяющая распространять общие выражения в различных небезопасных вызовах на общий одиночный вызов.

Хотя, по правде говоря, существует Стоимость. Когда accursedUnutterablePerformIO работает правильно, он потенциально может дать немного лучшую производительность, потому что есть больше возможностей для оптимизации, если вызов m realWorld# может быть встроен раньше, чем позже. Таким образом, фактическая библиотека bytestring по-прежнему использует accursedUnutterablePerformIO во многих местах, в частности, там, где не происходит выделения (например, head использует ее для просмотра первого байта буфера).

...