3

为什么创建Lazy类型这么慢?

假设以下代码:

type T() =
  let v = lazy (0.0)
  member o.a = v.Value

type T2() =
  member o.a = 0.0

#time "on"

for i in 0 .. 10000000 do
  T() |> ignore

#time "on"

for i in 0 .. 10000000 do
  T2() |> ignore

第一个循环给了我:Real: 00:00:00.647而第二个循环给了我Real: 00:00:00.051. Lazy 慢了 13 倍!!

我曾尝试以这种方式优化我的代码,结果我的模拟代码慢了 6 倍。追溯减速发生的地方很有趣......

4

2 回答 2

6

Lazy 版本有一些重要的开销代码 -

 60     .method public specialname
 61            instance default float64 get_a ()  cil managed
 62     {
 63         // Method begins at RVA 0x2078
 64     // Code size 14 (0xe)
 65     .maxstack 3
 66     IL_0000:  ldarg.0
 67     IL_0001:  ldfld class [FSharp.Core]System.Lazy`1<float64> Test/T::v
 68     IL_0006:  tail.
 69     IL_0008:  call instance !0 class [FSharp.Core]System.Lazy`1<float64>::get_Value()
 70     IL_000d:  ret
 71     } // end of method T::get_a

将此与直接版本进行比较

 .method public specialname
130            instance default float64 get_a ()  cil managed
131     {
132         // Method begins at RVA 0x20cc
133     // Code size 10 (0xa)
134     .maxstack 3
135     IL_0000:  ldc.r8 0.
136     IL_0009:  ret
137     } // end of method T2::get_a

所以直接版本有一个加载然后返回,而间接版本有一个加载然后调用然后返回。

由于该lazy版本有一个额外的调用,我预计它会明显变慢。

更新: 所以我想知道我们是否可以创建一个lazy不需要方法调用的自定义版本——我还将测试更新为实际调用方法,而不仅仅是创建对象。这是代码:

type T() =
  let v = lazy (0.0)
  member o.a() = v.Value

type T2() =
  member o.a() = 0.0

type T3() = 
  let mutable calculated = true
  let mutable value = 0.0
  member o.a() = if calculated then value else failwith "not done";;

#time "on"
let lazy_ = 
  for i in 0 .. 1000000 do
    T().a() |> ignore
  printfn "lazy"
#time "on"
let fakelazy = 
  for i in 0 .. 1000000 do
    T3().a() |> ignore
  printfn "fake lazy"

#time "on"
let direct = 
  for i in 0 .. 1000000 do
    T2().a() |> ignore
  printfn "direct";;

这给出了以下结果:

lazy
Real: 00:00:03.786, CPU: 00:00:06.443, GC gen0: 7

val lazy_ : unit = ()


--> Timing now on

fake lazy
Real: 00:00:01.627, CPU: 00:00:02.858, GC gen0: 2

val fakelazy : unit = ()


--> Timing now on

direct
Real: 00:00:01.759, CPU: 00:00:02.935, GC gen0: 2

val direct : unit = ()

这里的lazy版本只比直接版本慢 2 倍,而伪造的惰性版本甚至比直接版本快一点——这可能是由于在基准测试期间发生了 GC。

于 2012-08-20T09:22:36.743 回答
1

更新 .net 核心世界

一个新的构造函数被添加到 Lazy 来处理常量,比如你的 case。不幸的是,F# 的lazy“伪关键字”总是(目前!)将常量包装为函数。

无论如何,如果你改变:

let v = lazy (0.0)

至:

let v = Lazy<_> 0.0 // NB. Only .net core at the moment

然后你会发现你的T()课只需要你的 3 倍左右T2

(有一个惰性常量有什么意义?这意味着当您确实混合了常量和真正的惰性项时,您可以使用惰性作为抽象,开销很小......)

...和...

如果您实际上多次使用创建的值,则开销会进一步缩小。即诸如:

open System.Diagnostics

type T() =
  let v = Lazy<_> 0.1
  member o.a () = v.Value

type T2() =
  member o.a () = 0.1

let withLazyType () =
    let mutable sum = 0.0
    for i in 0 .. 10000000 do
        let t = T()
        for __ = 1 to 10 do
            sum <- sum + t.a()
    sum

let withoutLazyType () =
    let mutable sum = 0.0
    for i in 0 .. 10000000 do
        let t = T2()
        for __ = 1 to 10 do
            sum <- sum + t.a()
    sum

let runtest name count f =
    let mutable checksum = 0.
    let mutable totaltime = 0L
    for i = 0 to count do
        if i = 0 then
            f () |> ignore // warm up
        else
            let sw = Stopwatch.StartNew ()
            checksum <- checksum + f ()
            totaltime <- totaltime + sw.ElapsedMilliseconds
    printfn "%s: %4d (checksum=%f for %d runs)" name (totaltime/int64 count) checksum count

[<EntryPoint>]
let main _ =
    runtest "w/o  lazy" 10 withoutLazyType
    runtest "with lazy" 10 withLazyType

    0

使时间差小于 2 倍。

注意。我致力于新的惰性实现......

于 2017-12-19T23:56:47.473 回答