我正在尝试学习F#中的静态成员约束。从阅读Tomas Petricek's blog post开始,我理解编写一个inline
函数“仅使用自己使用静态成员约束编写的操作”将使我的函数对于满足这些约束的所有数值类型都能正常工作。 This question表示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 ms,而MultiplyGeneric
大约需要5150 ms。我最初认为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)
答案 0 :(得分: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
这里是the same test on ideone(在时间限制内进行了一些小的改动:512x512矩阵和一次测试迭代)。它运行F#2.0并产生类似的结果。
答案 1 :(得分:3)
好问题。我将首先回答简单的部分:^a
只是自然概括过程的一部分。想象一下你有这样的类型:
type T = | T with
static member (+)(T, i:int) = T
static member (*)(T, T) = 0
然后,您仍然可以将此MultiplyGeneric
函数与此类数组一起使用:A
和B
的乘法元素将为您提供int
,但这没关系,因为您仍然可以将它们添加到C
的元素中,并返回T
类型的值以存储回C
。
至于你的表现问题,我恐怕没有很好的解释。您的基本理解是正确的 - 使用MultiplyGeneric
double[,]
参数应该等同于使用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运行我看到的差异与您报告的差异相似。
我不清楚为什么会这样。我认为有两种可能性:
MultiplyGeneric
我的代码实际上会产生一个包含内联体的内部方法。也许CLR的JIT在运行时生成时处理公共方法和内部方法之间的区别,而不是在静态编译代码时处理它们。