在大型列表上计算最小值/最大值/总和时的Haskell性能

时间:2014-02-08 18:47:35

标签: haskell

我一直在尝试以下Haskell代码:

data Foo = Foo
  { fooMin :: Float
  , fooMax :: Float
  , fooSum :: Float
  } deriving Show


getLocalFoo :: [Float] -> Foo
getLocalFoo x = Foo a b c
  where
    a = minimum x
    b = maximum x
    c = sum x

getGlobalFoo :: [Foo] -> Foo
getGlobalFoo x = Foo a b c 
  where
    a = minimum $ fmap fooMin x
    b = maximum $ fmap fooMax x
    c = sum $ fmap fooSum x


main :: IO()
main = do
  let numItems = 2000
  let numLists = 100000
  putStrLn $ "numItems: " ++ show numItems
  putStrLn $ "numLists: " ++ show numLists

  -- Create an infinite list of lists of floats, x is [[Float]]
  let x = take numLists $ repeat [1.0 .. numItems] 

  -- Print two first elements of each item
  print $ take 2 (map (take 2) x)

  -- First calculate local min/max/sum for each float list 
  -- then calculate the global min/max/sum based on the results.
  print . getGlobalFoo $ fmap getLocalFoo x

在调整numItems和numLists时按顺序测试运行时:

小尺寸

numItems: 4.0
numLists: 2
[[1.0,2.0],[1.0,2.0]]
Foo {fooMin = 1.0, fooMax = 4.0, fooSum = 20.0}

real    0m0.005s
user    0m0.004s
sys 0m0.001s

大尺寸

numItems: 2000.0
numLists: 100000
[[1.0,2.0],[1.0,2.0]]
Foo {fooMin = 1.0, fooMax = 2000.0, fooSum = 1.9999036e11}

real    0m33.116s
user    0m33.005s
sys 0m0.109s

我用直觉和幼稚的方式编写了这段代码而不考虑性能,但是我担心这远远不是最佳代码,因为我实际上可能需要多次折叠列表然后需要?

有人可以建议更好地实施此测试吗?

3 个答案:

答案 0 :(得分:2)

使用foldl库可以一次有效地运行多个折叠。实际上,它非常擅长于您不需要将列表拆分为子列表。您可以将所有列表连接成一个巨大的列表并直接折叠。

以下是:

import Control.Applicative
import qualified Control.Foldl as L

data Foo = Foo
  { fooMin :: Maybe Float
  , fooMax :: Maybe Float
  , fooSum :: Float
  } deriving Show

foldFloats :: L.Fold Float Foo
foldFloats = Foo <$> L.minimum <*> L.maximum <*> L.sum
-- or: foldFloats = liftA3 Foo L.minimum L.maximum L.sum

main :: IO()
main = do
    let numItems = 2000
    let numLists = 100000
    putStrLn $ "numItems: " ++ show numItems
    putStrLn $ "numLists: " ++ show numLists

    -- Create an infinite list of lists of floats, x is [[Float]]
    let x = replicate numLists [1.0 .. numItems] 

    -- Print two first elements of each item
    print $ take 2 (map (take 2) x)

    print $ L.fold foldFloats (concat x)

与您的代码的主要区别是:

  • 我使用replicate n,这与take n . repeat相同。事实上,这就是replicate实际定义的方式

  • 我不打算单独处理子列表。我只是concat将它们全部放在一起并一次折叠。

  • 我使用Maybe作为最小值和最大值,因为我需要处理空列表的情况。

  • 此代码更快

以下是数字:

$ time ./fold
numItems: 2000.0
numLists: 100000
[[1.0,2.0],[1.0,2.0]]
Foo {fooMin = Just 1.0, fooMax = Just 2000.0, fooSum = 3.435974e10}

real  0m5.796s
user  0m5.756s
sys   0m0.024s

foldl是一个非常小且易于学习的库。您可以详细了解here

答案 1 :(得分:1)

拯救蠢货。所有操作 - 总和,最小值和最大值 - 都可以表示为幺半群。对于最小值和最大值,我们需要将它从semigroups包装到Option中,因为我们需要以某种方式表示空集合的最小值和最大值。 (另一种方法是将自己限制为非空集合,然后我们可以使用半群而不是幺半群。)

我们需要的另一件事是确保在每一步中强制执行所有计算。为此,我们声明Foo的{​​{1}}实例,添加我们使用的monoid类型的一些缺失实例,以及在折叠操作期间强制值的辅助函数。

NFData

答案 2 :(得分:1)

也许单个折叠更便宜。尝试运行一些类似的测试:

{-# LANGUAGE BangPatterns #-}
import Data.List

getLocalFoo :: [Float] -> Foo
getLocalFoo [] = error "getLocalFoo: empty list"
getLocalFoo (x:xs) = foldl' f (Foo x x x) xs
  where f (Foo !min1 !max1 !sum1) y =
          Foo (min1 `min` y) (max1 `max` y) (sum1 + y)

getGlobalFoo类似。