14

这是代码:

{-# 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我用and编译,-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

版本 1 与原始版本相同,而版本 2 和 3 的运行时间不到 0.09 秒(并且INLINING f没有任何改变)。

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

我也有兴趣添加整数模q

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

与添加Int64s 时一样,如果我编写一个指定每种类型的函数,

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. 版本 2 和 3 中发生了什么f会以任何方式影响性能?对我来说,(相当于)编码风格会影响这样的性能似乎是一个错误。在 Vector 之外是否还有其他示例,其中部分应用函数或其他样式选择会影响性能?
  3. 为什么多态性会使我减慢一个数量级,而与多态性的位置无关(即在向量类型中、在Num类型中、两者中或幻像类型中)?我知道多态性会使代码变慢,但这很荒谬。它周围有黑客吗?

编辑 1

我向 Vector library 页面提出了问题。我发现了一个与此问题相关的GHC问题。

编辑2

在从@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我用and编译,-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

版本 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 秒。这让我想知道最初的问题是否可能是由于(缺乏)类型推断,即使一切都应该很好地推断出来。

以下是我的问题:

  1. 减速来自哪里?
  2. 为什么使用forcehelp 编写,但使用 strictiterate不使用?
  3. 为什么U.force比不上DeepSeq.force?我不知道U.force应该做什么,但听起来很像DeepSeq.force,并且似乎有类似的效果。
  4. 为什么多态性会使我减慢一个数量级,而与多态性的位置无关(即在向量类型中,在Num类型中,或两者兼而有之)?
  5. 为什么版本 5 和 6,它们都不应该有任何严格的含义,就像严格的函数一样快?

正如@kqr 指出的那样,问题似乎不在于严格性。因此,我编写函数的方式导致使用泛型zipWith而不是未装箱的特定版本。这只是 GHC 和 Vector 库之间的侥幸,还是可以在这里说一些更笼统的东西?

4

1 回答 1

13

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

第一件事是x `seq` x,在语义上和计算上,与 just 相同x。维基说seq

一个常见的误解seqseq x“评估” x。嗯,有点。seq不会仅仅因为源文件中存在而评估任何东西,它所做的只是引入一个值对另一个值的人为数据依赖性:当seq评估结果时,第一个参数也必须(有点;见下文)是评估。

举个例子,假设x :: Integer, then的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]

    刮那个。你的 strictiterate'和我的 bang 模式版本完全一样。见评论。

  2. 写作(\z -> z `seq` z)并没有给你一个严格的身份功能,我认为这就是你想要的。事实上,通用恒等函数与您将得到的一样严格——它会在需要时立即评估其结果。

但是,我查看了 GHC 生成的核心

U.zipWith (+) y

U.zipWith (+) y . id

我未经训练的眼睛只能发现一个很大的不同。第一个只使用一个普通Data.Vector.Generic.zipWith的(这就是你的多态巧合可能发挥作用的地方——如果 GHC 选择一个泛型zipWith,它当然会像代码是多态的一样执行!)而后者将这个单一的函数调用分解为近 90 行状态单子代码和未打包的机器类型。

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


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

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

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

于 2013-11-06T06:29:07.450 回答