Sự khác biệt giữa unsafeDupablePerformIO và accursedUnutterablePerformIO là gì?


13

Tôi đang lang thang trong Khu vực hạn chế của Thư viện Haskell và tìm thấy hai phép thuật hèn hạ này:

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

Sự khác biệt thực tế dường như chỉ là giữa runRW#($ realWorld#), tuy nhiên. Tôi có một số ý tưởng cơ bản về những gì họ đang làm, nhưng tôi không nhận được hậu quả thực sự của việc sử dụng cái này hơn cái khác. Ai đó có thể giải thích cho tôi sự khác biệt là gì?


3
unsafeDupablePerformIOlà an toàn hơn cho một số lý do. Nếu tôi phải đoán thì có lẽ phải làm gì đó với nội tuyến và nổi runRW#. Mong được ai đó đưa ra một câu trả lời thích hợp cho câu hỏi này.
lehins

Câu trả lời:


11

Hãy xem xét một thư viện bytestring đơn giản hóa. Bạn có thể có một loại chuỗi byte bao gồm độ dài và bộ đệm byte được phân bổ:

data BS = BS !Int !(ForeignPtr Word8)

Để tạo một bytestring, bạn thường cần sử dụng hành động IO:

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

Tuy nhiên, nó không thuận tiện khi làm việc trong đơn vị IO, vì vậy bạn có thể bị cám dỗ để làm một chút IO không an toàn:

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

Với nội tuyến rộng rãi trong thư viện của bạn, thật tuyệt khi đặt nội tuyến IO không an toàn, để có hiệu suất tốt nhất:

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

Nhưng, sau khi bạn thêm một chức năng tiện lợi để tạo các chuỗi đơn lẻ:

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

bạn có thể ngạc nhiên khi phát hiện ra rằng các chương trình sau in 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

đó là một vấn đề nếu bạn mong đợi hai singletons khác nhau sử dụng hai bộ đệm khác nhau.

Điều sai ở đây là nội tuyến mở rộng có nghĩa là hai mallocForeignPtrBytes 1cuộc gọi vào singleton 1singleton 2có thể được thả ra thành một phân bổ duy nhất, với con trỏ được chia sẻ giữa hai chuỗi phụ.

Nếu bạn đã loại bỏ nội tuyến khỏi bất kỳ chức năng nào trong số này, thì việc thả nổi sẽ bị ngăn chặn và chương trình sẽ in Falsenhư mong đợi. Ngoài ra, bạn có thể thực hiện thay đổi sau đây để 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#

thay thế m realWorld#ứng dụng nội tuyến bằng lệnh gọi hàm không nội tuyến tới myRunRW# m = m realWorld#. Đây là đoạn mã tối thiểu, nếu không được nội tuyến, có thể ngăn các cuộc gọi phân bổ được dỡ bỏ.

Sau khi thay đổi, chương trình sẽ in Falsenhư mong đợi.

Đây là tất cả những gì chuyển từ inlinePerformIO(AKA accursedUnutterablePerformIO) sang unsafeDupablePerformIO. Nó thay đổi chức năng đó gọi m realWorld#từ một biểu thức được nội tuyến thành một biểu đồ không tương đương 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#

Ngoại trừ, tích hợp runRW#là ma thuật. Mặc dù nó được đánh dấu NOINLINE, nó được thực inlined bởi trình biên dịch, nhưng gần cuối biên soạn sau khi các cuộc gọi phân bổ đã được ngăn chặn từ nổi.

Vì vậy, bạn có được lợi ích hiệu suất của việc thực hiện unsafeDupablePerformIOcuộc gọi hoàn toàn mà không có tác dụng phụ không mong muốn của nội tuyến đó cho phép các biểu thức chung trong các cuộc gọi không an toàn khác nhau được chuyển sang một cuộc gọi chung.

Mặc dù, sự thật được nói, có một chi phí. Khi accursedUnutterablePerformIOhoạt động chính xác, nó có khả năng mang lại hiệu suất tốt hơn một chút vì có nhiều cơ hội tối ưu hóa hơn nếu m realWorld#cuộc gọi có thể được thực hiện sớm hơn thay vì muộn hơn. Vì vậy, bytestringthư viện thực tế vẫn sử dụng accursedUnutterablePerformIOnội bộ ở nhiều nơi, đặc biệt là không có phân bổ đang diễn ra (ví dụ: headsử dụng nó để xem trộm byte đầu tiên của bộ đệm).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.