Apa perbedaan antara unformablePerformIO dan accuredUnutterablePerformIO?

13

Saya berkeliaran di Bagian Terbatas dari Perpustakaan Haskell dan menemukan dua mantra keji ini:

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

Perbedaan sebenarnya tampaknya hanya antara runRW#dan ($ realWorld#), bagaimanapun. Saya memiliki beberapa ide dasar tentang apa yang mereka lakukan, tetapi saya tidak mendapatkan konsekuensi nyata dari penggunaan satu sama lain. Bisakah seseorang menjelaskan kepada saya apa bedanya?

Radrow
sumber
3
unsafeDupablePerformIOkarena beberapa alasan lebih aman. Jika saya harus menebak itu mungkin harus melakukan sesuatu dengan inlining dan melayang keluar runRW#. Menantikan seseorang yang memberikan jawaban yang tepat untuk pertanyaan ini.
lehins

Jawaban:

11

Pertimbangkan perpustakaan bytestring yang disederhanakan. Anda mungkin memiliki tipe string byte yang terdiri dari panjang dan buffer byte yang dialokasikan:

data BS = BS !Int !(ForeignPtr Word8)

Untuk membuat bytestring, Anda biasanya perlu menggunakan tindakan IO:

create :: Int -> (Ptr Word8 -> IO ()) -> IO BS
{-# INLINE create #-}
create n f = do
  p <- mallocForeignPtrBytes n
  withForeignPtr p $ f
  return $ BS n p

Namun, tidak mudah bekerja di IO monad, jadi Anda mungkin tergoda untuk melakukan sedikit IO yang tidak aman:

unsafeCreate :: Int -> (Ptr Word8 -> IO ()) -> BS
{-# INLINE unsafeCreate #-}
unsafeCreate n f = myUnsafePerformIO $ create n f

Dengan adanya inlining ekstensif di perpustakaan Anda, alangkah baiknya untuk menyejajarkan IO yang tidak aman, untuk kinerja terbaik:

myUnsafePerformIO :: IO a -> a
{-# INLINE myUnsafePerformIO #-}
myUnsafePerformIO (IO m) = case m realWorld# of (# _, r #) -> r

Tapi, setelah Anda menambahkan fungsi kenyamanan untuk menghasilkan bytestrings singleton:

singleton :: Word8 -> BS
{-# INLINE singleton #-}
singleton x = unsafeCreate 1 (\p -> poke p x)

Anda mungkin terkejut menemukan bahwa program berikut dicetak 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

yang merupakan masalah jika Anda mengharapkan dua lajang yang berbeda untuk menggunakan dua buffer yang berbeda.

Apa yang salah di sini adalah bahwa inlining yang luas berarti bahwa kedua mallocForeignPtrBytes 1panggilan masuk singleton 1dan singleton 2dapat dialihkan ke dalam alokasi tunggal, dengan pointer dibagi antara dua bytestrings.

Jika Anda menghapus inlining dari salah satu fungsi ini, maka pengapungan akan dicegah, dan program akan mencetak Falseseperti yang diharapkan. Atau, Anda dapat melakukan perubahan berikut untuk 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#

mengganti m realWorld#aplikasi inline dengan panggilan fungsi non-inline ke myRunRW# m = m realWorld#. Ini adalah potongan minimal kode yang, jika tidak inline, dapat mencegah panggilan alokasi tidak diangkat.

Setelah perubahan ini, program akan mencetak Falseseperti yang diharapkan.

Ini semua yang beralih dari inlinePerformIO(AKA accursedUnutterablePerformIO) ke unsafeDupablePerformIOlakukan. Ini mengubah pemanggilan fungsi m realWorld#dari ekspresi inline ke non-inline yang setara 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#

Kecuali, built-in runRW#adalah sihir. Meskipun itu ditandai NOINLINE, itu adalah benar-benar inline oleh compiler, tapi dekat akhir kompilasi setelah panggilan alokasi telah dicegah mengambang.

Jadi, Anda mendapatkan manfaat kinerja memiliki unsafeDupablePerformIOpanggilan yang sepenuhnya diuraikan tanpa efek samping yang tidak diinginkan dari yang memungkinkan ekspresi umum dalam panggilan yang tidak aman yang berbeda untuk dialihkan ke panggilan tunggal yang umum.

Padahal, jujur ​​saja, ada biaya. Ketika accursedUnutterablePerformIObekerja dengan benar, ini berpotensi memberikan kinerja yang sedikit lebih baik karena ada lebih banyak peluang untuk optimasi jika m realWorld#panggilan dapat diuraikan lebih awal daripada nanti. Jadi, bytestringperpustakaan aktual masih menggunakan secara accursedUnutterablePerformIOinternal di banyak tempat, khususnya di mana tidak ada alokasi yang terjadi (misalnya, headmenggunakannya untuk mengintip byte pertama dari buffer).

KA Buhr
sumber