Яка різниця між небезпечнимDupablePerformIO та incursedUnutterablePerformIO?


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


3
unsafeDupablePerformIOчомусь безпечніше. Якби я мусив здогадуватися, мабуть, треба щось робити з вишивкою та пливуть 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

Однак не все так зручно працювати в монаді 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використовує її для вигляду першого байта буфера).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.