Performance 如何在空间中填充数据地图&;省时方式
在第一眼看到哈斯克尔四年后回到这里。我总是对它的表现力感到惊讶,也对自己无法预测空间/时间性能感到困惑 作为一个热身,我开始翻译我用C++编写的一个小玩具程序。这是关于拼字游戏中的“作弊”。你输入你的游戏,它输出你可能玩的单词,用你的字母或者在棋盘上画一个字母 整个过程围绕着一个在启动时预装的字典展开。然后这些单词连同它们的字谜一起存储在地图中的列表中。这些键是已排序字母的字符串。举个例子可以更清楚地说明:Performance 如何在空间中填充数据地图&;省时方式,performance,haskell,dictionary,Performance,Haskell,Dictionary,在第一眼看到哈斯克尔四年后回到这里。我总是对它的表现力感到惊讶,也对自己无法预测空间/时间性能感到困惑 作为一个热身,我开始翻译我用C++编写的一个小玩具程序。这是关于拼字游戏中的“作弊”。你输入你的游戏,它输出你可能玩的单词,用你的字母或者在棋盘上画一个字母 整个过程围绕着一个在启动时预装的字典展开。然后这些单词连同它们的字谜一起存储在地图中的列表中。这些键是已排序字母的字符串。举个例子可以更清楚地说明: Key : "AEHPS" Value : ["HEAPS","PHASE","SHA
Key : "AEHPS" Value : ["HEAPS","PHASE","SHAPE"]
< C++版本一次读字典的320000个单词,总共大约200毫秒。生成的数据结构是存储在数组中的哈希映射
,占用大约12兆的内存
Haskell版本在大约5秒钟内读取相同的字典,程序堆大小膨胀到400兆字节!我将Data.Map
中的值类型从[String]
更改为[ByteString]
,以节省一些内存,从而将程序内存消耗降低到大约290兆字节。这仍然是我的C++版本的24倍。这不仅仅是“开销”,即使Data.Map
是一棵树而不是一个数组
所以我认为我做错了什么
整个模块在此处可见:(不推荐的链接)
我想我的问题与Data.Map
在以前版本的基础上以增量方式构建的方式有关?还是数据结构本身?还是别的什么
我会尝试其他解决方案,比如Data.HashMap
,或者用fromListWith
填充Data.Map
。尽管如此,我还是想对这里发生的事情有一些了解。非常感谢您的洞察力
简短答复: 使用Data.Map.Strict,强制计算值元素,并将键存储为bytestring,也创造了将内存占用几乎除以3的奇迹。结果是100Meg,它是标准的“代码> STD::C++中的MultIMAP < /代码>的两倍。不过没有加速。Git已更新
非常感谢所有贡献者,这里有有趣的材料 在我的笔记本电脑上,以下功能在大约一秒钟内就能工作:
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.List
type Dict = M.Map T.Text [T.Text]
newDict = M.empty
addWord:: T.Text -> Dict -> Dict
addWord word dict = M.insertWith (++) (T.pack $ sort $ T.unpack word) [word] dict
loadAnagramsFromFile fileName = do
full <- T.readFile fileName
let ls = T.lines full
return $ foldr addWord newDict lsct ls
导入符合条件的数据。映射为M
导入符合条件的数据。文本为T
将限定的Data.Text.IO作为T导入
导入数据。列表
键入Dict=M.Map T.Text[T.Text]
newDict=M.empty
addWord::T.Text->Dict->Dict
addWord word dict=M.insertWith(++)(T.pack$sort$T.unpack word)[word]dict
loadAnagramsFromFile fileName=do
完整编辑:删除了错误的基准测试和评论,只留下了一点小建议。有关OP问题的直接解决方案,请参见里德·巴顿的答案
如果您不需要在运行时更改字典,那么这几乎是您可以获得的最节省空间的解决方案(至少对于文字游戏而言)
例如,我们可以从您的字典生成并序列化仅占用295 Kb空间的DAWG,并支持非常高效的查找和前缀匹配:
import qualified Data.DAWG.Packed as D -- from my "packed-dawg" package
main = do
words <- lines <$> readFile "dict.txt"
D.toFile "dict.dawg" $ D.fromList words -- serialize as "dict.dawg"
import qualified Data.DAWG.Packed as D--从我的“Packed DAWG”包
main=do
单词您正在犯的一个尚未指出的错误是,您正在地图中的值列表中存储B.pack word
形式的未计算thunk。这意味着您在构建映射期间基本上以低效的字符串格式保留了整个输入文件,而输入文件中每个字符的成本为24字节。这里使用Data.Map.Strict
API没有什么区别,因为该API中的函数只强制映射的元素为弱头范式,这对于列表意味着只评估最外层的构造函数是[]
还是(:)
,而不评估列表的任何元素
您可以做的另一个改进是使用bytestring最新版本中可用的类型(GHC7.8附带的类型已经足够新了)。这是专门为在存储许多short ByTestRing时最小化内存使用而设计的,其折衷是ShortByteString
上的大多数操作都需要一个副本
András Kovács的地图示例代码如下所示:
{-# LANGUAGE BangPatterns #-}
import Control.Applicative
import Data.List
import qualified Data.Map.Strict as M
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Short as B (ShortByteString, toShort, fromShort)
shortPack = B.toShort . B.pack
main = do
words <- lines <$> readFile "dict.txt"
print $ M.size $
M.fromListWith (++) $ map (\w -> let !x = shortPack w in (shortPack $ sort w, [x])) words
{-#语言模式}
导入控制
导入数据。列表
导入符合条件的Data.Map.Strict作为M
将限定数据.ByteString.Char8作为B导入
将限定数据.ByteString.Short作为B导入(ShortByteString、ToSort、fromShort)
shortPack=B.toShort。B.包装
main=do
让我说吧!x=短包w in(短包$sort w,[x])字
每一项更改都会在我的测试中节省大约30%的最大驻留时间,总共节省50%以上的空间使用。此外,您使用ByteString进行存储,但您首先将每一行读入一个字符串。使用ByteString文件操作直接读取字节字符串。或者更好,使用文本,因为您有文本数据,并且文本还提供文件IO操作。据我所知,没有很好的理由在文件上启用行缓冲。还有,您使用M.insertWith(++)(sort word)[B.pack word]dict
而不是let!packed=B.在调整(packed:)(排序字)dict中打包字
?我不知道在这种情况下,adjust
是否会更有效,但至少它似乎能更好地传达意图。没有特别的原因。这是我在Haskell的第二个程序,超过5行(第一个是四年前),我仍然对库和习惯用法完全不熟悉由于拼字只需要ASCII码的一小部分(加上空格)文本
对我来说似乎有些过分了