我用正常/惰性文本和正常/嚴格 MVar 的不同變體對以下代碼示例進行了基準測驗:
import Control.Concurrent.MVar
import qualified Data.Text as T
main :: IO ()
main = do
mvar <- newMVar T.empty
let textArr = map (const $ T.pack "01234567890123456789") [0 .. 15000 :: Int]
mvarWriter = \newText -> modifyMVar_ mvar (\oldText -> return $ oldText <> newText)
mapM_ mvarWriter textArr
print . T.length =<< readMVar mvar
| Version | Execution time in seconds |
| ------------------ | ------------------------- |
| Text Strict MVar | 0.26 |
| Text MVar | 8.35 |
| Lazy Text Strict MVar | 17 |
| Lazy Text MVar | 17 |
在閱讀了一些關于此的文章后,我會認為惰性文本 嚴格 MVar 會是最快的,但令我驚訝的是它不是。
誰能解釋發生了什么?為什么嚴格的 MVar 普通文本比普通 Text 普通 MVar 快這么多?無論 MVar 的嚴格程度如何,為什么惰性文本如此緩慢?
uj5u.com熱心網友回復:
惰性文本與嚴格文本
首先,惰性文本就像一個嚴格文本的鏈表。該<>
函式遍歷整個串列并將其右引數添加到串列末尾。這意味著惰性文本版本最終會得到一個包含 15000 個元素的鏈表。每次添加元素時,程式都會遍歷整個串列,直到它到達末尾并可以追加元素。
嚴格<>
只是將兩個記憶體區域復制到一個新區域。這是一個更便宜的操作,因為這可以利用 SIMD 操作一次復制多達 64 個字符(這不僅僅是一大塊惰性文本)。此外,與可能位于記憶體中任何位置的鏈表指標相比,這對于快取區域性要好得多。
然后最后在惰性文本中有很多記憶體開銷,因為它必須存盤塊的標題(相當于 8 個字符)指向下一個塊(8 個字符)的指標,并且Text
它本身包含長度(8 個字符)和用于切片的偏移量(8 個字符),最后底層ByteArray#
有另一個長度(8 個字符)。所以惰性版本將存盤相當于每塊 20 個字符的 40 個額外字符。
我還應該注意,惰性文本在一個重要方面與嚴格文本串列不同:將嚴格文本解壓縮到惰性文本塊中。這種解包節省了一定程度的間接性,但它也阻止了塊之間的共享。在這種情況下,每個塊都包含完全相同的文本,因此它們都可以共享。我將在下一部分回到這個話題。
惰性與嚴格 MVar
這里并不是特別針對 MVar 的嚴格性,這只是為了方便。如果在這里使用,您可能會得到相同的結果$!
:
mvarWriter = \newText -> modifyMVar_ mvar (\oldText -> return $! oldText <> newText)
(或者$!!
如果你使用惰性文本)
惰性版本和嚴格版本的區別在于惰性版本oldText <> newText
在放入MVar
. 惰性版本會推遲該計算,直到遇到該print . T.length =<< readMVar mvar
行。
(GHC) Haskell 如何存盤計算,以便它能夠在以后的時間點運行它?作為堆上的閉包。閉包存盤一個指標,指向源自函式外部的所有引數(自由變數)。在這種情況下,這只是newText
.
所以實際上,嚴格的 Text 惰性 MVar 版本與惰性文本版本非常相似。兩者都在堆中構造了一種鏈表結構。這需要額外的空間、分配時間并增加間接性。
與惰性文本相比的一個區別是,嚴格文本 惰性 MVar 版本每次添加新文本時不必遍歷整個結構。此外,這種通過閉包的隱式鏈表具有可以在結構中共享指標的優點。所以一開始只會有一個“01234567890123456789”文本和許多指向它的指標。
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/478653.html