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