Haskell 中的内存高效字符串

Posted

技术标签:

【中文标题】Haskell 中的内存高效字符串【英文标题】:Memory efficient strings in Haskell 【发布时间】:2012-02-22 16:14:25 【问题描述】:

通常推荐的 Haskell 字符串类型似乎是 ByteString 或 Text。我经常使用大量的短(英文单词大小)字符串,并且通常需要将它们存储在 Data.Map 等查找表中。在许多情况下,我发现在这种情况下,字符串表占用的内存比字节字符串表要少。 Word8 的 Unboxed Data.Vectors 也(很多)比 ByteStrings 紧凑。

当需要在 Haskell 中存储和比较大量小字符串时,最佳实践是什么?

下面我尝试将一个特定的有问题的案例浓缩成一个小例子:

import qualified Data.ByteString.Lazy.Char8 as S
import qualified Data.ByteString as Strict
import qualified Data.Map as Map
import qualified Data.Vector.Unboxed as U
import qualified Data.Serialize as Serialize
import Control.Monad.State

main =   putStr 
  . unlines . map show . flip evalState (0,Map.empty) 
  . mapM toInt 
  . S.words
  =<<
  S.getContents


toInt x = do  
  let x' =   
          U.fromList . Strict.unpack .  -- Comment this line to increase memory usage
           Serialize.encode $ x  
  (i,t) <- get
  case Map.lookup x' t of
    Just j -> return j
    Nothing -> do 
      let i' = i + (1::Int)
      put (i', Map.insert x' i t)
      return i

当我在一个包含大约 400.000 个英文文本的文件上运行此程序时,带有严格字节串键的版本使用大约 50MB 内存,带有 Word8 向量的版本使用 6MB。

【问题讨论】:

您能否举一些代码示例,其中 ByteStrings 比 Strings 占用更多内存或比 Word8 向量“多得多”的内存?我不明白为什么会这样,除非你在做一些奇怪的事情。 @shang:如果您错误地将充满严格 ByteStrings 的映射的大小与包含字符串 thunk 的映射进行比较,我可以想象会发生这种情况。虽然更多细节会有所帮助。演示该问题的简短测试程序会特别好。 @hammar:是的,这是一种选择。另一个可能是您正在从一个大的 ByteString 中分割单词并保留对它的引用。 你可能想看看Data.Trie。 另见短字节串hackage.haskell.org/package/bytestring-0.10.4.0/docs/… 【参考方案1】:

在没有其他答案的情况下,我将在这里冒险。

当需要在 Haskell 中存储和比较大量小字符串时,最佳实践是什么?

如果小字符串是人类可读的(例如英文单词),则使用Text。如果它们只能由计算机读取,请使用ByteString。使用严格或惰性变体的决定取决于您如何构建和使用这些小字符串。

您不需要使用自己未装箱的Vectors 或Word8。如果您遇到常规StringTextByteString 快的特定情况,请在 *** 上提供详细信息,我们将尝试找出原因。如果您进行详细分析并可以证明Word8 的未装箱Vector 始终比TextByteString 工作得更好,然后开始在邮件列表、irc、reddit 等上进行对话;标准库不是一成不变的,总是欢迎改进。

但我认为你很可能只是在做一些奇怪的事情,正如 hammar 和 shang 所暗示的那样。

附:对于您的特定用例,您应该考虑更合适的数据结构来满足您的需求,而不是存储大量小字符串,例如danr 建议的 Trie。

【讨论】:

short 字符串进行排序是常规String 性能优于ByteString 的一个地方(我不知道Text,但我不会如果String 也能胜任这项任务,我会感到惊讶)。为什么这是显而易见的:ByteString 使用计数排序。【参考方案2】:

一个(严格的)ByteSting 是一个构造函数,将一个未装箱的 ForiegnPtr 转换为一个 Word8 和两个未装箱的 Ints。

ForeignPtrAddr#(GHC prim)和ForeignPtrContents 之上的另一个构造函数:

data ForeignPtrContents
  = PlainForeignPtr !(IORef (Finalizers, [IO ()]))
  | MallocPtr      (MutableByteArray# RealWorld) !(IORef (Finalizers, [IO ()]))
  | PlainPtr       (MutableByteArray# RealWorld)

...

对于短字符串,ByteStrings 只是打包了太多的管理,以使其对实际“字符串”数据的连续表示受益。

对于最初的问题 - 我会检查您的语料库的平均字长,但我看不出 ByteString 比 String aka [Char] 更有效,每个 Char 使用 12 个字节(来源原始 ByteString 论文)。

对 Haskellers 的一般请求(不是针对原始问题的发布者) - 请停止抨击 String aka [Char] - 同时拥有 String 和 Text(以及当你真正需要字节时的 ByteString)是有道理的。或者在连续字符串表示更适合短字符串的地方使用 Clean。

警告 - 我可能一直在查看旧版本的 ByteString 内部结构,了解它在内部使用的数据类型。

【讨论】:

【参考方案3】:

我知道这是一篇已有 6 年历史的帖子,但我最近也有同样的疑惑,发现这篇有用的博文:https://markkarpov.com/post/short-bs-and-text.html。看来是的,这是一个公认的问题,Short(Text/ByteString) 是解决方案。

【讨论】:

以上是关于Haskell 中的内存高效字符串的主要内容,如果未能解决你的问题,请参考以下文章

需要高效的内存方式来存储大量字符串(以前是:Java 中的 HAT-Trie 实现)

Haskell 对字符串中的字符进行递归

基于 Haskell 中的字符串映射证明打印函数的穷举性

haskell 中的前缀列表

Haskell“字符串移动”函数

具有haskell中的多参数函数的延迟过滤器