Wędrowałem w Dziale Ograniczonym Biblioteki Haskell i znalazłem te dwa ohydne zaklęcia:
{- 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
Rzeczywista różnica wydaje się jednak być pomiędzy, runRW#
a ($ realWorld#)
jednak. Mam podstawowe pojęcie o tym, co oni robią, ale nie dostaję prawdziwych konsekwencji używania jednego nad drugim. Czy ktoś mógłby mi wyjaśnić, na czym polega różnica?
haskell
io
unsafe
unsafe-perform-io
radrow
źródło
źródło
unsafeDupablePerformIO
z jakiegoś powodu jest bezpieczniejszy. Gdybym musiał zgadywać, to prawdopodobnie musi coś zrobić z inklinacją i odpłynięciemrunRW#
. Czekam na kogoś, kto udzieli właściwej odpowiedzi na to pytanie.Odpowiedzi:
Rozważ uproszczoną bibliotekę testową. Możesz mieć typ ciągu bajtów składający się z długości i przydzielonego bufora bajtów:
Aby utworzyć bajtowanie, zazwyczaj trzeba użyć akcji IO:
Jednak praca w monadzie IO nie jest zbyt wygodna, więc możesz mieć ochotę zrobić trochę niebezpieczne IO:
Biorąc pod uwagę obszerne wstawianie w bibliotece, dobrze byłoby umieścić niebezpieczne IO, aby uzyskać najlepszą wydajność:
Ale po dodaniu funkcji wygodnej do generowania testów pojedynczych:
możesz być zaskoczony, gdy zobaczysz, że następujący program drukuje
True
:co jest problemem, jeśli oczekujesz, że dwa różne singletony będą używać dwóch różnych buforów.
To, co dzieje się tutaj źle, polega na tym, że rozległe wstawianie oznacza, że dwa
mallocForeignPtrBytes 1
połączenia przychodząsingleton 1
isingleton 2
mogą zostać przeniesione do jednego przydziału, ze wskaźnikiem dzielonym między dwoma bajtami.Jeśli usuniesz wstawianie z którejkolwiek z tych funkcji, wówczas pływanie zostanie zablokowane, a program wydrukuje się
False
zgodnie z oczekiwaniami. Alternatywnie możesz wprowadzić następującą zmianę wmyUnsafePerformIO
:podstawienie
m realWorld#
aplikacji wbudowanej niewymienionym wywołaniem funkcji domyRunRW# m = m realWorld#
. Jest to minimalna część kodu, która, jeśli nie zostanie wstawiona, może uniemożliwić zniesienie wywołań alokacji.Po tej zmianie program wydrukuje
False
zgodnie z oczekiwaniami.To wszystko, co zmienia się z
inlinePerformIO
(AKAaccursedUnutterablePerformIO
) naunsafeDupablePerformIO
. Zmienia to wywołanie funkcjim realWorld#
z wyrażenia wbudowanego na równoważne nieliniowanierunRW# m = m realWorld#
:Z wyjątkiem tego, że wbudowana
runRW#
jest magia. Nawet jeśli jest to zaznaczoneNOINLINE
, to jest rzeczywiście inlined przez kompilator, ale pod koniec zestawiania połączeń po alokacji zostały uniemożliwione pływających.W ten sposób uzyskujesz korzyść z wydajności polegającą na
unsafeDupablePerformIO
pełnym wprowadzeniu połączenia bez niepożądanego efektu ubocznego tego wstawiania, umożliwiając przeniesienie wspólnych wyrażeń w różnych niebezpiecznych połączeniach do jednego pojedynczego połączenia.Chociaż prawdę mówiąc, istnieje pewna opłata. Gdy
accursedUnutterablePerformIO
działa poprawnie, może potencjalnie dać nieco lepszą wydajność, ponieważ istnieje więcej możliwości optymalizacji, jeślim realWorld#
wywołanie można wstawić wcześniej niż później. Tak więc rzeczywistabytestring
biblioteka nadal korzystaaccursedUnutterablePerformIO
wewnętrznie w wielu miejscach, w szczególności tam, gdzie nie ma miejsca alokacja (np.head
Wykorzystuje ją do zerknięcia pierwszego bajtu bufora).źródło