¿Cuál es la diferencia entre unsafeDupablePerformIO y accursedUnutterablePerformIO?

13

Estaba vagando por la Sección restringida de la Biblioteca Haskell y encontré estos dos hechizos viles:

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

Sin embargo, la diferencia real parece ser solo entre runRW#y ($ realWorld#). Tengo una idea básica de lo que están haciendo, pero no entiendo las consecuencias reales de usar uno sobre otro. ¿Podría alguien explicarme cuál es la diferencia?

rayo
fuente
3
unsafeDupablePerformIOEs más seguro por alguna razón. Si tuviera que adivinar, probablemente tenga que hacer algo con alinearse y salir flotando runRW#. Espero que alguien dé una respuesta adecuada a esta pregunta.
Lehins

Respuestas:

11

Considere una biblioteca de cadenas de bytes simplificada. Es posible que tenga un tipo de cadena de bytes que consta de una longitud y un búfer de bytes asignado:

data BS = BS !Int !(ForeignPtr Word8)

Para crear una cadena de bytes, generalmente necesitaría usar una acción IO:

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

Sin embargo, no es tan conveniente trabajar en la mónada de IO, por lo que puede tener la tentación de hacer un poco de IO insegura:

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

Dada la amplia alineación en su biblioteca, sería bueno alinear el IO inseguro, para un mejor rendimiento:

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

Pero, después de agregar una función conveniente para generar cadenas de bytes singleton:

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

Es posible que se sorprenda al descubrir que se imprime el siguiente programa 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

lo cual es un problema si espera que dos singletons diferentes usen dos buffers diferentes.

Lo que está mal aquí es que la línea extensa significa que las dos mallocForeignPtrBytes 1llamadas entran singleton 1y singleton 2se pueden flotar en una sola asignación, con el puntero compartido entre las dos cadenas de bytes.

Si eliminara la alineación de cualquiera de estas funciones, se evitaría la flotación y el programa se imprimiría Falsecomo se esperaba. Alternativamente, puede realizar el siguiente cambio en 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#

sustituyendo la m realWorld#aplicación en línea con una llamada de función no en línea a myRunRW# m = m realWorld#. Este es el fragmento mínimo de código que, si no está en línea, puede evitar que se levanten las llamadas de asignación.

Después de este cambio, el programa se imprimirá Falsecomo se esperaba.

Esto es todo lo que hace el cambio de inlinePerformIO(AKA accursedUnutterablePerformIO) a unsafeDupablePerformIO. Cambia esa llamada de función m realWorld#de una expresión en línea a una no en línea equivalente 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#

Excepto que lo incorporado runRW#es mágico. A pesar de que éste se crea NOINLINE, que es en realidad inlined por el compilador, pero cerca del final de la compilación después de las llamadas de asignación ya se han impedido flotante.

Por lo tanto, obtiene el beneficio de rendimiento de tener la unsafeDupablePerformIOllamada totalmente en línea sin el efecto secundario indeseable de esa línea que permite que las expresiones comunes en diferentes llamadas inseguras se transfieran a una sola llamada común.

Aunque, a decir verdad, hay un costo. Cuando accursedUnutterablePerformIOfunciona correctamente, puede ofrecer un rendimiento ligeramente mejor porque hay más oportunidades de optimización si la m realWorld#llamada se puede alinear más temprano que tarde. Por lo tanto, la bytestringbiblioteca real todavía se usa accursedUnutterablePerformIOinternamente en muchos lugares, en particular donde no hay asignación en curso (por ejemplo, la headusa para mirar el primer byte del búfer).

KA Buhr
fuente