Qual é a diferença entre unsafeDupablePerformIO e accursedUnutterablePerformIO?

13

Eu estava vagando na Seção Restrita da Biblioteca Haskell e encontrei esses dois feitiços vis:

{- 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

A diferença real parece ser apenas entre runRW#e ($ realWorld#), no entanto. Tenho uma idéia básica do que eles estão fazendo, mas não tenho as reais consequências de usar um sobre o outro. Alguém poderia me explicar qual é a diferença?

radrow
fonte
3
unsafeDupablePerformIOé mais seguro por algum motivo. Se eu tivesse que adivinhar, provavelmente tem que fazer algo com inlinear e flutuar runRW#. Ansioso para alguém dar uma resposta adequada a esta pergunta.
lehins 03/04

Respostas:

11

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 1chamadas são iniciadas singleton 1e singleton 2podem 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 Falseconforme 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 Falseconforme o esperado.

Isso é tudo o que muda de inlinePerformIO(AKA accursedUnutterablePerformIO) para unsafeDupablePerformIOfaz. 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 unsafeDupablePerformIOchamada 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 accursedUnutterablePerformIOfunciona 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 bytestringbiblioteca real ainda usa accursedUnutterablePerformIOinternamente em muitos lugares, principalmente onde não há alocação (por exemplo, headusa-a para espiar o primeiro byte do buffer).

KA Buhr
fonte