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 1połączenia przychodzą singleton 1i singleton 2mogą 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ę Falsezgodnie 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 Falsezgodnie 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 unsafeDupablePerformIOpeł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 accursedUnutterablePerformIOdział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 bytestringbiblioteka nadal korzysta accursedUnutterablePerformIOwewnętrznie w wielu miejscach, w szczególności tam, gdzie nie ma miejsca alokacja (np. headWykorzystuje ją do zerknięcia pierwszego bajtu bufora).
unsafeDupablePerformIOz 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.