在Haskell中存储和排序矩形数据的最佳方法是什么?

时间:2013-08-22 13:48:52

标签: arrays performance haskell bytestring repa

我有一些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,因为我有很多相关的代码(也解析相同的文件和生成汇总统计信息),工作正常,我不想重写它。

5 个答案:

答案 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的范围。

够好吗?