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一方、2番目のループは私に与えますReal: 00:00:00.051。怠惰は13倍遅い!!

この方法でコードを最適化しようとしましたが、シミュレーションコードが6倍遅くなりました。その後、減速が発生した場所を追跡するのは楽しかったです...

4

2 に答える 2

6

レイジーバージョンにはいくつかの重要なオーバーヘッドコードがあります-

 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に追加されました。残念ながら、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倍未満にします。

NB。私は新しい怠惰な実装に取り​​組みました...

于 2017-12-19T23:56:47.473 に答える