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 1
chiamate in singleton 1
e singleton 2
possono 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 False
come 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 False
come 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 unsafeDupablePerformIO
chiamata 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 accursedUnutterablePerformIO
funziona 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 bytestring
libreria attuale utilizza ancora accursedUnutterablePerformIO
internamente in molti punti, in particolare dove non è in corso alcuna allocazione (ad esempio, la head
utilizza per sbirciare il primo byte del buffer).
unsafeDupablePerformIO
è più sicuro per qualche motivo. Se dovessi indovinare, probabilmente devo fare qualcosa con l'allineamento e il fluttuare fuorirunRW#
. In attesa di qualcuno che dia una risposta adeguata a questa domanda.