为什么列表保险丝和Vector都没有? (Haskell的)

时间:2014-12-06 03:35:46

标签: performance haskell fusion

考虑以下基准:

module Main where

import qualified Data.List as L
import qualified Data.Vector.Unboxed as U
import Criterion.Main



goodSum :: Int -> Double
{-# NOINLINE goodSum #-}
goodSum n =
  let ints = U.enumFromN 0 (n * n * 10) :: U.Vector Int
  in U.foldl' (+) 0 $ U.map fromIntegral ints

badSum :: Int -> Double
{-# NOINLINE badSum #-}
badSum n = L.foldl' (+) 0.5 [fromIntegral i | i <- [0 .. 10*n*n]]

badSum2 :: Int -> Double
{-# NOINLINE badSum2 #-}
badSum2 n = L.foldr (+) 0.5 [fromIntegral i | i <- [0 .. 10*n*n]]

worstSum :: Int -> Double
{-# NOINLINE worstSum #-}
worstSum n = L.foldl1' (+) $ do
  i <- [0 .. n*n]
  return $ L.foldl1' (+) $ do
    k <- [0 .. 10]
    return $ fromIntegral $ k + i

main = do
  defaultMain
    [ bench "good" $ nf goodSum 500
    , bench "bad" $ nf badSum 500
    , bench "bad2" $ nf badSum2 500
    , bench "worst" $ nf worstSum 500
    ]

结果:

benchmarking good
time                 1.826 ms   (1.819 ms .. 1.835 ms)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 1.810 ms   (1.803 ms .. 1.817 ms)
std dev              23.18 μs   (19.91 μs .. 27.96 μs)

benchmarking bad
time                 38.38 ms   (38.07 ms .. 38.74 ms)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 38.23 ms   (38.07 ms .. 38.38 ms)
std dev              298.5 μs   (220.6 μs .. 417.8 μs)

benchmarking bad2
time                 77.87 ms   (73.74 ms .. 82.67 ms)
                     0.992 R²   (0.976 R² .. 0.998 R²)
mean                 78.14 ms   (75.33 ms .. 82.13 ms)
std dev              5.184 ms   (3.056 ms .. 7.966 ms)
variance introduced by outliers: 19% (moderately inflated)

benchmarking worst
time                 80.80 ms   (79.53 ms .. 82.10 ms)
                     1.000 R²   (0.999 R² .. 1.000 R²)
mean                 80.73 ms   (80.29 ms .. 81.19 ms)
std dev              756.9 μs   (591.9 μs .. 938.2 μs)

列表理解是good producers and foldr is a good consumer,为什么列表不能融合?

1 个答案:

答案 0 :(得分:4)

与你的问题相反,foldr和列表理解事实上, 保险丝。但是,您必须回忆foldr的定义,而不是它 不是尾递归函数。前奏将foldr定义为,而不是 编译成一个紧凑的循环,如基于Vector的示例。

foldr k z = go
  where
    go []     = z
    go (y:ys) = y `k` go ys

badSum2生成的核心重要部分如下所示

$wgo_s8AH [Occ=LoopBreaker] :: Int# -> Double#
$wgo_s8AH =
  \ (w_s8AD :: Int#) ->
    case tagToEnum#
           @ Bool (==# w_s8AD y1_a69V)
    of _ [Occ=Dead] {
      False ->
        case $wgo_s8AH (+# w_s8AD 1) of ww_s8AG { __DEFAULT ->
        +## (int2Double# w_s8AD) ww_s8AG
        };
      True -> +## (int2Double# w_s8AD) 0.5

大致等同于此函数(模数未装箱算术)

badSum3 :: Int -> Double
badSum3 n = go 0
  where
    stop = 10 * n * n

    go i | i == stop = fromIntegral i + 0.5
         | otherwise = fromIntegral i + go (i + 1)

通过Criterion运行此功能,该功能提供与运行时相同的运行时间 badSum2。虽然生成的函数不会产生和消耗中间缺点单元格,但它仍然执行函数调用并执行所有相关的堆栈操作。

基于foldl'版本的糟糕表现是由于foldl'不是一个好的消费者,所以它不能与列表理解相融合。 left-fold将产生一个尾递归循环,它遍历列表推导产生的列表,从而产生所有分配和相关内存操作的开销。

我不确定您是否可以使用标准列表操作获得与Vector操作相同的性能,但stream-fusion包使用不同的融合方法提供列表组合器,这可以实现类似的性能对于这个问题。

import qualified Data.List.Stream as S

-- The stream-fusion package does not seem to have `enumFrom` functions
enumerate :: Int -> [Int]
enumerate n = S.unfoldr f 0
  where
    f i | i > n     = Nothing
        | otherwise = Just (i, i + 1)

goodSum2 :: Int -> Double
goodSum2 n = S.foldl' (+) 0.5 $ S.map fromIntegral $ enumerate (n * n * 10)