【发布时间】:2014-07-15 01:56:09
【问题描述】:
我将在下面留下对问题的描述,但我正在编辑所有不相关的部分。
1) 多亏了 dfeuer,我已经能够从这个程序中减少几乎整整一秒钟的时间。我从 10.1 降到了 9.2 秒。
2) 感谢 SO 用户 applicative posted 的修改,我能够将时间缩短到 7.0 秒,从而使 IO 变得非常高效。
3) 感谢发现 here 的不同版本的 groupBy,我现在是 6.2 秒。
4) 构建器输出的重新实现,现在为 5.8 秒。
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy.Char8 as DB
import qualified Data.Function as DF
import Control.Applicative
import Data.Monoid
import System.IO
groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
groupBy rel [] = []
groupBy rel (x:xs) = (x:ys) : groupBy rel zs
where (ys,zs) = groupByAux x xs
groupByAux x0 (x:xs)
| rel x0 x = (x:ys, zs)
where (ys,zs) = groupByAux x xs
groupByAux y xs = ([], xs)
filterLengthBy :: (Int -> Bool) -> [[a]] -> [[a]]
filterLengthBy fn = filter (flb fn)
where flb fn xs = fn $ length xs
buildOutput x y = BB.intDec x <> BB.char7 ' ' <> BB.intDec y <> BB.char7 '\n'
tensAndHundreds :: [[[Int]]] -> [BB.Builder]
tensAndHundreds [] = []
tensAndHundreds (x:xs)
| length x /= 10 = tens x ++ tensAndHundreds xs
| otherwise = buildOutput (head $ head x) 100 : tensAndHundreds xs
tens :: [[Int]] -> [BB.Builder]
tens = foldr (\x acc -> buildOutput (head x) 10 : acc) []
dbIntVals :: DB.ByteString -> [Int]
dbIntVals xs =
case (DB.readInt xs) of
Just (x', xs') -> x' : dbIntVals (DB.tail xs')
Nothing -> if xs == DB.empty
then []
else dbIntVals (DB.tail xs)
generateResults :: [Int] -> [BB.Builder]
generateResults xs = tensAndHundreds
$ groupBy ((==) `DF.on` (`quot` 100) . head)
$ filterLengthBy (== 10)
$ groupBy ((==) `DF.on` (`quot` 10)) xs
displayResults :: [BB.Builder] -> IO ()
displayResults xs = BB.hPutBuilder stdout $ mconcat xs
main :: IO ()
main = DB.getContents >>=
displayResults . generateResults . dbIntVals
我在原帖底部的两个问题是:
鉴于我在上面发布的方法,我是否遗漏了什么可以提高该程序的效率并因此提高该程序的性能?空间分配是最终的瓶颈吗?换句话说,groupBy 方法是否正常运行?
我还有什么其他方法可以解决这个问题,既能提高效率,又能避免传统意义上的计数器?我还没有体验过 Haskell 的高级功能,但请随时提供指点。
这是原来的问题。我留下了代码的第一个版本,但删除了所有分析器的东西
我(再次)开始学习 Haskell,并决定尝试一些我发现的随机问题。我从 Arch Linux 论坛解决的一个问题是这样的:
对于从 0 开始的 100 的每个潜在子范围(0-99、100-199、1300-1399 等),如果该子范围有所有 100 个元素,打印 head 值后跟 100:
0 100
145600 100
否则,对于从 0(0-9、10-19、15430-15439 等)开始的同一范围内的每个连续 10 组,打印该范围的头部,然后打印 10:
30 10
70 10
145620 10
145650 10
一个小型测试集的示例输出如下所示:
0 10
19000 100
20320 10
54540 100
我的主测试文件有 56,653,371 个 int,其中有 290,197 组 10 组和 4 组 100 组。测试机有 AMD 1090T 处理器和 8 GB RAM,在 64 位 Linux 上运行 GHC 7.8.3 .唯一使用的编译器标志是“-O2 -fflvm”。
有一件重要的事情需要注意:我有意避免使用在编程中经常使用的计数器。例如,拥有跟踪 10 和 100 的计数器:“如果计数器 1 == 100,则……”之类的东西。我想避免跟踪这些计数器的状态,而只使用可用的功能/HOF。
以下是我迄今为止最好的尝试。它在 10.1 秒内完成任务(目前为 5.8 秒)。从这个角度来看,我之前提到的线程的 C 版本在 9.4 秒内完成。还有另一个我没有编写的 Haskell 版本,它使用计数器完成 8.1 秒(目前为 4.7)。这就是我的灵感(感谢 Xyne 并教导案例陈述如何发挥作用)。
** 编辑 **
我忘了提到它的作用。它使用整数除法将输入数据拆分为十个可能的范围,并将它们放入列表列表中(编辑以澄清 luqui 询问的拆分)。所以:
1 4 7 9 -> [1,4,7,9]
1 2 3 8 -> [1,2,3,8]
0 1 2 3 4 5 6 7 8 9 -> [0,1,2,3,4,5,6,7,8,9]
7 8 9 10 11 12 13 14 15 16 17 18 19 20 25 27 -> [7,8,9] [10,11,12,13,14,15,16,17,18,19] [20,25,27]
filterLengthBy 然后删除长度不是 10 的所有内容,因为我们不需要它。结果列表根据头部的整数除法拆分为列表列表。所以:
[[0,1,2,3,4,5,6,7,8,9],[10,11,12,13,14,15,16,17,18,19],[100,101,102,103,104,105,106,107,108,109]]
变成:
[[[0,1,2,3,4,5,6,7,8,9],[10,11,12,13,14,15,16,17,18,19]],[[100,101,102,103,104,105,106,107,108,109]]]
如果列表的第二级长度为10,则存在范围为100的匹配并输出到解决方案集。否则,该列表包含范围为 10 的列表,应单独添加到解决方案集中。
旧版代码
import qualified Data.ByteString.Lazy.Char8 as DB
import qualified Data.Function as DF
import qualified Data.List as DL
import Control.Applicative
groupByEqOn :: Eq b => (a -> b) -> [a] -> [[a]]
groupByEqOn fn = DL.groupBy ((==) `DF.on` fn)
filterLengthBy :: (Int -> Bool) -> [[a]] -> [[a]]
filterLengthBy fn = filter (flb fn)
where flb fn xs = fn $ length xs
tensAndHundreds :: [[[Int]]] -> [(Int, Int)]
tensAndHundreds [] = []
tensAndHundreds (x:xs)
| length x /= 10 = tens x ++ tensAndHundreds xs
| otherwise = (head $ head x, 100) : tensAndHundreds xs
tens :: [[Int]] -> [(Int, Int)]
tens = map (\x -> (head x, 10))
dbEmpty :: DB.ByteString
dbEmpty = DB.empty
dbIntVals :: [DB.ByteString] -> [Int]
dbIntVals [] = []
dbIntVals (x:xs) =
case (DB.readInt x) of
(Just (x', dbEmpty)) -> x' : dbIntVals xs
_ -> error "*** Error: Non-integer input ***"
generateResults :: [Int] -> [(Int, Int)]
generateResults xs = tensAndHundreds
$ groupByEqOn ((`quot` 100) . head)
$ filterLengthBy (== 10)
$ groupByEqOn (`quot` 10) xs
displayResults :: [(Int, Int)] -> IO ()
displayResults = mapM_ (\(a, b) -> putStrLn (show a ++ " " ++ show b))
main :: IO ()
main = dbIntVals <$> DB.lines <$> DB.getContents >>=
displayResults . generateResults
分析器让我感到惊讶的唯一两件事是 filterLengthBy 的速度有多快。向所有参与过滤器的人致以崇高的敬意。考虑到我投入了多少数据,这确实是一件了不起的事情。就此而言,值得注意的是,我写的东西和它一样快。另一个惊喜是 dbIntVals 的速度有多慢。我确实实施了正确的错误检查,这让事情变得有点慢,但我仍然感到惊讶。除此之外,它看起来和我的代码完全一样。
【问题讨论】:
-
嗯,我还没有审查你的代码,但你能抓住十个
5 6 7 8 9 10 11 12 13 14的范围吗? -
如果您使用
map函数重写dbIntVals并将最后一行更改为displayResults . generateResults . dbIntVals会发生什么? -
所以你已经在 C 版本性能的 -6% 以内,你的问题是如何让它更快,我说对了吗?
-
lines真的对你有用吗?如果您(基本上)将其折叠在输入上,DB.readInt应该能够为您解决这个问题。要考虑的另一件事是放弃惰性 IO 的东西,并使用显式严格 IO 或其中一个花哨的包,如pipes。
标签: performance algorithm haskell