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?
haskell
io
unsafe
unsafe-perform-io
Radrow
quelle
quelle
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.Antworten:
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:
Um einen Bytestring zu erstellen, müssen Sie im Allgemeinen eine E / A-Aktion verwenden:
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:
Angesichts des umfangreichen Inlining in Ihrer Bibliothek wäre es schön, das unsichere E / A für eine optimale Leistung zu integrieren:
Nachdem Sie jedoch eine praktische Funktion zum Generieren von Singleton-Bytestrings hinzugefügt haben:
Sie werden überrascht sein, dass das folgende Programm gedruckt wird
True
: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 eingehensingleton 1
undsingleton 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 vornehmenmyUnsafePerformIO
:Ersetzen der Inline-
m realWorld#
Anwendung durch einen nicht inline-Funktionsaufruf anmyRunRW# 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
(AKAaccursedUnutterablePerformIO
) aufunsafeDupablePerformIO
bewirkt. Dieser Funktionsaufruf wirdm realWorld#
von einem inline-Ausdruck in einen äquivalenten nichtinlinierten Ausdruck geändertrunRW# m = m realWorld#
:Außer, das eingebaute
runRW#
ist Magie. Auch wenn es markiert istNOINLINE
, 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 derm realWorld#
Anruf eher früher als später eingebunden werden kann. Die eigentlichebytestring
Bibliothek wird also immer nochaccursedUnutterablePerformIO
intern an vielen Stellen verwendet, insbesondere dort, wo keine Zuordnung stattfindet (z. B.head
zum Durchsuchen des ersten Bytes des Puffers).quelle