Considere uma biblioteca simplificada de bytestring. Você pode ter um tipo de sequência de bytes composto por um comprimento e um buffer de bytes alocado:
data BS = BS !Int !(ForeignPtr Word8)
Para criar uma bytestring, você geralmente precisa usar uma ação de E / S:
create :: Int -> (Ptr Word8 -> IO ()) -> IO BS
{-# INLINE create #-}
create n f = do
p <- mallocForeignPtrBytes n
withForeignPtr p $ f
return $ BS n p
Porém, não é tão conveniente trabalhar na mônada de IO; portanto, você pode ficar tentado a fazer um IO pouco seguro:
unsafeCreate :: Int -> (Ptr Word8 -> IO ()) -> BS
{-# INLINE unsafeCreate #-}
unsafeCreate n f = myUnsafePerformIO $ create n f
Dada a extensa linha de informações da sua biblioteca, seria interessante incorporar a IO insegura, para obter o melhor desempenho:
myUnsafePerformIO :: IO a -> a
{-# INLINE myUnsafePerformIO #-}
myUnsafePerformIO (IO m) = case m realWorld# of (# _, r #) -> r
Mas, depois de adicionar uma função de conveniência para gerar bytestrings de singleton:
singleton :: Word8 -> BS
{-# INLINE singleton #-}
singleton x = unsafeCreate 1 (\p -> poke p x)
você pode se surpreender ao descobrir que o seguinte programa é impresso 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
o que é um problema se você espera que dois singletons diferentes usem dois buffers diferentes.
O que está acontecendo de errado aqui é que o inlining extenso significa que as duas mallocForeignPtrBytes 1
chamadas são iniciadas singleton 1
e singleton 2
podem ser lançadas em uma única alocação, com o ponteiro compartilhado entre as duas sequências de caracteres.
Se você remover o inlining de qualquer uma dessas funções, a flutuação será impedida e o programa será impresso False
conforme o esperado. Como alternativa, você pode fazer a seguinte alteração em 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#
substituindo o m realWorld#
aplicativo inline por uma chamada de função não inline paramyRunRW# m = m realWorld#
. Esse é o pedaço mínimo de código que, se não for incorporado, pode impedir que as chamadas de alocação sejam levantadas.
Após essa alteração, o programa será impresso False
conforme o esperado.
Isso é tudo o que muda de inlinePerformIO
(AKA accursedUnutterablePerformIO
) para unsafeDupablePerformIO
faz. m realWorld#
Altera essa chamada de função de uma expressão embutida para uma equivalente não embutida 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#
Exceto que o built-in runRW#
é mágico. Mesmo marcado NOINLINE
, ele é realmente incorporado pelo compilador, mas próximo ao final da compilação após as chamadas de alocação já terem sido impedidas de flutuar.
Portanto, você obtém o benefício de desempenho de ter a unsafeDupablePerformIO
chamada totalmente incorporada, sem o efeito colateral indesejável, permitindo que expressões comuns em diferentes chamadas não seguras sejam transferidas para uma única chamada comum.
Embora, verdade seja dita, há um custo. Quando accursedUnutterablePerformIO
funciona corretamente, pode oferecer um desempenho um pouco melhor, pois há mais oportunidades de otimização se a m realWorld#
chamada puder ser incorporada mais cedo ou mais tarde. Portanto, a bytestring
biblioteca real ainda usa accursedUnutterablePerformIO
internamente em muitos lugares, principalmente onde não há alocação (por exemplo, head
usa-a para espiar o primeiro byte do buffer).
unsafeDupablePerformIO
é mais seguro por algum motivo. Se eu tivesse que adivinhar, provavelmente tem que fazer algo com inlinear e flutuarrunRW#
. Ansioso para alguém dar uma resposta adequada a esta pergunta.