4

我已阅读有关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

Vector 版本甚至有点慢Unboxed,盒装 Vector 版本需要 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

4 回答 4

6

这是优化标志的问题:

-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 标签信息

性能问题

如果出现性能问题,请确保您在编译代码时启用了优化。通过 -O2 可以消除许多性能问题。

更新

要快速解释为什么 Unboxed 更快,这里有一个

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

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

对于未装箱:

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

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

于 2013-10-03T14:53:16.647 回答
2

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

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

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

于 2013-10-03T14:18:50.250 回答
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)

s上的流融合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
于 2013-10-03T15:13:35.197 回答
0

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

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

于 2013-10-03T14:11:10.567 回答