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 1
appels singleton 1
et singleton 2
peuvent ê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 False
comme 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 False
comme prévu.
C'est tout ce que le passage de inlinePerformIO
(AKA accursedUnutterablePerformIO
) à unsafeDupablePerformIO
fait. 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' unsafeDupablePerformIO
appel 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 accursedUnutterablePerformIO
fonctionne 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 bytestring
bibliothèque réelle utilise toujours en accursedUnutterablePerformIO
interne dans de nombreux endroits, en particulier où il n'y a pas d'allocation en cours (par exemple, l' head
utilise pour jeter un œil au premier octet du tampon).
unsafeDupablePerformIO
est plus sûr pour une raison quelconque. Si je devais deviner, il fallait probablement faire quelque chose avec l'incrustation et la flottaisonrunRW#
. Au plaisir que quelqu'un donne une bonne réponse à cette question.