使用向量的样式与性能

时间:2013-11-06 04:09:37

标签: haskell lambda pointfree

以下是代码:

{-# 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

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

    我还注意到,如果我使f多态(上面有三个签名中的任何一个),即使使用“快速”定义(即2或3),它也会慢下来......到1.7秒。这让我想知道原始问题是否可能是由于(缺乏)类型推断,即使我明确给出了Vector类型和元素类型的类型。

    我也有兴趣添加模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

    以下是我的问题:

    1. 减速来自何处?
    2. f版本2和3中以任何方式影响性能的情况如何?对我来说似乎是一个错误(编码风格)会影响这样的性能。在Vector之外还有其他示例,其中部分应用函数或其他样式选择会影响性能吗?
    3. 为什么多态性会使我减慢一个数量级,而与多态性的位置无关(即在矢量类型,Num类型,两者或幻像类型中)?我知道多态性会使代码变慢,但这很荒谬。周围有黑客吗?
    4.   

      编辑1

           

      我向Vector库页面提交了issue。我找到了与此问题有关的GHC issue

           

      EDIT2

           

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

      -------------- ORIGINAL QUESTION --------------------

      以下是代码:

      {-# 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

      1. f x = U.zipWith (+) x
      2. f x = (U.zipWith (+) x) . U.force
      3. f x = (U.zipWith (+) x) . Control.DeepSeq.force)
      4. f x = (U.zipWith (+) x) . (\z -> z `seq` z)
      5. f x = (U.zipWith (+) x) . id
      6. f x y = U.zipWith (+) x y
      7. 版本1与原版相同,版本2在0.111秒内运行,版本3-6在0.09秒内运行(INLINING f不会改变任何内容。)< / p>

        因此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秒。这让我想知道原始问题是否可能是由于(缺乏)类型推断造成的,即使所有内容都应该很好地推断出来。

        以下是我的问题:

        1. 减速来自何处?
        2. 为什么使用force撰写帮助,但使用严格的iterate却没有?
        3. 为什么U.forceDeepSeq.force更差?我不知道U.force应该做什么,但听起来很像DeepSeq.force,似乎也有类似的效果。
        4. 为什么多态性会使我减少一个数量级,而与多态性的位置无关(即在矢量类型,Num类型中,或两者都有)?
        5. 为什么版本5和版本6都没有任何严格意义,就像严格的函数一样快?
        6. 正如@kqr指出的那样,问题似乎并不严格。因此,我编写函数的方式导致使用泛型zipWith而不是Unboxed特定版本。这只是GHC和Vector库之间的侥幸,还是有更普遍的东西可以在这里说出来?

1 个答案:

答案 0 :(得分:13)

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

首先,x `seq` x在语义和计算上都与x完全相同。维基说seq

  

关于seq的一个常见误解是seq x“评估”x。好吧,有点。 seq仅仅根据源文件中的现有值来评估任何内容,它所做的只是将一个值的人工数据依赖性引入另一个值:当评估seq的结果时,第一个参数还必须(有点;见下文)进行评估。

     

例如,假设x :: Integer,则seq x b的行为与if x == 0 then b else b基本相同 - 无条件等于b,但在此过程中强制x。特别是,表达式x `seq` x完全是多余的,并且始终与仅写x具有完全相同的效果。

第一段所说的是写seq a b并不意味着a会立即神奇地评估,这意味着a会在b后立即进行评估需要进行评估。这可能会在程序的后期发生,也可能永远不会发生。当你从那个角度看待它时,显而易见的是seq x x是一种冗余,因为它只是说,“只要x需要评估就评估x。”如果你刚刚写了x,那当然会发生什么。

这对您有两个含义:

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

    抓一点。您的严格iterate'与我的爆炸模式版本完全相同。见评论。

  2. (\z -> z `seq` z)并没有给你一个严格的身份功能,我认为这是你想要的。事实上,共同的身份功能与您将获得的一样严格 - 它会在需要时立即评估其结果。

  3. 然而,我偷看了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]:What core U.zipWith (+) y . id gets translated into.