Qual è la differenza tra unsafeDupablePerformIO e accursedUnutterablePerformIO?


13

Stavo vagando nella sezione riservata della Biblioteca Haskell e ho trovato questi due incantesimi vili:

{- 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 differenza effettiva sembra essere solo tra runRW#e ($ realWorld#), tuttavia. Ho un'idea di base su cosa stiano facendo, ma non ottengo le vere conseguenze dell'uso l'uno sull'altro. Qualcuno potrebbe spiegarmi qual è la differenza?


3
unsafeDupablePerformIOè più sicuro per qualche motivo. Se dovessi indovinare, probabilmente devo fare qualcosa con l'allineamento e il fluttuare fuori runRW#. In attesa di qualcuno che dia una risposta adeguata a questa domanda.
lehins

Risposte:


11

Considera una libreria semplificata bytestring. Potresti avere un tipo di stringa di byte costituito da una lunghezza e un buffer allocato di byte:

data BS = BS !Int !(ForeignPtr Word8)

Per creare un bytestring, in genere è necessario utilizzare un'azione IO:

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

Tuttavia, non è così conveniente lavorare nella monade IO, quindi potresti essere tentato di fare un po 'di IO non sicuro:

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

Data l'ampia inclinazione nella tua libreria, sarebbe bello incorporare l'IO non sicuro, per le migliori prestazioni:

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

Ma, dopo aver aggiunto una funzione di convenienza per la generazione di bytestring singleton:

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

potresti essere sorpreso di scoprire che il seguente programma stampa 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

il che è un problema se ci si aspetta che due singoli singoli utilizzino due buffer diversi.

Che cosa c'è che non va è che gli ampi mezzi inline che le due mallocForeignPtrBytes 1chiamate in singleton 1e singleton 2possono essere galleggiavano fuori in una dotazione unica, con il puntatore condiviso tra le due stringhe di byte.

Se si dovesse rimuovere la linea interna da una di queste funzioni, il float verrebbe impedito e il programma verrebbe stampato Falsecome previsto. In alternativa, è possibile apportare la seguente modifica a 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#

sostituendo l' m realWorld#applicazione incorporata con una chiamata di funzione non incorporata a myRunRW# m = m realWorld#. Questa è la porzione minima di codice che, se non in linea, può impedire il sollevamento delle chiamate di allocazione.

Dopo questa modifica, il programma verrà stampato Falsecome previsto.

Questo è tutto ciò che passa da inlinePerformIO(AKA accursedUnutterablePerformIO) a unsafeDupablePerformIO. Cambia quella chiamata di funzione m realWorld#da un'espressione incorporata ad una equivalente non incorporata 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#

Tranne, il built-in runRW#è magico. Anche se è contrassegnato NOINLINE, in realtà è integrato dal compilatore, ma verso la fine della compilazione dopo che le chiamate di allocazione sono già state bloccate.

Pertanto, si ottiene il vantaggio in termini di prestazioni di avere la unsafeDupablePerformIOchiamata completamente incorporata senza l'effetto indesiderato di tale allineamento che consente alle espressioni comuni in diverse chiamate non sicure di essere trasferite in una singola chiamata comune.

Tuttavia, a dire la verità, c'è un costo. Se accursedUnutterablePerformIOfunziona correttamente, può potenzialmente offrire prestazioni leggermente migliori perché ci sono più opportunità di ottimizzazione se la m realWorld#chiamata può essere incorporata prima piuttosto che dopo. Pertanto, la bytestringlibreria attuale utilizza ancora accursedUnutterablePerformIOinternamente in molti punti, in particolare dove non è in corso alcuna allocazione (ad esempio, la headutilizza per sbirciare il primo byte del buffer).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.