使用向量的风格与性能

代码如下:

{-# LANGUAGE FlexibleContexts #-}

import Data.Int
import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Generic as V

{-# NOINLINE f #-} -- Note the 'NO'
--f :: (Num r, V.Vector v r) => v r -> v r -> v r
--f :: (V.Vector v Int64) => v Int64 -> v Int64 -> v Int64
--f :: (U.Unbox r, Num r) => U.Vector r -> U.Vector r -> U.Vector r
f :: U.Vector Int64 -> U.Vector Int64 -> U.Vector Int64
f = V.zipWith (+) -- or U.zipWith, it doesn't make a difference

main = do
    let iters = 100
        dim = 221184
        y = U.replicate dim 0 :: U.Vector Int64
    let ans = iterate ((f y)) y !! iters
    putStr $ (show $ U.sum ans)

我用ghc 7.6.2-O2编译,花费了1.7秒的时间。

我尝试了几个不同版本的f

  • fx = U.zipWith (+) x
  • fx = (U.zipWith (+) x) . id
  • fxy = U.zipWith (+) xy
  • 版本1与原始版本相同,而版本2和版本3在0.09秒内运行(并且INLINING f不会改变任何内容)。

    我也注意到,如果我做f多态性(与以上三种签名),即使有一个“快”的定义(即2或3),它减缓回落...准确1.7秒。 这让我想知道原始问题是否可能归因于(缺少)类型推断,尽管我明确地给出了Vector类型和元素类型的类型。

    我也有兴趣添加整数modulo q

    newtype Zq q i = Zq {unZq :: i}
    

    当添加Int64 ,如果我写一个指定了每种类型的函数,

    h :: U.Vector (Zq Q17 Int64) -> U.Vector (Zq Q17 Int64) -> U.Vector (Zq Q17 Int64)
    

    与留下任何多态性相比,我获得的性能提高了一个数量级

    h :: (Modulus q) => U.Vector (Zq q Int64) -> U.Vector (Zq q Int64) -> U.Vector (Zq q Int64)
    

    但我应该至少能够删除特定的幻像类型! 应当编出来的,因为我负责的一个newtype

    这是我的问题:

  • 减速从何而来?
  • f中的版本2和版本3会以什么方式影响性能? 这似乎是一个错误,(编码风格)会影响这样的性能。 除矢量之外,部分应用功能或其他风格选择是否会影响性能?
  • 为什么多态会使我的速度减慢一个数量级,与多态性的位置无关(即向量类型, Num类型,两者还是幻像类型)? 我知道多态使代码变慢,但这很荒谬。 它周围有黑客吗?
  • 编辑1

    我向Vector库页面提出了一个问题。 我发现与这个问题有关的GHC问题。

    EDIT2

    从@ kqr的回答中获得了一些见解后,我重写了这个问题。 以下是原件供参考。

    --------------原文问题--------------------

    代码如下:

    {-# LANGUAGE FlexibleContexts #-}
    
    import Control.DeepSeq
    import Data.Int
    import qualified Data.Vector.Unboxed as U
    import qualified Data.Vector.Generic as V
    
    {-# NOINLINE f #-} -- Note the 'NO'
    --f :: (Num r, V.Vector v r) => v r -> v r -> v r
    --f :: (V.Vector v Int64) => v Int64 -> v Int64 -> v Int64
    --f :: (U.Unbox r, Num r) => U.Vector r -> U.Vector r -> U.Vector r
    f :: U.Vector Int64 -> U.Vector Int64 -> U.Vector Int64
    f = V.zipWith (+)
    
    main = do
        let iters = 100
            dim = 221184
            y = U.replicate dim 0 :: U.Vector Int64
        let ans = iterate ((f y)) y !! iters
        putStr $ (show $ U.sum ans)
    

    我用ghc 7.6.2-O2编译,花费了1.7秒的时间。

    我尝试了几个不同版本的f

  • fx = U.zipWith (+) x
  • fx = (U.zipWith (+) x) . U.force
  • fx = (U.zipWith (+) x) . Control.DeepSeq.force)
  • fx = (U.zipWith (+) x) . (z -> z `seq` z)
  • fx = (U.zipWith (+) x) . id
  • fxy = U.zipWith (+) xy
  • 版本1与原版相同,版本2在0.111秒内运行,版本3-6在0.09秒内运行(并且INLINING f不会改变任何内容)。

    所以这个数量级的放缓似乎是由于force帮助而导致的懒惰,但我不确定懒惰是从哪里来的。 取消装箱的类型不允许偷懒,对吗?

    我试着写一个严格的iterate版本,认为矢量本身必须是懒惰的:

    {-# INLINE iterate' #-}
    iterate' :: (NFData a) => (a -> a) -> a -> [a]
    iterate' f x =  x `seq` x : iterate' f (f x)
    

    但是使用f的免费版本,这完全没有帮助。

    我也注意到了别的东西,这可能只是一个巧合,红鲱鱼:如果我让f多态性(与以上三种签名),即使有一个“快”的定义,它减缓回落...准确1.7秒。 这让我怀疑原始问题是否可能归因于(缺少)类型推断,即使所有的东西都应该被很好地推断出来。

    这是我的问题:

  • 减速从何而来?
  • 为什么force合成帮助,但使用严格的iterate不?
  • 为什么U.forceDeepSeq.force更糟? 我不知道U.force应该做什么,但听起来很像DeepSeq.force ,似乎也有类似的效果。
  • 为什么多态性使我减慢了一个数量级,与多态性的位置无关(即在向量类型中,在Num类型中还是在两者中)?
  • 为什么版本5和版本6都没有任何严格的含义,就像严格的功能一样快?
  • 正如@kqr指出的那样,这个问题似乎并不严格。 所以我写这个函数的方式会导致使用通用的zipWith而不是zipWith特定的版本。 这仅仅是GHC和Vector库之间的一种偶然,还是可以在这里说一些更一般的东西?


    虽然我没有你想要的明确答案,但有两件事可以帮助你。

    首先, x `seq` x在语义和计算上都与x相同。 该wiki说seq

    关于seq的常见误解是seq x “评估” x 。 好吧,有点。 seq并不仅仅依靠源文件中存在的东西来评估任何东西,它所做的只是在另一个值上引入一个数值的仿真数据依赖性:当seq的结果被评估时,第一个参数也必须(有点;见下文)进行评估。

    举个例子,假设x :: Integer ,然后seq xb行为基本上类似于if x == 0 then b else b - 无条件等于b ,但强制x沿途。 特别是, x `seq` x这个表达式是完全多余的,并且总是和写x一样具有完全相同的效果。

    什么第一段说的是,写作seq ab并不意味着a会奇迹般地得到评估这一刻,就意味着a如将得到尽快评估b需要进行评估。 这可能会在程序的后面发生,也可能永远不会发生。 当你从这个角度来看时,显然seq xx是一个冗余,因为它所说的只是“一旦x需要被评估就评估x ”。 无论如何,如果你只写了x ,那当然会发生什么。

    这对你有两个影响:

  • 你的“严格的” iterate'函数并不比没有seq更严格。 事实上,我很难想象iterate函数如何变得比现在更严格。 你不能严格限制列表的尾部,因为它是无限的。 你可以做的主要事情是强制使用“累加器” fx ,但这样做并不会给我的系统带来任何显着的性能提升[1]。

    抓住那个。 你的严格iterate'和我的爆炸模式版本完全一样。 查看评论。

  • 写作(z -> z `seq` z)并没有给你一个严格的身份识别功能,我认为这就是你要做的。 事实上,普通的身份识别功能就像你会得到的那样严格 - 它会在需要时立即评估其结果。

  • 然而,我偷看了GHC核心的核心

    U.zipWith (+) y
    

    U.zipWith (+) y . id
    

    而且我的未经训练的眼睛只能看到一个很大的区别。 第一个使用的只是一个普通的Data.Vector.Generic.zipWith (这里是你的多态性巧合可能会发挥作用的地方 - 如果GHC选择一个通用的zipWith它当然会执行,就好像代码是多态的!),而后者爆炸了这个单个函数调用几乎90行的状态monad代码和解压缩的机器类型。

    状态monad代码看起来几乎就像用命令式语言编写的循环和破坏性更新,所以我认为它适合运行的机器。 如果我不那么着急,我会花更长的眼光来看看它是如何工作的,以及为什么GHC突然决定需要一个紧密的循环。 我和其他任何想看看的人一样,为我自己附加了生成的核心。[2]


    [1]:沿途强制累加器:(这是你已经做的事,我误解了代码!)

    {-# LANGUAGE BangPatterns #-}
    iterate' f !x = x : iterate f (f x)
    

    [2]:什么核心U.zipWith (+) y . id U.zipWith (+) y . id被翻译成。

    链接地址: http://www.djcxy.com/p/43395.html

    上一篇: Style vs Performance Using Vectors

    下一篇: Moving a compiled Haskell program