Jaka jest różnica między unsafeDupablePerformIO i exactedUnutterablePerformIO?

13

Wędrowałem w Dziale Ograniczonym Biblioteki Haskell i znalazłem te dwa ohydne zaklęcia:

{- 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

Rzeczywista różnica wydaje się jednak być pomiędzy, runRW#a ($ realWorld#)jednak. Mam podstawowe pojęcie o tym, co oni robią, ale nie dostaję prawdziwych konsekwencji używania jednego nad drugim. Czy ktoś mógłby mi wyjaśnić, na czym polega różnica?

radrow
źródło
3
unsafeDupablePerformIOz jakiegoś powodu jest bezpieczniejszy. Gdybym musiał zgadywać, to prawdopodobnie musi coś zrobić z inklinacją i odpłynięciem runRW#. Czekam na kogoś, kto udzieli właściwej odpowiedzi na to pytanie.
lehins

Odpowiedzi:

11

Rozważ uproszczoną bibliotekę testową. Możesz mieć typ ciągu bajtów składający się z długości i przydzielonego bufora bajtów:

data BS = BS !Int !(ForeignPtr Word8)

Aby utworzyć bajtowanie, zazwyczaj trzeba użyć akcji IO:

create :: Int -> (Ptr Word8 -> IO ()) -> IO BS
{-# INLINE create #-}
create n f = do
  p <- mallocForeignPtrBytes n
  withForeignPtr p $ f
  return $ BS n p

Jednak praca w monadzie IO nie jest zbyt wygodna, więc możesz mieć ochotę zrobić trochę niebezpieczne IO:

unsafeCreate :: Int -> (Ptr Word8 -> IO ()) -> BS
{-# INLINE unsafeCreate #-}
unsafeCreate n f = myUnsafePerformIO $ create n f

Biorąc pod uwagę obszerne wstawianie w bibliotece, dobrze byłoby umieścić niebezpieczne IO, aby uzyskać najlepszą wydajność:

myUnsafePerformIO :: IO a -> a
{-# INLINE myUnsafePerformIO #-}
myUnsafePerformIO (IO m) = case m realWorld# of (# _, r #) -> r

Ale po dodaniu funkcji wygodnej do generowania testów pojedynczych:

singleton :: Word8 -> BS
{-# INLINE singleton #-}
singleton x = unsafeCreate 1 (\p -> poke p x)

możesz być zaskoczony, gdy zobaczysz, że następujący program drukuje 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

co jest problemem, jeśli oczekujesz, że dwa różne singletony będą używać dwóch różnych buforów.

To, co dzieje się tutaj źle, polega na tym, że rozległe wstawianie oznacza, że ​​dwa mallocForeignPtrBytes 1połączenia przychodzą singleton 1i singleton 2mogą zostać przeniesione do jednego przydziału, ze wskaźnikiem dzielonym między dwoma bajtami.

Jeśli usuniesz wstawianie z którejkolwiek z tych funkcji, wówczas pływanie zostanie zablokowane, a program wydrukuje się Falsezgodnie z oczekiwaniami. Alternatywnie możesz wprowadzić następującą zmianę w 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#

podstawienie m realWorld#aplikacji wbudowanej niewymienionym wywołaniem funkcji do myRunRW# m = m realWorld#. Jest to minimalna część kodu, która, jeśli nie zostanie wstawiona, może uniemożliwić zniesienie wywołań alokacji.

Po tej zmianie program wydrukuje Falsezgodnie z oczekiwaniami.

To wszystko, co zmienia się z inlinePerformIO(AKA accursedUnutterablePerformIO) na unsafeDupablePerformIO. Zmienia to wywołanie funkcji m realWorld#z wyrażenia wbudowanego na równoważne nieliniowanie 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#

Z wyjątkiem tego, że wbudowana runRW#jest magia. Nawet jeśli jest to zaznaczone NOINLINE, to jest rzeczywiście inlined przez kompilator, ale pod koniec zestawiania połączeń po alokacji zostały uniemożliwione pływających.

W ten sposób uzyskujesz korzyść z wydajności polegającą na unsafeDupablePerformIOpełnym wprowadzeniu połączenia bez niepożądanego efektu ubocznego tego wstawiania, umożliwiając przeniesienie wspólnych wyrażeń w różnych niebezpiecznych połączeniach do jednego pojedynczego połączenia.

Chociaż prawdę mówiąc, istnieje pewna opłata. Gdy accursedUnutterablePerformIOdziała poprawnie, może potencjalnie dać nieco lepszą wydajność, ponieważ istnieje więcej możliwości optymalizacji, jeśli m realWorld#wywołanie można wstawić wcześniej niż później. Tak więc rzeczywista bytestringbiblioteka nadal korzysta accursedUnutterablePerformIOwewnętrznie w wielu miejscach, w szczególności tam, gdzie nie ma miejsca alokacja (np. headWykorzystuje ją do zerknięcia pierwszego bajtu bufora).

KA Buhr
źródło