Betrachten Sie eine vereinfachte Bytestring-Bibliothek. Möglicherweise haben Sie einen Byte-String-Typ, der aus einer Länge und einem zugewiesenen Byte-Puffer besteht:
data BS = BS !Int !(ForeignPtr Word8)
Um einen Bytestring zu erstellen, müssen Sie im Allgemeinen eine E / A-Aktion verwenden:
create :: Int -> (Ptr Word8 -> IO ()) -> IO BS
{-# INLINE create #-}
create n f = do
p <- mallocForeignPtrBytes n
withForeignPtr p $ f
return $ BS n p
Es ist jedoch nicht so bequem, in der E / A-Monade zu arbeiten, sodass Sie möglicherweise versucht sind, ein wenig unsicheres E / A zu erstellen:
unsafeCreate :: Int -> (Ptr Word8 -> IO ()) -> BS
{-# INLINE unsafeCreate #-}
unsafeCreate n f = myUnsafePerformIO $ create n f
Angesichts des umfangreichen Inlining in Ihrer Bibliothek wäre es schön, das unsichere E / A für eine optimale Leistung zu integrieren:
myUnsafePerformIO :: IO a -> a
{-# INLINE myUnsafePerformIO #-}
myUnsafePerformIO (IO m) = case m realWorld# of (# _, r #) -> r
Nachdem Sie jedoch eine praktische Funktion zum Generieren von Singleton-Bytestrings hinzugefügt haben:
singleton :: Word8 -> BS
{-# INLINE singleton #-}
singleton x = unsafeCreate 1 (\p -> poke p x)
Sie werden überrascht sein, dass das folgende Programm gedruckt wird 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
Dies ist ein Problem, wenn Sie erwarten, dass zwei verschiedene Singletons zwei verschiedene Puffer verwenden.
Was hier falsch läuft, ist, dass das umfangreiche Inlining bedeutet, dass die beiden mallocForeignPtrBytes 1
Aufrufe eingehen singleton 1
und singleton 2
in eine einzige Zuordnung verschoben werden können, wobei der Zeiger zwischen den beiden Bytestrings geteilt wird.
Wenn Sie das Inlining aus einer dieser Funktionen entfernen würden, würde das Floating verhindert und das Programm würde False
wie erwartet gedruckt . Alternativ können Sie folgende Änderungen vornehmen 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#
Ersetzen der Inline- m realWorld#
Anwendung durch einen nicht inline-Funktionsaufruf an myRunRW# m = m realWorld#
. Dies ist der minimale Codeabschnitt, der, wenn er nicht inline ist, verhindern kann, dass die Zuordnungsaufrufe aufgehoben werden.
Nach dieser Änderung wird das Programm False
wie erwartet gedruckt .
Dies ist alles, was das Umschalten von inlinePerformIO
(AKA accursedUnutterablePerformIO
) auf unsafeDupablePerformIO
bewirkt. Dieser Funktionsaufruf wird m realWorld#
von einem inline-Ausdruck in einen äquivalenten nichtinlinierten Ausdruck geändert 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#
Außer, das eingebaute runRW#
ist Magie. Auch wenn es markiert ist NOINLINE
, es ist tatsächlich durch den Compiler inlined, aber am Ende der Zusammenstellung nach der Zuteilung Anrufen bereits Aufschwimmen verhindert worden.
Sie erhalten also den Leistungsvorteil, wenn der unsafeDupablePerformIO
Anruf vollständig inline ist, ohne dass der unerwünschte Nebeneffekt dieses Inlining besteht, sodass gemeinsame Ausdrücke in verschiedenen unsicheren Anrufen auf einen gemeinsamen einzelnen Anruf übertragen werden können.
Um ehrlich zu sein, es gibt jedoch Kosten. Wenn es accursedUnutterablePerformIO
richtig funktioniert, kann es möglicherweise zu einer etwas besseren Leistung führen, da es mehr Optimierungsmöglichkeiten gibt, wenn der m realWorld#
Anruf eher früher als später eingebunden werden kann. Die eigentliche bytestring
Bibliothek wird also immer noch accursedUnutterablePerformIO
intern an vielen Stellen verwendet, insbesondere dort, wo keine Zuordnung stattfindet (z. B. head
zum Durchsuchen des ersten Bytes des Puffers).
unsafeDupablePerformIO
ist aus irgendeinem Grund sicherer. Wenn ich raten müsste, muss es wahrscheinlich etwas mit Inlining und Herausschweben tunrunRW#
. Ich freue mich darauf, dass jemand diese Frage richtig beantwortet.