Quelle est la différence entre unsafeDupablePerformIO et accursedUnutterablePerformIO?

13

Je me promenais dans la section restreinte de la bibliothèque Haskell et j'ai trouvé ces deux sorts ignobles:

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

La différence réelle semble toutefois être juste entre runRW#et ($ realWorld#). J'ai une idée de base de ce qu'ils font, mais je n'ai pas les vraies conséquences de les utiliser les uns par rapport aux autres. Quelqu'un pourrait-il m'expliquer quelle est la différence?

radrow
la source
3
unsafeDupablePerformIOest plus sûr pour une raison quelconque. Si je devais deviner, il fallait probablement faire quelque chose avec l'incrustation et la flottaison runRW#. Au plaisir que quelqu'un donne une bonne réponse à cette question.
lehins

Réponses:

11

Considérons une bibliothèque simplifiée de bytestring. Vous pouvez avoir un type de chaîne d'octets composé d'une longueur et d'un tampon d'octets alloué:

data BS = BS !Int !(ForeignPtr Word8)

Pour créer un bytestring, vous devez généralement utiliser une action IO:

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

Ce n'est pas si pratique de travailler dans la monade d'E / S, cependant, vous pourriez être tenté de faire un peu d'E / S non sécurisées:

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

Compte tenu de l'importante inlining de votre bibliothèque, il serait intéressant d'inclure l'IO dangereux, pour de meilleures performances:

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

Mais, après avoir ajouté une fonction pratique pour générer des sous-tests singleton:

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

vous pourriez être surpris de découvrir que le programme suivant s'imprime 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

ce qui est un problème si vous vous attendez à ce que deux singletons différents utilisent deux tampons différents.

Ce qui ne va pas ici, c'est que l'intensification étendue signifie que les deux mallocForeignPtrBytes 1appels singleton 1et singleton 2peuvent être flottants dans une seule allocation, avec le pointeur partagé entre les deux bytestrings.

Si vous deviez supprimer l'inline de l'une de ces fonctions, le flottement serait empêché et le programme s'imprimerait Falsecomme prévu. Vous pouvez également apporter la modification suivante à 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#

en remplaçant l' m realWorld#application en ligne par un appel de fonction non en ligne à myRunRW# m = m realWorld#. Il s'agit de la portion minimale de code qui, si elle n'est pas insérée, peut empêcher la levée des appels d'allocation.

Après cette modification, le programme s'imprimera Falsecomme prévu.

C'est tout ce que le passage de inlinePerformIO(AKA accursedUnutterablePerformIO) à unsafeDupablePerformIOfait. Il modifie cet appel m realWorld#de fonction d'une expression en ligne vers une expression non en ligne équivalente 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#

Sauf que le intégré runRW#est magique. Même s'il est marqué NOINLINE, il est en fait inséré par le compilateur, mais vers la fin de la compilation après que les appels d'allocation ont déjà été empêchés de flotter.

Ainsi, vous obtenez l'avantage de performances d'avoir l' unsafeDupablePerformIOappel entièrement en ligne sans l'effet secondaire indésirable de cette mise en ligne permettant aux expressions communes dans différents appels non sécurisés d'être transférées à un seul appel commun.

Mais, à vrai dire, il y a un coût. Lorsqu'il accursedUnutterablePerformIOfonctionne correctement, il peut potentiellement donner des performances légèrement meilleures car il y a plus de possibilités d'optimisation si l' m realWorld#appel peut être intégré plus tôt que tard. Ainsi, la bytestringbibliothèque réelle utilise toujours en accursedUnutterablePerformIOinterne dans de nombreux endroits, en particulier où il n'y a pas d'allocation en cours (par exemple, l' headutilise pour jeter un œil au premier octet du tampon).

KA Buhr
la source