Haskell MVar 和文本惰性比较

Haskell MVar & Text laziness comparison

我使用 normal/lazy 文本和 normal/strict 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 + 普通文本比普通文本 + 普通 MVar 快得多?为什么无论 MVar 的严格程度如何,惰性文本都这么慢?

懒惰与严格的文本

首先,惰性文本就像一个严格文本的链表。 <> 函数遍历整个列表并将其右参数添加到列表的末尾。这意味着惰性文本版本以包含 15000 个元素的链表结束。每次添加元素时,程序都会遍历整个列表,直到到达末尾并可以追加元素。

严格的<>只是将两个内存区域复制到一个新区域。这是一个更便宜的操作,因为它可以利用 SIMD 操作一次复制最多 64 个字符(这比一大块惰性文本还多)。此外,与可能位于内存中任何位置的链表指针相比,这对于缓存局部性要好得多。

然后最终在惰性文本中有很多内存开销,因为它必须为块(相当于 8 个字符)存储一个 header 指向下一个块(8 个字符)的指针和 Text 本身包含切片的长度(8 个字符)和偏移量(8 个字符),最后底层 ByteArray# 有另一个长度(8 个字符)。因此惰性版本将存储相当于每块 20 个字符的 40 个额外字符。

我还应该注意到惰性文本在一个重要方面与严格文本列表不同:严格文本被解压缩到惰性文本块中。解包节省了一定程度的间接性,但它也阻止了块之间的共享。在这种情况下,每个块都包含完全相同的文本,因此它们都可以共享。我会在下一部分再谈这个。

懒惰与严格 MVar

这里并没有具体说明MVar的严格性,只是为了方便。如果您在此处使用 $!,您可能会得到相同的结果:

mvarWriter = \newText -> modifyMVar_ mvar (\oldText -> return $! oldText <> newText)

(或者 $!! 如果你使用惰性文本)

惰性版本和严格版本的区别在于惰性版本在将oldText <> newText放入MVar之前实际上并不计算oldText <> newText。惰性版本将计算推迟到遇到 print . T.length =<< readMVar mvar 行。

(GHC) Haskell 如何存储计算以便它能够 运行 在以后的某个时间点进行计算?作为堆上的闭包。闭包存储一个指向所有源自函数外部的参数(自由变量)的指针。在这种情况下,它只是 newText.

所以实际上,严格的 Text + lazy MVar 版本与 lazy Text 版本非常相似。两者都在堆中构建了一种链表结构。这需要额外的 space、分配时间并增加间接寻址。

与惰性文本相比的一个区别是严格文本 + 惰性 MVar 版本不必在每次添加新文本时遍历整个结构。此外,这种通过闭包的隐式链表的优点是它可以在结构中共享指针。所以一开始只有一个“01234567890123456789”文本和许多指向它的指针。