10

我正在尝试学习 F# 中的静态成员约束。通过阅读Tomas Petricek 的博客文章,我了解到编写一个inline“仅使用本身使用静态成员约束编写的操作”的函数将使我的函数对于满足这些约束的所有数字类型都能正常工作。这个问题表明它的inline工作方式与 c++ 模板有些相似,所以我没想到这两个函数之间有任何性能差异:

let MultiplyTyped (A : double[,]) (B : double[,]) =
    let rA, cA = (Array2D.length1 A) - 1, (Array2D.length2 A) - 1
    let cB = (Array2D.length2 B) - 1
    let C = Array2D.zeroCreate<double> (Array2D.length1 A) (Array2D.length2 B)
    for i = 0 to rA do
        for k = 0 to cA do
            for j = 0 to cB do
                C.[i,j] <- C.[i,j] + A.[i,k] * B.[k,j]
    C

let inline MultiplyGeneric (A : 'T[,]) (B : 'T[,]) =
    let rA, cA = Array2D.length1 A - 1, Array2D.length2 A - 1
    let cB = Array2D.length2 B - 1
    let C = Array2D.zeroCreate<'T> (Array2D.length1 A) (Array2D.length2 B)
    for i = 0 to rA do
        for k = 0 to cA do
            for j = 0 to cB do
                C.[i,j] <- C.[i,j] + A.[i,k] * B.[k,j]
    C

然而,将两个 1024 x 1024 矩阵相乘,MultiplyTyped在我的机器上平均需要 2550 毫秒,而MultiplyGeneric大约需要 5150 毫秒。我最初认为这zeroCreate是通用版本的错误,但是将该行更改为下面的行并没有什么不同。

let C = Array2D.init<'T> (Array2D.length1 A) (Array2D.length2 B) (fun i j -> LanguagePrimitives.GenericZero)

有什么我在这里想念的东西来做MultiplyGeneric一样的MultiplyTyped吗?或者这是预期的?

编辑:我应该提一下,这是 VS2010,F# 2.0,Win7 64bit,发布版本。平台目标是 x64(用于测试更大的矩阵) - 这会有所不同:x86 为这两个函数产生相似的结果。

奖励问题:推断的类型MultiplyGeneric如下:

val inline MultiplyGeneric :
   ^T [,] ->  ^T [,] ->  ^T [,]
    when ( ^T or  ^a) : (static member ( + ) :  ^T *  ^a ->  ^T) and
          ^T : (static member ( * ) :  ^T *  ^T ->  ^a)

^a类型从何而来?

编辑 2:这是我的测试代码:

let r = new System.Random()
let A = Array2D.init 1024 1024 (fun i j -> r.NextDouble())
let B = Array2D.init 1024 1024 (fun i j -> r.NextDouble())

let test f =
    let sw = System.Diagnostics.Stopwatch.StartNew()
    f() |> ignore
    sw.Stop()
    printfn "%A" sw.ElapsedMilliseconds

for i = 1 to 5 do
    test (fun () -> MultiplyTyped A B)

for i = 1 to 5 do
    test (fun () -> MultiplyGeneric A B)
4

2 回答 2

3

我想看看你的基准。我没有得到相同的结果(VS 2012 F# 3.0 Win 7 64 位)。

let m = Array2D.init 1024 1024 (fun i j -> float i * float j)

let test f =
  let sw = System.Diagnostics.Stopwatch.StartNew()
  f() |> ignore
  sw.Stop()
  printfn "%A" sw.Elapsed

test (fun () -> MultiplyTyped m m)
> 00:00:09.6013188

test (fun () -> MultiplyGeneric m m)
> 00:00:09.1686885

用 Reflector 反编译,函数看起来是一样的。

关于您的最后一个问题,推断出限制最少的约束。在这一行

C.[i,j] <- C.[i,j] + A.[i,k] * B.[k,j]

因为 的结果类型A.[i,k] * B.[k,j]未指定,并且立即传递给(+),所以可能涉及额外的类型。如果要收紧约束,可以将该行替换为

let temp : 'T = A.[i,k] * B.[k,j]
C.[i,j] <- C.[i,j] + temp

这会将签名更改为

val inline MultiplyGeneric :
  A: ^T [,] -> B: ^T [,] ->  ^T [,]
    when  ^T : (static member ( * ) :  ^T *  ^T ->  ^T) and
          ^T : (static member ( + ) :  ^T *  ^T ->  ^T)

编辑

使用您的测试,输出如下:

//MultiplyTyped
00:00:09.9904615
00:00:09.5489653
00:00:10.0562346
00:00:09.7023183
00:00:09.5123992
//MultiplyGeneric
00:00:09.1320273
00:00:08.8195283
00:00:08.8523408
00:00:09.2496603
00:00:09.2950196

这是对 ideone 的相同测试(在时间限制内进行了一些小的更改:512x512 矩阵和一次测试迭代)。它运行 F# 2.0 并产生类似的结果。

于 2013-02-21T16:39:39.333 回答
3

好问题。我将首先回答简单的部分:这^a只是自然泛化过程的一部分。想象一下你有一个这样的类型:

type T = | T with
    static member (+)(T, i:int) = T
    static member (*)(T, T) = 0

然后你仍然可以将你的MultiplyGeneric函数与这种类型的数组一起使用:将元素相乘AB得到ints,但这没关系,因为你仍然可以将它们添加到元素中C并取回类型的值T以存储回C.

至于你的表现问题,恐怕我没有很好的解释。您的基本理解是正确的 - using MultiplyGenericwith double[,]arguments 应该等同于 using MultiplyTyped。如果您使用 ildasm 查看编译器为以下 F# 代码生成的 IL:

let arr = Array2D.zeroCreate 1024 1024
let f1 = MultiplyTyped arr
let f2 = MultiplyGeneric arr

let timer = System.Diagnostics.Stopwatch()
timer.Start()

f1 arr |> ignore

printfn "%A" timer.Elapsed
timer.Restart()

f2 arr |> ignore

printfn "%A" timer.Elapsed

然后您可以看到编译器确实为它们中的每一个生成了相同的代码,将内联代码MultipyGeneric放入内部静态函数中。我在生成的代码中看到的唯一区别是本地人的名字,当从命令行运行时,我得到大致相等的经过时间。但是,从 FSI 运行时,我看到了与您报告的类似的差异。

我不清楚为什么会这样。在我看来,有两种可能性:

  1. FSI 的代码生成可能与静态编译器稍有不同
  2. CLR 的 JIT 编译器对运行时生成的代码的处理方式可能与编译后的代码略有不同。例如,正如我在上面提到的那样,我的代码MultiplyGeneric实际上使用了一个包含内联主体的内部方法。也许 CLR 的 JIT 处理公共方法和内部方法之间的差异时,它们在运行时生成时与在静态编译代码中时不同。
于 2013-02-21T17:10:25.333 回答