【问题标题】:Haskell algorithm advice and suggestions for alternate solutionsHaskell 算法建议和替代解决方案的建议
【发布时间】: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

我在原帖底部的两个问题是:

  1. 鉴于我在上面发布的方法,我是否遗漏了什么可以提高该程序的效率并因此提高该程序的性能?空间分配是最终的瓶颈吗?换句话说,groupBy 方法是否正常运行?

  2. 我还有什么其他方法可以解决这个问题,既能提高效率,又能避免传统意义上的计数器?我还没有体验过 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


【解决方案1】:

我已经完成了这个线程。我来这里是为了看看 SO Haskell 社区提供了什么,我得到了一些有用的回应。为此,我很感激。

我发布的最新版本的代码表明,只要提供良好的代码,GHC 可以仅使用函数式惯用语来生成极快、准确的程序。这似乎很明显,但这是我从头开始编写的第一个 Haskell 程序(带有明显的 groupBy 复制和粘贴)。

我学到了什么?

dfeuer 提醒我要小心那些仅依赖于像 map 这样的内置函数不一定是最好的方法的时候。有时结合逻辑很有帮助。在这种情况下,我获得了非常显着的性能提升。

从 applicative 我了解了高效输出。性能的提升是惊人的。

如果有任何 Haskell/GHC 开发人员正在阅读本文:

在我看来,groupBy here 应该包含在标准库中,而不是推送到 Hackage。为什么?在保持准确性的同时速度很快。对于数百万个元素的完全相同的操作并产生完全相同的结果,性能提高了约 8.2%。我知道它不会做完全相同的事情。我还是觉得值得一看。

this 真的有意义吗?我看到与上述相同的数据集相比,性能至少提高了 10%,准确度为 100%。我知道'rem'和'mod'做不同的事情并且它们倾向于在不同的“方向”上四舍五入,但前者以巨大的优势胜过后者。我可能不理解这个范围,但 10% 是很难忽略的。

【讨论】:

  • 我说我已经完成了这个工作,但我确实发现了一些显着的性能提升。其中之一是 dfeuer 是正确的,因为使用惰性字节串会减慢速度。我最初的测试表明这不是真的。我一定没有正确测试。这导致我通过以 $ 的形式实现严格性来发现这一点!在一个地方会产生更好的性能。另一个是用十倍的折叠替换地图,这有点帮助。最后,这个相当低效的算法运行时间为 5.1 秒。对于一匹死马,它似乎仍然活跃!
  • 这个算法已经正常运行了。与惰性相比,严格会导致内存使用问题。我的最终版本回归懒惰。
  • 这种问题不是更适合codereview.stackexchange.com吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-01-20
  • 1970-01-01
  • 1970-01-01
  • 2015-07-27
  • 2011-07-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多