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 1
cuộc gọi vào singleton 1
và singleton 2
có 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 False
như 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 False
như 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 unsafeDupablePerformIO
cuộ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 accursedUnutterablePerformIO
hoạ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, bytestring
thư viện thực tế vẫn sử dụng accursedUnutterablePerformIO
nội bộ ở nhiều nơi, đặc biệt là không có phân bổ đang diễn ra (ví dụ: head
sử dụng nó để xem trộm byte đầu tiên của bộ đệm).
unsafeDupablePerformIO
là 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.