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:
data BS = BS !Int !(ForeignPtr Word8)
Aby utworzyć bajtowanie, zazwyczaj trzeba użyć akcji IO:
create :: Int -> (Ptr Word8 -> IO ()) -> IO BS
{-# INLINE create #-}
create n f = do
p <- mallocForeignPtrBytes n
withForeignPtr p $ f
return $ BS n p
Jednak praca w monadzie IO nie jest zbyt wygodna, więc możesz mieć ochotę zrobić trochę niebezpieczne IO:
unsafeCreate :: Int -> (Ptr Word8 -> IO ()) -> BS
{-# INLINE unsafeCreate #-}
unsafeCreate n f = myUnsafePerformIO $ create n f
Biorąc pod uwagę obszerne wstawianie w bibliotece, dobrze byłoby umieścić niebezpieczne IO, aby uzyskać najlepszą wydajność:
myUnsafePerformIO :: IO a -> a
{-# INLINE myUnsafePerformIO #-}
myUnsafePerformIO (IO m) = case m realWorld# of (# _, r #) -> r
Ale po dodaniu funkcji wygodnej do generowania testów pojedynczych:
singleton :: Word8 -> BS
{-# INLINE singleton #-}
singleton x = unsafeCreate 1 (\p -> poke p x)
możesz być zaskoczony, gdy zobaczysz, że następujący program drukuje 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
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
i singleton 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ę w 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#
podstawienie m realWorld#
aplikacji wbudowanej niewymienionym wywołaniem funkcji do myRunRW# 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
(AKA accursedUnutterablePerformIO
) na unsafeDupablePerformIO
. Zmienia to wywołanie funkcji m realWorld#
z wyrażenia wbudowanego na równoważne nieliniowanie 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#
Z wyjątkiem tego, że wbudowana runRW#
jest magia. Nawet jeśli jest to zaznaczone NOINLINE
, 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śli m realWorld#
wywołanie można wstawić wcześniej niż później. Tak więc rzeczywista bytestring
biblioteka nadal korzysta accursedUnutterablePerformIO
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).
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.