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 1và singleton 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).
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ổirunRW#. Mong được ai đó đưa ra một câu trả lời thích hợp cho câu hỏi này.