F#记录类型

时间:2016-10-18 23:21:06

标签: performance f# record

我正在尝试通过一堆函数传递的设置对象(有点像堆栈)。它有很多字段(混合字符,枚举字符,DU字符串,字符串),我想知道这项任务的最佳数据类型是什么(过去几年我已经回过头几次了......)。 / p>

虽然我目前使用的是本土数据类型,但它很慢而且不是线程安全的,所以我正在寻找一个更合乎逻辑的选择并决定尝试记录类型。

由于它们使用了复制和更新语义,因此认为F#编译器足够智能以仅更新必要数据并保留非变异数据似乎是合乎逻辑的,因为它无论如何都是不可变的。

所以我运行了几个测试,并期望在完整副本(每个字段更新)和单字段副本(一个字段更新)之间看到性能提升。但我不确定我是否使用正确的测试方法,或者其他数据类型是否更合适。

作为一个实际示例,我们采用XML限定名称。它们有一个小的本地部分和一个前缀(可以忽略)和一个长命名空间部分。命名空间大致相同,因此只需要更新本地部分。

type RecQName = { Ns :string; Prefix :string; Name :string }

// full update
let f a b c = { Ns = a; Prefix = b; Name = c}
let g() = f "http://test/is/a/long/name/here/and/there" "xs" "value"

// partial update
let test = g();;
let h a b = {a with Name = b }
let k() = h test "newname"

打开时间,在FSI(设置为Release,x64和Debug off)中,我得到(每次运行两次):

> for i in 0 .. 100000000 do g() |> ignore;;
Real: 00:00:01.412, CPU: 00:00:01.404, GC gen0: 637, gen1: 1, gen2: 1

> for i in 0 .. 100000000 do g() |> ignore;;
Real: 00:00:01.317, CPU: 00:00:01.310, GC gen0: 636, gen1: 0, gen2: 0

> for i in 0 .. 100000000 do k() |> ignore;;
Real: 00:00:01.191, CPU: 00:00:01.185, GC gen0: 636, gen1: 1, gen2: 0

> for i in 0 .. 100000000 do k() |> ignore;;
Real: 00:00:01.099, CPU: 00:00:01.092, GC gen0: 636, gen1: 0, gen2: 0

现在我知道时间并不是一切,并且有大约20%的明显差异,但这似乎很小以证明改变的合理性,并且很可能因为其他原因(例如,CLI可能会实施字符串)。

我做出错误的假设吗?我用Google搜索记录类型的性能,但它们都是关于将它们与结构进行比较。有人知道用于复制和更新的算法吗?有关此数据类型或其他内容是否更明智的选择的任何想法(给定许多字段,不仅仅是上面的三个字段,并且希望使用不变量而不使用复制和更新锁定)。

更新1

上面的测试看起来并没有真正测试任何东西,正如下一次测试所示。

type Rec = { A: int; B: int; C: int; D: int; E: int};;

// full
let f a b c d e = { A = a; B = b; C = c; D = d; E = e }

// partial
let temp = f 1 2 3 4 5
let g b = { temp with B = b }

// perf tests (subtract is necessary or the compiler optimizes them away)
let mutable res = 0;;
for i in 0 .. 1000000000 do f i (i-1) (i-2) (i-3) (i-4) |> function { B = b } -> if b < 1000 then res <- b
for i in 0 .. 1000000000 do g i |> function { B = b } -> if b < 1000 then res <- b

结果:

> for i in 0 .. 1000000000 do f i (i-1) (i-2) (i-3) (i-4) |> function { B = b } -> if b < 1000 then res <- b ;;
Real: 00:00:09.039, CPU: 00:00:09.032, GC gen0: 6358, gen1: 1, gen2: 0

> for i in 0 .. 1000000000 do g i |> function { B = b } -> if b < 1000 then res <- b;;
Real: 00:00:10.571, CPU: 00:00:10.576, GC gen0: 6358, gen1: 2, gen2: 0

现在差异,虽然出乎意料,但显示出来。看起来复制肯定不比不复制快。在这种情况下复制的开销可能是由于需要额外的堆栈插槽,我不知道。

至少它表明没有任何魔力继续存在(正如Fyodor在评论中已经提到的那样)。

更新2

好的,还有一次更新。如果我内联fg函数,则时间会变得非常不同,有利于部分更新。显然,内联具有这样的效果:编译器或JIT“知道”它不必执行完整副本,或者它只是将所有内容放在堆栈上的效果(JIT或编译器摆脱了装箱),可以看到0 GC集合。

> for i in 0 .. 1000000000 do f i (i-1) (i-2) (i-3) (i-4) |> function { B = b } -> if b < 1000 then res <- b ;;
Real: 00:00:08.885, CPU: 00:00:08.876, GC gen0: 6359, gen1: 1, gen2: 1

> for i in 0 .. 1000000000 do g i |> function { B = b } -> if b < 1000 then res <- b ;;
Real: 00:00:00.571, CPU: 00:00:00.561, GC gen0: 0, gen1: 0, gen2: 0

这是否适用于“现实世界”是值得商榷的,并且(当然)应该进行测试。这种改进是否与记录类型有任何关系,我对此表示怀疑。

0 个答案:

没有答案