我认为我正在测试partition函数的性能,并得到了一些奇怪的结果。

我们有那个partition p xs == (filter p xs, filter (not . p) xs),但是我们选择了第一个实现,因为它只对列表执行一次遍历。但是,我得到的结果是,最好使用使用两次遍历的实现。

这是显示我所见内容的最少代码

import Criterion.Main
import System.Random
import Data.List (partition)

mypartition :: (a -> Bool) -> [a] -> ([a],[a])
mypartition p l = (filter p l, filter (not . p) l)



randList :: RandomGen g => g -> Integer -> [Integer]
randList gen 0 = []
randList gen n = x:xs
  where
    (x, gen') = random gen
    xs = randList gen' (n - 1)

main = do
  gen <- getStdGen
  let arg10000000 = randList gen 10000000
  defaultMain [
      bgroup "filters -- split list in half " [
        bench "partition100"         $ nf (partition (>= 50)) arg10000000
      , bench "mypartition100"       $ nf (mypartition (>= 50)) arg10000000
      ]
      ]

我同时使用-O和不使用ghc-7.10.3进行了测试,两次都得到了更好的两次遍历。

我正在使用criterion-1.1.1.0(filter p xs, filter (not . p) xs)
我的问题是:
  • 这是预期的吗?
  • 我正确使用了Criterion吗?我知道懒惰可能很棘手,如果同时使用了元组的两个元素,则ojit_code仅会进行两次遍历。
  • 这是否与Haskell中处理列表的方式有关?

  • 非常感谢!

    最佳答案

    这个问题没有黑白答案。要分析该问题,请考虑以下代码:

    import Control.DeepSeq
    import Data.List (partition)
    import System.Environment (getArgs)
    
    
    mypartition :: (a -> Bool) -> [a] -> ([a],[a])
    mypartition p l = (filter p l, filter (not . p) l)
    
    
    main :: IO ()
    main = do
      let cnt = 10000000
          xs = take cnt $ concat $ repeat [1 .. 100 :: Int]
      args <- getArgs
      putStrLn $ unwords $ "Args:" : args
      case args of
        [percent, fun]
          -> let p = (read percent >=)
             in case fun of
               "partition"      ->              print $ rnf $ partition   p xs
               "mypartition"    ->              print $ rnf $ mypartition p xs
               "partition-ds"   -> deepseq xs $ print $ rnf $ partition   p xs
               "mypartition-ds" -> deepseq xs $ print $ rnf $ mypartition p xs
               _ -> err
        _ -> err
      where
        err = putStrLn "Sorry, I do not understand."
    

    我没有使用Criterion更好地控制评估顺序。为了获得时间,我使用+RTS -s运行时选项。使用不同的命令行选项执行不同的测试用例。第一个命令行选项定义谓词占数据百分比。第二个命令行选项在不同的测试之间进行选择。

    测试区分两种情况:
  • 数据是延迟生成的(第二个参数partitionmypartition)。
  • 数据已经在内存中进行了完全评估(第二个参数partition-dsmypartition-ds)。

  • 分区的结果总是从左到右评估,即从包含谓词所持有的所有元素的列表开始。

    在情况1中,partition的优点是,甚至在生成输入列表的所有元素之前,第一个结果列表的元素也会被丢弃。如果谓词与许多元素匹配,即第一个命令行参数很大,则情况1特别好。

    在情况2中,由于所有元素已经在内存中,因此partition无法发挥这一优势。

    对于mypartition,无论如何,在评估第一个结果列表之后,所有元素都保留在内存中,因为再次需要它们来计算第二个结果列表。因此,这两种情况之间没有太大区别。

    似乎,使用的内存越多,垃圾收集就越困难。因此,如果谓词与许多元素匹配并且使用了lazy变体,则partition非常适合。

    相反,如果谓词与许多元素不匹配,或者所有元素都已在内存中,则mypartition的性能会更好,因为与partition相比,它的递归不处理对。

    Stackoverflow问题“Irrefutable pattern does not leak memory in recursion, but why?”可能会提供更多关于partition递归中对对的处理的见解。

    关于haskell - 基准过滤器和分区,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/38671397/

    10-12 23:14