我有一些ASCII文件,总共包含大约1700万行,并且在每个/大多数行中是一个固定的36字节标识符。所以我的数据是矩形的:我有很多固定宽度的行。使用Haskell,我想读取所有行,使用正则表达式提取标识符(我很好),然后对它们进行排序并计算唯一标识符的数量(非常接近grep | sort | uniq
)。 (我已经通过并行读取每个文件来进行并行化。)听起来像一个简单的问题,但是......
即使在排序阶段之前,我发现很难从这个问题中获得不错的表现。据我所知。 String
对于36字节的ASCII来说是过度的,所以我想到使用ByteString
。但是一个大小为1700万的(链接)列表似乎是一个坏主意,所以我尝试了IOVector ByteString
。但这似乎很慢。我相信垃圾收集正在受到影响,因为我在向量中保留了数百万个小字节:GC至少占代码的3倍(根据+RTS -s
)并且我认为它只会随着程序变得更糟继续跑步。
我在想我应该使用Repa或某种单一巨型ByteString
/ IOVector Char8
/其他(因为我知道每行的确切宽度为36)将数据存储在一个每个线程的大规模矩形数组,这应该可以缓解GC问题。但是,我仍然需要事后对行进行排序,而Repa似乎不支持排序,我不想自己编写排序算法。所以我不知道如何拥有一个巨大的矩形阵列,但仍然对它进行排序。
建议使用库,设置GC标志还是其他什么?这台机器有24个内核和24GB内存,因此我不受硬件限制。我想留在Haskell,因为我有很多相关的代码(也解析相同的文件和生成汇总统计信息),工作正常,我不想重写它。
答案 0 :(得分:1)
我相信垃圾收集正在受到影响,因为我在向量中保留了数百万个小字节串
可疑。不应收集保留ByteStrings。也许在代码中的某处有过多的数据复制?
ByteString
是标头(8字节)+ ForeignPtr Word8
ref(8字节)+ Int
偏移量(4字节)+ Int
长度(4字节) )
ForeignPtr
是一个标头(8个字节)+ Addr#
(8个字节)+ PlainPtr
ref(8个字节)
PlainPtr
是一个标题(8个字节)+ MutableByteArray#
ref(8个字节)
(根据https://stackoverflow.com/a/3256825/648955修订)
总而言之,ByteString
开销至少 64字节(纠正我,某些字段是共享的)。
编写自己的数据管理:大扁平Word8
数组和adhoc偏移包装
newtype ByteId = ByteId { offset :: Word64 }
使用Ord
个实例。
每个标识符的开销为 8字节。在未装箱的Vector
中存储偏移量。排序方式如下:http://hackage.haskell.org/packages/archive/vector-algorithms/0.5.4.2/doc/html/Data-Vector-Algorithms-Intro.html#v:sort
答案 1 :(得分:0)
Array
类型系列内置支持多维数组。索引可以是具有Ix
实例的任何内容,特别是对于您的案例(Int, Int)
。不幸的是,它也不支持排序。
但是对于您的用例,您真的需要排序吗?如果您有从标识符到Int的映射,您可以随时增加计数,然后选择值为1的所有键。您可以查看bytestring-trie
包,但是对于您的用例,它建议使用{ {3}}
另一种算法是携带两个集合(例如HashSet),一个标识符只能看到一次,一个标识符可以看到多次,并且在您浏览列表时更新这些集合。
另外,你如何读取你的文件:如果你把它读作一个大的ByteString并小心地从它构造小的ByteString对象,它们实际上只是指向大文件的大块内存,可能会消除你的GC问题。但要评估我们需要查看您的代码。
答案 2 :(得分:0)
mmap周围有几个包装器可以为你的文件中的数据提供Ptrs或者提供一个大的ByteString。 ByteString实际上只是一个指针,偏移量,长度元组;将那个大的ByteString拆分成一堆小的实际上只是制作了一堆指向大的子集的新元组。由于你说每个记录都在文件中的固定偏移量,你应该能够通过ByteString的take
创建一堆新记录而不实际访问任何文件。
我对问题的排序部分没有任何好的建议,但是首先避免复制文件数据应该是一个好的开始。
答案 3 :(得分:0)
特里应该有效。在具有4 GB RAM的双核笔记本电脑上,此代码在一个1800万行的文件,600万个唯一密钥上需要45分钟:
--invoked: test.exe +RTS -K3.9G -c -h
import qualified Data.ByteString.Char8 as B
import qualified Data.Trie as T
file = "data.txt"
main = ret >>= print
ret = do -- retreive the data
ls <- B.readFile file >>= return.B.lines
trie <- return $ tupleUp ls
return $ T.size trie
tupleUp:: [B.ByteString] -> T.Trie Int
tupleUp l = foldl f T.empty l
f acc str = case T.lookup str acc
of Nothing -> T.insert str 1 acc
Just n -> T.adjust (+1) str acc
这是用于生成数据文件的代码(6MM密钥,然后3个副本到1个文件中以获取18MM密钥:
import qualified Data.ByteString.Char8 as BS
import System.Random
import Data.List.Split
file = "data.txt"
numLines = 6e6 --17e6
chunkSize = 36
charSet = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9']
-- generate the file
gen = do
randgen <- getStdGen
dat <- return $ t randgen
writeFile file (unlines dat)
t gen = take (ceiling numLines) $ charChunks
where
charChunks = chunksOf chunkSize chars
chars = map (charSet!!) rands
rands = randomRs (0,(length charSet) -1) gen
main = gen
答案 4 :(得分:0)
那么,我们有多快?让我们用@ ja。代码生成的文件做一些测试:
cat data.txt > /dev/null
--> 0.17 seconds
在Haskell中也一样吗?
import qualified Data.ByteString as B
f = id
main = B.readFile "data.txt" >>= return . f >>= B.putStr
时序?
time ./Test > /dev/null
--> 0.32 seconds
需要两倍的时间,但我想这不是太糟糕。使用严格的字节串因为 我们想在一秒钟内把它搞定。
接下来,我们可以使用Vector
还是太慢?让我们构建一个Vector
个块,然后再将它们重新组合在一起。我使用blaze-builder
包来优化输出。
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import qualified Data.Vector as V
import qualified Blaze.ByteString.Builder as BB
import Data.Monoid
recordLen = 36
lineEndingLen = 2 -- Windows! change to 1 for Unix
numRecords = (`div` (recordLen + lineEndingLen)) . B.length
substr idx len = B.take len . B.drop idx
recordByIdx idx = substr (idx*(recordLen+lineEndingLen)) recordLen
mkVector :: B.ByteString -> V.Vector (B.ByteString)
mkVector bs = V.generate (numRecords bs) (\i -> recordByIdx i bs)
mkBS :: V.Vector (B.ByteString) -> L.ByteString
mkBS = BB.toLazyByteString . V.foldr foldToBS mempty
where foldToBS :: B.ByteString -> BB.Builder -> BB.Builder
foldToBS = mappend . BB.fromWrite . BB.writeByteString
main = B.readFile "data.txt" >>= return . mkBS . mkVector >>= L.putStr
需要多长时间?
time ./Test2 > /dev/null
--> 1.06 seconds
一点都不差!即使你使用正则表达式来读取行而不是我固定的块位置,我们仍然可以得出结论,你可以将你的块放在Vector
中而没有严重的性能命中。
还剩下什么?排序。从理论上讲,铲斗分类应该是解决此类问题的理想算法。我试着自己实现一个:
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import qualified Data.Vector as V
import qualified Data.Vector.Generic.Mutable as MV
import qualified Blaze.ByteString.Builder as BB
import Data.Monoid
import Control.Monad.ST
import Control.Monad.Primitive
recordLen = 36
lineEndingLen = 2 -- Windows! change to 1 for Unix
numRecords = (`div` (recordLen + lineEndingLen)) . B.length
substr idx len = B.take len . B.drop idx
recordByIdx idx = substr (idx*(recordLen+lineEndingLen)) (recordLen+lineEndingLen)
mkVector :: B.ByteString -> V.Vector (B.ByteString)
mkVector bs = V.generate (numRecords bs) (\i -> recordByIdx i bs)
mkBS :: V.Vector (B.ByteString) -> L.ByteString
mkBS = BB.toLazyByteString . V.foldr foldToBS mempty
where foldToBS :: B.ByteString -> BB.Builder -> BB.Builder
foldToBS = mappend . BB.fromWrite . BB.writeByteString
bucketSort :: Int -> V.Vector B.ByteString -> V.Vector B.ByteString
bucketSort chunkSize v = runST $ emptyBuckets >>= \bs ->
go v bs lastIdx (chunkSize - 1)
where lastIdx = V.length v - 1
emptyBuckets :: ST s (V.MVector (PrimState (ST s)) [B.ByteString])
emptyBuckets = V.thaw $ V.generate 256 (const [])
go :: V.Vector B.ByteString ->
V.MVector (PrimState (ST s)) [B.ByteString] ->
Int -> Int -> ST s (V.Vector B.ByteString)
go v _ _ (-1) = return v
go _ buckets (-1) testIdx = do
v' <- unbucket buckets
bs <- emptyBuckets
go v' bs lastIdx (testIdx - 1)
go v buckets idx testIdx = do
let testChunk = v V.! idx
testByte = fromIntegral $ testChunk `B.index` testIdx
b <- MV.read buckets testByte
MV.write buckets testByte (testChunk:b)
go v buckets (idx-1) testIdx
unbucket :: V.MVector (PrimState (ST s)) [B.ByteString] ->
ST s (V.Vector B.ByteString)
unbucket v = do
v' <- V.freeze v
return . V.fromList . concat . V.toList $ v'
main = B.readFile "data.txt" >>= return . process >>= L.putStr
where process = mkBS .
bucketSort (recordLen) .
mkVector
测试时间约为1:50分钟,这可能是可以接受的。我们讨论的是一种O(c * n)算法,其中n在几百万的范围内,而常数c为36 *。但我相信你可以进一步优化它。
或者您可以使用vector-algorithms
包。使用堆排序进行测试:
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import qualified Data.Vector as V
import qualified Blaze.ByteString.Builder as BB
import Data.Vector.Algorithms.Heap (sort)
import Data.Monoid
import Control.Monad.ST
recordLen = 36
lineEndingLen = 2 -- Windows! change to 1 for Unix
numRecords = (`div` (recordLen + lineEndingLen)) . B.length
substr idx len = B.take len . B.drop idx
recordByIdx idx = substr (idx*(recordLen+lineEndingLen)) (recordLen+lineEndingLen)
mkVector :: B.ByteString -> V.Vector (B.ByteString)
mkVector bs = V.generate (numRecords bs) (\i -> recordByIdx i bs)
mkBS :: V.Vector (B.ByteString) -> L.ByteString
mkBS = BB.toLazyByteString . V.foldr foldToBS mempty
where foldToBS :: B.ByteString -> BB.Builder -> BB.Builder
foldToBS = mappend . BB.fromWrite . BB.writeByteString
sortIt v = runST $ do
mv <- V.thaw v
sort mv
V.freeze mv
main = B.readFile "data.txt" >>= return . process >>= L.putStr
where process = mkBS .
sortIt .
mkVector
这可以在我的机器上大约1:20分钟完成工作,所以现在它比我的水桶排序更快。两种最终解决方案都占用了1-1.2 GB RAM的范围。
够好吗?