Розглянемо спрощену бібліотеку тестування. У вас може бути тип байтового рядка, що складається з довжини та виділеного буфера байтів:
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
Однак не все так зручно працювати в монаді IO, тому ви можете спокуситися зробити трохи небезпечний 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
виклики в 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
чомусь безпечніше. Якби я мусив здогадуватися, мабуть, треба щось робити з вишивкою та пливутьrunRW#
. З нетерпінням чекаю, що хтось дасть належну відповідь на це питання.