我已经阅读过使用现代优化技术的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
版本。遗憾
答案 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
效果问题
如果出现性能问题,请确保您编译 启用了优化的代码。传递-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)
这与懒惰有关。使用列表的示例可以利用延迟评估,因此它可以有效地遍历数字范围,而无需在内存中存储任何列表。带向量的示例实际上必须在内存中分配一个向量,这需要一些额外的时间。
对于这样的情况,列表永远不需要存储在内存中,列表可能更快。对于您确实需要将数据存储在内存中的情况,矢量通常会更快。