Рассмотрим упрощенную библиотеку тестовых строк. У вас может быть тип строки байтов, состоящий из длины и выделенного буфера байтов:
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
использует ее для просмотра первого байта буфера).
unsafeDupablePerformIO
по какой-то причине безопаснее. Если бы я должен был догадаться, это, вероятно, нужно сделать что-то с использованием inlining и float outrunRW#
. Ждем того, кто даст правильный ответ на этот вопрос.