为什么Vector代码比标准列表慢

时间:2013-10-03 13:54:50

标签: haskell

我已经阅读过使用现代优化技术的Vector库,并尝试将其性能与列表进行比较。下面的代码生成一些类似声音的数据(这对我的主题区域很重要)并总结结果:

import System.Environment (getArgs)
import System.TimeIt
import Data.List
import Data.Vector.Unboxed as V

x1 :: Int -> [Double]
x1 n = [1..(fromIntegral n)]

x2 :: Int -> V.Vector Double
x2 n = V.enumFromN 1 n

osc1 f = Prelude.map (\x -> sin(2*pi*x*f/44100.0))
osc2 f = V.map (\x -> sin(2*pi*x*f/44100.0))

sum1 = Data.List.foldl1' (+)
sum2 = V.foldl1' (+)

zip1 = Prelude.zipWith (+)
zip2 = V.zipWith (+)

main = do s <- getArgs
          let n = read (s !! 0) :: Int
          print "Prelude version"
          timeIt $ print $ sum1 $ zip1 (osc1 55.5 (x1 n)) (osc1 110.0 $ x1 n)
          print "Vector version"
          timeIt $ print $ sum2 $ zip2 (osc2 55.5 (x2 n)) (osc2 110.0 $ x2 n)
使用vector0.10.0.1和timeit1.0.0.0在win7上运行的GHC 7.6.3给了我这些结果:

c:\coding>test 10000000
"Prelude version"
90.98579564908658
CPU time:   9.92s
"Vector version"
90.98579564908658
CPU time:  11.03s

即使是Unboxed,矢量版本也有点慢,盒装矢量版本需要22.67秒。为什么会这样?我应该如何编写此代码以获得最大的性能?

UPD。添加-O2(**)后,我对结果更清楚了。它看起来像盒装矢量更难融合。

                  List    Vector.Unboxed    Vector
ghc test.hs       9.78    10.94             21.95
ghc test.hs -O2   3.39    1.25              7.57

(**)我没有注意到,因为即使命令行标志不同,ghc也不会重新编译未更改的文件,而且在注意到之前我没有实际运行-O2版本。遗憾

4 个答案:

答案 0 :(得分:5)

这是优化标志的问题:

<强> -o0

>test 10000000
"Prelude version"
90.98579564908658
CPU time:   6.66s
"Vector version"
90.98579564908658
CPU time:   8.27s

<强> -o1

>test 10000000
"Prelude version"
90.98579565011536
CPU time:   2.70s
"Vector version"
90.98579565011924
CPU time:   1.62s

<强> -O2

>test 10000000
"Prelude version"
90.98579565011536
CPU time:   2.72s
"Vector version"
90.98579565011924
CPU time:   1.34s

来自Haskell tag info

  

效果问题

     

如果出现性能问题,请确保您编译   启用了优化的代码。传递-O2会产生很多性能   问题消失了。

更新

快速解释为什么Unboxed更快here's one

  

最灵活的类型是Data.Vector.Vector,它提供盒装数组:指向Haskell值的指针数组。

     

这些数组适用于存储复杂的Haskell类型(求和类型或代数数据类型),但对于简单数据类型,更好的选择是Data.Vector.Unboxed。

对于Unboxed:

  

简单,原子类型和对类型可以以更有效的方式存储:没有指针的连续内存插槽。

[off]优化稍微改变结果,这很有趣。 [开/关]

答案 1 :(得分:2)

我会说vector版本被强制实际实现了向量(为它分配内存),并且就像在命令式设置中使用for循环和数组的实现一样。从某种意义上说,它“做了人们所期望的”(至少有一个必要的背景)。

但是使用列表的版本会发生一些有趣的事情,这种魔法称为“流融合”:编译器足够聪明,可以确定跟踪总和以计算最终结果就足够了。这是通过计算值并将它们相加来完成的,最后打印出总和。根本不需要实际的列表,因此它永远不会被分配或遍历。

我没有通过查看生成的Core验证这一点,所以......

答案 2 :(得分:2)

在启用优化的情况下进行编译时,Vector更快。启用优化后,编译器会内联并专门化矢量函数,从而消除了大量函数调用和盒装临时值。

通过将所有计算步骤融合到一个循环中,切换到流可以为您提供1.5倍的改进。没有构建数组。

import Data.Vector.Fusion.Stream as S

x3 :: Int -> S.Stream Double
x3 n = S.enumFromStepN 1 1 n
osc3 f = S.map (\x -> sin(2*pi*x*f/44100.0))
sum3 = S.foldl1' (+)
zip_3 = S.zipWith (+)

main = do s <- getArgs
          let n = read (s !! 0) :: Int
          print "Stream version"
          timeIt $ print $ sum3 $ zip_3 (osc3 55.5 (x3 n)) (osc3 110.0 $ x3 n)

Vector上的流融合不会融合zipWith的输入,因此矢量代码不会以相同的方式进行优化。

使用-O2进行编译,Prelude版本速度最慢,Stream版本最快。

$ ./Test 10000000
"Prelude version"
90.98579565011536
3.051188s
"Vector version"
90.98579565011924
1.81228s
"Stream version"
90.98579565011907
1.155345s

答案 3 :(得分:0)

这与懒惰有关。使用列表的示例可以利用延迟评估,因此它可以有效地遍历数字范围,而无需在内存中存储任何列表。带向量的示例实际上必须在内存中分配一个向量,这需要一些额外的时间。

对于这样的情况,列表永远不需要存储在内存中,列表可能更快。对于您确实需要将数据存储在内存中的情况,矢量通常会更快。