Was ist der Unterschied zwischen unsafeDupablePerformIO und accursedUnutterablePerformIO?

13

Ich war in der eingeschränkten Abteilung der Haskell-Bibliothek unterwegs und fand diese beiden abscheulichen Zaubersprüche:

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

Der eigentliche Unterschied scheint jedoch nur zwischen runRW#und zu liegen ($ realWorld#). Ich habe eine grundlegende Vorstellung davon, was sie tun, aber ich habe nicht die wirklichen Konsequenzen, wenn ich sie übereinander benutze. Könnte mir jemand erklären, was der Unterschied ist?

Radrow
quelle
3
unsafeDupablePerformIOist aus irgendeinem Grund sicherer. Wenn ich raten müsste, muss es wahrscheinlich etwas mit Inlining und Herausschweben tun runRW#. Ich freue mich darauf, dass jemand diese Frage richtig beantwortet.
Lehins

Antworten:

11

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 1Aufrufe eingehen singleton 1und singleton 2in 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 Falsewie 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 Falsewie erwartet gedruckt .

Dies ist alles, was das Umschalten von inlinePerformIO(AKA accursedUnutterablePerformIO) auf unsafeDupablePerformIObewirkt. 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 unsafeDupablePerformIOAnruf 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 accursedUnutterablePerformIOrichtig 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 bytestringBibliothek wird also immer noch accursedUnutterablePerformIOintern an vielen Stellen verwendet, insbesondere dort, wo keine Zuordnung stattfindet (z. B. headzum Durchsuchen des ersten Bytes des Puffers).

KA Buhr
quelle