在Haskell中有效地计算列表的平均值

时间:2010-07-21 15:29:26

标签: performance list haskell

我设计了一个计算列表均值的函数。虽然它工作正常,但我认为它可能不是最好的解决方案,因为它需要两个功能而不是一个。是否可以仅使用一个递归函数完成这项工作?

calcMeanList (x:xs) = doCalcMeanList (x:xs) 0 0

doCalcMeanList (x:xs) sum length =  doCalcMeanList xs (sum+x) (length+1)
doCalcMeanList [] sum length = sum/length

6 个答案:

答案 0 :(得分:10)

你的解决方案很好,使用两个功能并不比一个好。您仍然可以将尾递归函数放在where子句中。

但如果你想在一行中做到这一点:

calcMeanList = uncurry (/) . foldr (\e (s,c) -> (e+s,c+1)) (0,0)

答案 1 :(得分:8)

关于您所能做的最好的事情是this version

import qualified Data.Vector.Unboxed as U

data Pair = Pair {-# UNPACK #-}!Int {-# UNPACK #-}!Double

mean :: U.Vector Double -> Double
mean xs = s / fromIntegral n
  where
    Pair n s       = U.foldl' k (Pair 0 0) xs
    k (Pair n s) x = Pair (n+1) (s+x)

main = print (mean $ U.enumFromN 1 (10^7))

它融合到Core中的最佳循环(你能写的最好的Haskell):

main_$s$wfoldlM'_loop :: Int#
                              -> Double#
                              -> Double#
                              -> Int#
                              -> (# Int#, Double# #)    
main_$s$wfoldlM'_loop =
  \ (sc_s1nH :: Int#)
    (sc1_s1nI :: Double#)
    (sc2_s1nJ :: Double#)
    (sc3_s1nK :: Int#) ->
    case ># sc_s1nH 0 of _ {
      False -> (# sc3_s1nK, sc2_s1nJ #);
      True ->
        main_$s$wfoldlM'_loop
          (-# sc_s1nH 1)
          (+## sc1_s1nI 1.0)
          (+## sc2_s1nJ sc1_s1nI)
          (+# sc3_s1nK 1)
    }

以下大会:

Main_mainzuzdszdwfoldlMzqzuloop_info:
.Lc1pN:
        testq %r14,%r14
        jg .Lc1pQ
        movq %rsi,%rbx
        movsd %xmm6,%xmm5
        jmp *(%rbp)
.Lc1pQ:
        leaq 1(%rsi),%rax
        movsd %xmm6,%xmm0
        addsd %xmm5,%xmm0
        movsd %xmm5,%xmm7
        addsd .Ln1pS(%rip),%xmm7
        decq %r14
        movsd %xmm7,%xmm5
        movsd %xmm0,%xmm6
        movq %rax,%rsi
        jmp Main_mainzuzdszdwfoldlMzqzuloop_info

基于Data.Vector。例如,

$ ghc -Odph --make A.hs -fforce-recomp
[1 of 1] Compiling Main             ( A.hs, A.o )
Linking A ...
$ time ./A
5000000.5
./A  0.04s user 0.00s system 93% cpu 0.046 total

请参阅the statistics package中的有效实施。

答案 2 :(得分:4)

当我看到你的问题时,我立即想到“你想要fold那里!”

果然,之前已经在StackOverflow上询问过a similar questionthis answer有一个非常高效的解决方案,您可以在GHCi等交互式环境中进行测试:

import Data.List

let avg l = let (t,n) = foldl' (\(b,c) a -> (a+b,c+1)) (0,0) l 
            in realToFrac(t)/realToFrac(n)

avg ([1,2,3,4]::[Int])
2.5
avg ([1,2,3,4]::[Double])
2.5

答案 3 :(得分:3)

虽然我不确定在一个函数中编写它是否“最好”,但可以按如下方式完成:

如果您事先知道长度(让我们在这里称之为'),那么您可以计算每个值“加”到平均值的数量;这将是价值/长度。由于avg(x1,x2,x3)= sum(x1,x2,x3)/ length =(x1 + x2 + x3)/ 3 = x1 / 3 + x2 / 3 + x2 / 3

如果你事先不知道它的长度,它有点棘手:

假设我们使用列表{x1,x2,x3}而不知道它的n = 3。

第一次迭代只是x1(因为我们假设它只有n = 1) 第二次迭代会添加x2 / 2并将现有平均值除以2,所以现在我们有x1 / 2 + x2 / 2

在第三次迭代之后我们有n = 3而我们想要x1 / 3 + x2 / 3 + x3 / 3但我们有x1 / 2 + x2 / 2

因此我们需要乘以(n-1)并除以n得到x1 / 3 + x2 / 3,然后我们只需将当前值(x3)除以n,最后得到x1 / 3 + x2 / 3 + x3 / 3

一般而言:

给出n-1个项目的平均值(算术平均值 - 平均值),如果你想将一个项目(newval)添加到你的等式的平均值:

avg *(n-1)/ n + newval / n。可以使用归纳法在数学上证明该等式。

希望这有帮助。

*请注意,此解决方案的效率低于简单求和变量并除以总长度,就像在示例中一样。

答案 4 :(得分:3)

对于那些想知道什么是焕发编码器和Assaf的方法在Haskell中会是什么样子的人来说,这是一个翻译:

avg [] = 0
avg x@(t:ts) = let xlen = toRational $ length x
                   tslen = toRational $ length ts
                   prevAvg = avg ts
               in (toRational t) / xlen + prevAvg * tslen / xlen

这种方式确保每个步骤都具有正确计算的“平均值”,但这样做的代价是一堆冗余乘法/除以长度,并且每步的计算长度非常低效。没有经验丰富的Haskeller会这样写。

唯一稍微好一点的方法是:

avg2 [] = 0
avg2 x = fst $ avg_ x
    where 
      avg_ [] = (toRational 0, toRational 0)
      avg_ (t:ts) = let
           (prevAvg, prevLen) = avg_ ts
           curLen = prevLen + 1
           curAvg = (toRational t) / curLen + prevAvg * prevLen / curLen
        in (curAvg, curLen)

这避免了重复的长度计算。但它需要辅助功能,这正是原始海报试图避免的功能。而且它还需要一大堆取消长度的术语。

为了避免取消长度,我们可以在最后建立总和和长度并进行划分:

avg3 [] = 0
avg3 x = (toRational total) / (toRational len)
    where 
      (total, len) = avg_ x
      avg_ [] = (0, 0)
      avg_ (t:ts) = let 
          (prevSum, prevLen) = avg_ ts
       in (prevSum + t, prevLen + 1)

这可以更简洁地写成折叠器:

avg4 [] = 0
avg4 x = (toRational total) / (toRational len)
    where
      (total, len) = foldr avg_ (0,0) x
      avg_ t (prevSum, prevLen) = (prevSum + t, prevLen + 1)

可根据上述帖子进一步简化。

折叠真的是去这里的方式。

答案 5 :(得分:0)

为了跟进Don在2010年的回复,在GHC 8.0.2上,我们可以做得更好。首先让我们试试他的版本。

module Main (main) where

import System.CPUTime.Rdtsc (rdtsc)
import Text.Printf (printf)
import qualified Data.Vector.Unboxed as U

data Pair = Pair {-# UNPACK #-}!Int {-# UNPACK #-}!Double

mean' :: U.Vector Double -> Double
mean' xs = s / fromIntegral n
  where
    Pair n s       = U.foldl' k (Pair 0 0) xs
    k (Pair n s) x = Pair (n+1) (s+x)

main :: IO ()
main = do
  s <- rdtsc
  let r = mean' (U.enumFromN 1 30000000)
  e <- seq r rdtsc
  print (e - s, r)

这给了我们

[nix-shell:/tmp]$ ghc -fforce-recomp -O2 MeanD.hs -o MeanD && ./MeanD +RTS -s
[1 of 1] Compiling Main             ( MeanD.hs, MeanD.o )
Linking MeanD ...
(372877482,1.50000005e7)
     240,104,176 bytes allocated in the heap
           6,832 bytes copied during GC
          44,384 bytes maximum residency (1 sample(s))
          25,248 bytes maximum slop
             230 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0         1 colls,     0 par    0.000s   0.000s     0.0000s    0.0000s
  Gen  1         1 colls,     0 par    0.006s   0.006s     0.0062s    0.0062s

  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time    0.087s  (  0.087s elapsed)
  GC      time    0.006s  (  0.006s elapsed)
  EXIT    time    0.006s  (  0.006s elapsed)
  Total   time    0.100s  (  0.099s elapsed)

  %GC     time       6.2%  (6.2% elapsed)

  Alloc rate    2,761,447,559 bytes per MUT second

  Productivity  93.8% of total user, 93.8% of total elapsed

然而,代码很简单:理想情况下,不需要vector:只需内联列表生成就可以实现最佳代码。幸运的是,GHC可以为我们做这件事[0]。

module Main (main) where

import System.CPUTime.Rdtsc (rdtsc)
import Text.Printf (printf)
import Data.List (foldl')

data Pair = Pair {-# UNPACK #-}!Int {-# UNPACK #-}!Double

mean' :: [Double] -> Double
mean' xs = v / fromIntegral l
  where
    Pair l v = foldl' f (Pair 0 0) xs
    f (Pair l' v') x = Pair (l' + 1) (v' + x)

main :: IO ()
main = do
  s <- rdtsc
  let r = mean' $ fromIntegral <$> [1 :: Int .. 30000000]
      -- This is slow!
      -- r = mean' [1 .. 30000000]
  e <- seq r rdtsc
  print (e - s, r)

这给了我们:

[nix-shell:/tmp]$ ghc -fforce-recomp -O2 MeanD.hs -o MeanD && ./MeanD +RTS -s
[1 of 1] Compiling Main             ( MeanD.hs, MeanD.o )
Linking MeanD ...
(128434754,1.50000005e7)
         104,064 bytes allocated in the heap
           3,480 bytes copied during GC
          44,384 bytes maximum residency (1 sample(s))
          17,056 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0         0 colls,     0 par    0.000s   0.000s     0.0000s    0.0000s
  Gen  1         1 colls,     0 par    0.000s   0.000s     0.0000s    0.0000s

  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time    0.032s  (  0.032s elapsed)
  GC      time    0.000s  (  0.000s elapsed)
  EXIT    time    0.000s  (  0.000s elapsed)
  Total   time    0.033s  (  0.032s elapsed)

  %GC     time       0.1%  (0.1% elapsed)

  Alloc rate    3,244,739 bytes per MUT second

  Productivity  99.8% of total user, 99.8% of total elapsed

[0]:注意我必须如何映射fromIntegral:没有这个,GHC无法消除[Double],解决方案要慢得多。这有点令人伤心:我不明白为什么GHC没有内联/决定它没有必要没有这个。如果你有真正的分数收集,那么这个黑客不会为你工作,而矢量可能仍然是必要的。