サーチ…


効率的な反復のためのテール再帰の使用

多くの開発者は、命令型言語から、 F#breakcontinueまたはreturnサポートしていないため、早期に終了するfor-loopを書く方法を知ります。 F#の答えは、優れたパフォーマンスを提供しながら、反復する柔軟で慣用的な方法であるテール再帰を使用することです。

List tryFindを実装したいとします。 F#returnをサポートしていれば、 tryFindのように書くでしょう:

let tryFind predicate vs =
  for v in vs do
    if predicate v then
      return Some v
  None

これはF#では動作しません。代わりにtail-recursionを使って関数を記述します:

let tryFind predicate vs =
  let rec loop = function
    | v::vs -> if predicate v then 
                   Some v 
               else 
                   loop vs
    | _ -> None
  loop vs

F#コンパイラが関数がテール再帰的であることを検出すると、再帰を効率的なwhile-loopに書き換えるので、テール再帰はF#実行されwhile-loopILSpyを使用すると、関数loopでこれが当てはまることがわかりloop

internal static FSharpOption<a> loop@3-10<a>(FSharpFunc<a, bool> predicate, FSharpList<a> _arg1)
{
  while (_arg1.TailOrNull != null)
  {
    FSharpList<a> fSharpList = _arg1;
    FSharpList<a> vs = fSharpList.TailOrNull;
    a v = fSharpList.HeadOrDefault;
    if (predicate.Invoke(v))
    {
      return FSharpOption<a>.Some(v);
    }
    FSharpFunc<a, bool> arg_2D_0 = predicate;
    _arg1 = vs;
    predicate = arg_2D_0;
  }
  return null;
}

不要な割り当てとは別に(JIT-erがうまくいけば)、これは本質的に効率的なループです。

さらに、 F#ではテール再帰が慣用的であるため、変更可能な状態を避けることができます。 Listすべての要素を合計するsum関数を考えてみましょう。明白な最初の試行はこれでしょう:

let sum vs =
  let mutable s = LanguagePrimitives.GenericZero
  for v in vs do
    s <- s + v
  s

ループをtail-recursionに書き直すと、可変状態を避けることができます:

let sum vs =
  let rec loop s = function
    | v::vs -> loop (s + v) vs
    | _ -> s
  loop LanguagePrimitives.GenericZero vs

効率のため、 F#コンパイラはこれを可変状態を使用するwhile-loop変換します。

パフォーマンスの前提を測定して検証する

この例はF#を念頭に置いて書かれていますが、アイデアはすべての環境で適用可能です

パフォーマンスを最適化する際の最初のルールは、前提に頼らないことです。常にあなたの前提を測定し、確認してください。

マシンコードを直接記述するのではないので、コンパイラとJITがプログラムをマシンコードに変換する方法を予測することは難しいです。そのため、実行時間を測定して期待されるパフォーマンスの向上が見られ、実際のプログラムに隠されたオーバーヘッドが含まれていないことを確認する必要があります。

検証は、 ILSpyのようなツールを使用して実行可能ファイルをリバースエンジニアリングすることを含む非常に簡単なプロセスです。 JIT:erは、実際のマシンコードが厄介だが実行可能であることを確認することで検証を複雑にする。しかし、通常、 IL-codeを調べると、大きな利益が得られます。

より困難な問題は測定です。コードの改善を測定できる現実的な状況を設定するのは難しいため、難しいです。まだ測定は非常に貴重です。

シンプルなF#関数の解析

さまざまな方法で書かれた1..nすべての整数を累積する簡単なF#関数を調べてみましょう。範囲は単純な算術演算であるため、結果は直接計算できますが、この例では範囲を繰り返し処理します。

まず、関数の時間を測定するための便利な関数をいくつか定義します。

// now () returns current time in milliseconds since start
let now : unit -> int64 =
  let sw = System.Diagnostics.Stopwatch ()
  sw.Start ()
  fun () -> sw.ElapsedMilliseconds

// time estimates the time 'action' repeated a number of times
let time repeat action : int64*'T =
  let v = action ()  // Warm-up and compute value

  let b = now ()
  for i = 1 to repeat do
    action () |> ignore
  let e = now ()

  e - b, v

timeが反復してアクションを実行すると、分散を減らすために数百ミリ秒間テストを実行する必要があります。

次に、 1..nにすべての整数を累積する関数をいくつか定義します。

// Accumulates all integers 1..n using 'List'
let accumulateUsingList n =
  List.init (n + 1) id
  |> List.sum

// Accumulates all integers 1..n using 'Seq'
let accumulateUsingSeq n =
  Seq.init (n + 1) id
  |> Seq.sum

// Accumulates all integers 1..n using 'for-expression'
let accumulateUsingFor n =
  let mutable sum = 0
  for i = 1 to n do
    sum <- sum + i
  sum

// Accumulates all integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach n =
  let mutable sum = 0
  for i in 1..n do
    sum <- sum + i
  sum

// Accumulates all integers 1..n using 'foreach-expression' over list range
let accumulateUsingForEachOverList n =
  let mutable sum = 0
  for i in [1..n] do
    sum <- sum + i
  sum

// Accumulates every second integer 1..n using 'foreach-expression' over range
let accumulateUsingForEachStep2 n =
  let mutable sum = 0
  for i in 1..2..n do
    sum <- sum + i
  sum

// Accumulates all 64 bit integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach64 n =
  let mutable sum = 0L
  for i in 1L..int64 n do
    sum <- sum + i
  sum |> int

// Accumulates all integers n..1 using 'for-expression' in reverse order
let accumulateUsingReverseFor n =
  let mutable sum = 0
  for i = n downto 1 do
    sum <- sum + i
  sum

// Accumulates all 64 integers n..1 using 'tail-recursion' in reverse order
let accumulateUsingReverseTailRecursion n =
  let rec loop sum i =
    if i > 0 then
      loop (sum + i) (i - 1)
    else
      sum
  loop 0 n

結果は同じとみなされます( 2インクリメントを使用する関数の1つを除く)が、パフォーマンスに違いがあります。これを測定するには、次の関数が定義されています。

let testRun (path : string) =
  use testResult = new System.IO.StreamWriter (path)
  let write   (l : string)  = testResult.WriteLine l
  let writef  fmt           = FSharp.Core.Printf.kprintf write fmt

  write "Name\tTotal\tOuter\tInner\tCC0\tCC1\tCC2\tTime\tResult"

  // total is the total number of iterations being executed
  let total   = 10000000
  // outers let us variate the relation between the inner and outer loop
  //  this is often useful when the algorithm allocates different amount of memory
  //  depending on the input size. This can affect cache locality
  let outers  = [| 1000; 10000; 100000 |]
  for outer in outers do
    let inner = total / outer

    // multiplier is used to increase resolution of certain tests that are significantly
    //  faster than the slower ones

    let testCases =
      [|
    //   Name of test                         multiplier    action
        "List"                              , 1           , accumulateUsingList
        "Seq"                               , 1           , accumulateUsingSeq
        "for-expression"                    , 100         , accumulateUsingFor
        "foreach-expression"                , 100         , accumulateUsingForEach
        "foreach-expression over List"      , 1           , accumulateUsingForEachOverList
        "foreach-expression increment of 2" , 1           , accumulateUsingForEachStep2
        "foreach-expression over 64 bit"    , 1           , accumulateUsingForEach64
        "reverse for-expression"            , 100         , accumulateUsingReverseFor
        "reverse tail-recursion"            , 100         , accumulateUsingReverseTailRecursion
      |]
    for name, multiplier, a in testCases do
      System.GC.Collect (2, System.GCCollectionMode.Forced, true)
      let cc g = System.GC.CollectionCount g

      printfn "Accumulate using %s with outer=%d and inner=%d ..." name outer inner

      // Collect collection counters before test run
      let pcc0, pcc1, pcc2 = cc 0, cc 1, cc 2

      let ms, result       = time (outer*multiplier) (fun () -> a inner)
      let ms               = (float ms / float multiplier)

      // Collect collection counters after test run
      let acc0, acc1, acc2 = cc 0, cc 1, cc 2
      let cc0, cc1, cc2    = acc0 - pcc0, acc1 - pcc1, acc1 - pcc1
      printfn "  ... took: %f ms, GC collection count %d,%d,%d and produced %A" ms cc0 cc1 cc2 result

      writef "%s\t%d\t%d\t%d\t%d\t%d\t%d\t%f\t%d" name total outer inner cc0 cc1 cc2 ms result

.NET 4.5.2 x64で動作しているときのテスト結果:

.NET 4.5.2 x64のテスト結果

私たちは劇的な違いを見て、予期せず悪い結果を出すものもあります。

悪いケースを見てみましょう:

リスト

// Accumulates all integers 1..n using 'List'
let accumulateUsingList n =
  List.init (n + 1) id
  |> List.sum

ここで起こるのは、すべての整数を含む完全なリストです1..nは合計を使用して作成され、縮小されます。これは、範囲を反復して累積するよりも高価なはずです。forループよりも約42倍遅いようです。

さらに、コードでは多くのオブジェクトが割り当てられているため、テスト実行中にGCが約100倍実行されたことがわかります。これはCPUのコストもかかります。

Seq

// Accumulates all integers 1..n using 'Seq'
let accumulateUsingSeq n =
  Seq.init (n + 1) id
  |> Seq.sum

Seqバージョンは完全List割り当てないので、これは〜forループよりも270倍遅いというのは驚くべきことです。さらに、GCが661xを実行したことがわかります。

Seqは、1項目あたりの作業量が非常に少ない場合(この場合、2つの整数を集計する場合)、非効率的です。

要点は決してSeq使用しないことです。ポイントは測定することです。

(manofstick編集: Seq.init 、この深刻なパフォーマンスの問題の原因であり、式を使用するはるかefficentある。 { 0 .. n }の代わりSeq.init (n+1) idこれはまだはるかに効率的になるであろう。 このPRがマージされてリリースされても、元のSeq.init ... |> Seq.sumはまだ遅くなりますが、やや直感的には、 Seq.init ... |> Seq.map id |> Seq.sumは非常に高速ですが、これはSeq.initの実装との下位互換性を維持するためで、 Current最初に計算するのではなく、 Lazyオブジェクトでラップします。 このPR 。編集者への注:残念ながらこれはちょっとしたメモですが、改善がすぐに終わったときに人々をSeqから離れることは望ましくありません。 その時が来たら、チャートを更新するのが良いでしょうこれはこのページにあります

リスト上のforeach-expression

// Accumulates all integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach n =
  let mutable sum = 0
  for i in 1..n do
    sum <- sum + i
  sum

// Accumulates all integers 1..n using 'foreach-expression' over list range
let accumulateUsingForEachOverList n =
  let mutable sum = 0
  for i in [1..n] do
    sum <- sum + i
  sum

これらの2つの機能の違いは非常に微妙ですが、パフォーマンスの差は約76倍ではありません。どうして?悪いコードをリバースエンジニアリングしましょう:

public static int accumulateUsingForEach(int n)
{
  int sum = 0;
  int i = 1;
  if (n >= i)
  {
    do
    {
      sum += i;
      i++;
    }
    while (i != n + 1);
  }
  return sum;
}

public static int accumulateUsingForEachOverList(int n)
{
  int sum = 0;
  FSharpList<int> fSharpList = SeqModule.ToList<int>(Operators.CreateSequence<int>(Operators.OperatorIntrinsics.RangeInt32(1, 1, n)));
  for (FSharpList<int> tailOrNull = fSharpList.TailOrNull; tailOrNull != null; tailOrNull = fSharpList.TailOrNull)
  {
    int i = fSharpList.HeadOrDefault;
    sum += i;
    fSharpList = tailOrNull;
  }
  return sum;
}

accumulateUsingForEachは効率的なwhileループとして実装されますがfor i in [1..n]は以下のように変換されます:

FSharpList<int> fSharpList =
  SeqModule.ToList<int>(
    Operators.CreateSequence<int>(
      Operators.OperatorIntrinsics.RangeInt32(1, 1, n)));

これはまずSeq1..n以上作成し、最後に1..nを呼び出しToList

高価な。

foreach-expressionのインクリメント2

// Accumulates all integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach n =
  let mutable sum = 0
  for i in 1..n do
    sum <- sum + i
  sum

// Accumulates every second integer 1..n using 'foreach-expression' over range
let accumulateUsingForEachStep2 n =
  let mutable sum = 0
  for i in 1..2..n do
    sum <- sum + i
  sum

もう一度これらの2つの機能の違いは微妙ですが、パフォーマンスの差は残酷です:〜25x

もう一度ILSpy実行しましょう:

public static int accumulateUsingForEachStep2(int n)
{
  int sum = 0;
  IEnumerable<int> enumerable = Operators.OperatorIntrinsics.RangeInt32(1, 2, n);
  foreach (int i in enumerable)
  {
    sum += i;
  }
  return sum;
}

Seq1..2..nで作成され、次に列挙子を使用してSeqを反復処理します。

私たちはF#が次のようなものを作ることを期待していました。

public static int accumulateUsingForEachStep2(int n)
{
  int sum = 0;
  for (int i = 1; i < n; i += 2)
  {
    sum += i;
  }
  return sum;
}

しかし、 F#コンパイラは、1だけインクリメントするint32の範囲で効率的なforループをサポートします。それ以外の場合は、 Operators.OperatorIntrinsics.RangeInt32に戻ります。次の驚くべき結果を説明します

64ビットを超えるforeach-expression

// Accumulates all 64 bit integers 1..n using 'foreach-expression' over range
let accumulateUsingForEach64 n =
  let mutable sum = 0L
  for i in 1L..int64 n do
    sum <- sum + i
  sum |> int

これはforループより47倍遅く実行されますが、唯一の違いは64ビット整数を反復することです。 ILSpyは私たちに次の理由を示します:

public static int accumulateUsingForEach64(int n)
{
  long sum = 0L;
  IEnumerable<long> enumerable = Operators.OperatorIntrinsics.RangeInt64(1L, 1L, (long)n);
  foreach (long i in enumerable)
  {
    sum += i;
  }
  return (int)sum;
}

F#は、 int32番号のための効率的なforループのみをサポートします。これは、フォールバックOperators.OperatorIntrinsics.RangeInt64を使用する必要があります。

他のケースでは、ほぼ同様の処理が行われます。

.NET 4.5.2 x64のテスト結果

パフォーマンスは大きなテスト実行のために低下する理由は、呼び出しのオーバーヘッドということであるaction私たちが少なく仕事をして成長しているaction

0ルーピングすると、CPUレジスタを節約できるためパフォーマンス上の利点が得られることがありますが、この場合CPUには余裕を持ってレジスタがあるため、違いがないようです。

結論

測定は重要です。そうしないと、これらの選択肢はすべて同じだと思うかもしれませんが、いくつかの選択肢は他の選択肢よりも270倍も遅いからです。

検証ステップでは、実行可能ファイルをリバースエンジニアリングすることで、私たちがなぜ私たちが期待したパフォーマンスを得たか、または得られなかったのを説明します。さらに、検証は、適切な測定を行うことが難しい場合のパフォーマンスの予測に役立ちます。

常にパフォーマンスを予測することは難しい。測定する。常にパフォーマンスの前提を確認する。

異なるF#データパイプラインの比較

F#には、 ListSeqArrayなど、データパイプラインを作成するための多くのオプションがあります。

メモリ使用量とパフォーマンスの観点から、どのデータパイプラインが望ましいですか?

これに答えるために、異なるパイプラインを使用してパフォーマンスとメモリ使用量を比較します。

データパイプライン

オーバーヘッドを測定するために、処理されるアイテムごとのCPUコストが低いデータパイプラインを使用します。

let seqTest n =
  Seq.init (n + 1) id
  |> Seq.map    int64
  |> Seq.filter (fun v -> v % 2L = 0L)
  |> Seq.map    ((+) 1L)
  |> Seq.sum

すべての代替案に対応するパイプラインを作成して比較します。

nのサイズは変えますが、作業の総数は同じにします。

データパイプラインの代替

以下の選択肢を比較します。

  1. 命令コード
  2. 配列(非遅延)
  3. リスト(非遅延)
  4. LINQ(レイジープルストリーム)
  5. Seq(レイジープルストリーム)
  6. ネッソス(レイジープル/プッシュストリーム)
  7. PullStream(単純なプルストリーム)
  8. PushStream(シンプルなプッシュストリーム)

データパイプラインではありませんが、CPUがコードをどのように実行するかに最も近いので、 Imperativeコードと比較します。データパイプラインのパフォーマンスオーバーヘッドを測定できるように、その結​​果を計算するための最速の方法です。

ArrayList完全な計算Array / Listように、我々はメモリのオーバーヘッドを期待して、各ステップで。

LINQSeqはどちらもlazy pullストリームであるIEnumerable<'T>基づいています(プルとは、コンシューマストリームがプロデューサストリームからデータを引き出していることを意味します)。したがって、パフォーマンスとメモリの使用率は同じであると考えています。

Nessosはプッシュ&プル両方をサポートする高性能ストリームライブラリです(Java Stream )。

PullStreamとPushStreamは、 PullPushストリームの単純化された実装です。

実行結果:F#4.0 - .NET 4.6.1 - x64

実行結果:F#4.0  -  .NET 4.6.1  -  x64

バーは経過時間を示し、低い方が良い。有用な作業の総量はすべてのテストで同じですので、結果は同等です。これはまた、実行が少ないほど大きなデータセットを意味することを意味します。

測定時にはいつものように興味深い結果が見られます。

  1. Listパフォーマンスの低さは、大規模なデータセットの他の選択肢と比較されます。これは、 GCやキャッシュのローカリティが悪いためです。
  2. Arrayパフォーマンスが予想以上に向上しました。
  3. LINQSeqよりも優れていますが、これは両方ともIEnumerable<'T>基づいているため予期しないことです。しかし、 Seq内部的にはすべてのアルゴリズムに対する一般的な実装をベースにしていますが、 LINQは特殊なアルゴリズムを使用しています。
  4. PushPullよりも優れた性能を発揮します。これは、プッシュデータパイプラインのチェック数が少ないために予想されます
  5. シンプルなPushデータパイプラインはNessos匹敵します。しかし、 Nessosはプルと並列処理をサポートしています。
  6. 小規模なデータパイプラインでは、パイプラインがオーバーヘッドを設定するため、 Nessosのパフォーマンスが低下します。
  7. 予想通り、 Imperativeコードは最高

実行中のGCコレクション数:F#4.0 - .NET 4.6.1 - x64

実行中のGCコレクション数:F#4.0  -  .NET 4.6.1  -  x64

バーは、試験中のGC収集カウントの総数を示し、低い方が良い。これは、データパイプラインによって作成されるオブジェクトの数の測定値です。

測定時にはいつものように興味深い結果が見られます。

  1. Listは本質的にノードの単一のリンクされたリストであるため、 ListArrayより多くのオブジェクトを作成することが予想されます。配列は連続したメモリ領域です。
  2. 基本となる数字を見ると、 ListArrayは2つの世代のコレクションを強制します。この種のコレクションは高価です。
  3. Seqはコレクションの驚くべき量を引き起こしています。この点で、 Listよりも驚くほど悪いです。
  4. LINQNessosPushおよびPull 、ほとんど実行されないコレクションをトリガーします。しかし、オブジェクトはGC最終的に実行されるように割り当てられます。
  5. Imperativeコードはオブジェクトを割り当てないので、 GCコレクションはトリガされませんでした。

結論

すべてのデータパイプラインはすべてのテストケースで同じ量の有用な作業を行いますが、異なるパイプライン間でパフォーマンスとメモリ使用量に大きな違いがあります。

さらに、データパイプラインのオーバーヘッドは、処理されるデータのサイズによって異なります。たとえば、小さなサイズの場合、 Arrayは非常にうまく機能しています。

オーバーヘッドを測定するには、パイプラインの各ステップで実行される作業量が非常に少ないことに注意してください。実際の作業ではSeqのオーバーヘッドは重要ではないかもしれません。なぜなら、実際の作業にはより時間がかかるからです。

より重要なのは、メモリ使用量の違いです。 GCはフリーではありません。長時間使用するアプリケーションでは、 GC圧力を下げることが有益です。

パフォーマンスとメモリの使用を懸念しているF#開発者にとっては、 Nessos Streamsをチェックアウトすることをお勧めします。

戦略的に配置された最高のパフォーマンスが必要な場合は、 Imperativeコードを検討する価値があります。

最後に、パフォーマンスになると仮定しないでください。測定と検証。

完全なソースコード:

module PushStream =
  type Receiver<'T> = 'T -> bool
  type Stream<'T>   = Receiver<'T> -> unit

  let inline filter (f : 'T -> bool) (s : Stream<'T>) : Stream<'T> =
    fun r -> s (fun v -> if f v then r v else true)

  let inline map (m : 'T -> 'U) (s : Stream<'T>) : Stream<'U> =
    fun r -> s (fun v -> r (m v))

  let inline range b e : Stream<int> =
    fun r ->
      let rec loop i = if i <= e && r i then loop (i + 1)
      loop b

  let inline sum (s : Stream<'T>) : 'T =
    let mutable state = LanguagePrimitives.GenericZero<'T>
    s (fun v -> state <- state + v; true)
    state

module PullStream =

  [<Struct>]
  [<NoComparison>]
  [<NoEqualityAttribute>]
  type Maybe<'T>(v : 'T, hasValue : bool) =
    member    x.Value        = v
    member    x.HasValue     = hasValue
    override  x.ToString ()  =
      if hasValue then
        sprintf "Just %A" v
      else
        "Nothing"

  let Nothing<'T>     = Maybe<'T> (Unchecked.defaultof<'T>, false)
  let inline Just v   = Maybe<'T> (v, true)

  type Iterator<'T> = unit -> Maybe<'T>
  type Stream<'T>   = unit -> Iterator<'T>

  let filter (f : 'T -> bool) (s : Stream<'T>) : Stream<'T> =
    fun () ->
      let i = s ()
      let rec pop () =
        let mv = i ()
        if mv.HasValue then
          let v = mv.Value
          if f v then Just v else pop ()
        else
          Nothing
      pop

  let map (m : 'T -> 'U) (s : Stream<'T>) : Stream<'U> =
    fun () ->
      let i = s ()
      let pop () =
        let mv = i ()
        if mv.HasValue then
          Just (m mv.Value)
        else
          Nothing
      pop

  let range b e : Stream<int> =
    fun () ->
      let mutable i = b
      fun () ->
        if i <= e then
          let p = i
          i <- i + 1
          Just p
        else
          Nothing

  let inline sum (s : Stream<'T>) : 'T =
    let i = s ()
    let rec loop state =
      let mv = i ()
      if mv.HasValue then
        loop (state + mv.Value)
      else
        state
    loop LanguagePrimitives.GenericZero<'T>

module PerfTest =

  open System.Linq
#if USE_NESSOS
  open Nessos.Streams
#endif

  let now =
    let sw = System.Diagnostics.Stopwatch ()
    sw.Start ()
    fun () -> sw.ElapsedMilliseconds

  let time n a =
    let inline cc i       = System.GC.CollectionCount i

    let v                 = a ()

    System.GC.Collect (2, System.GCCollectionMode.Forced, true)

    let bcc0, bcc1, bcc2  = cc 0, cc 1, cc 2
    let b                 = now ()

    for i in 1..n do
      a () |> ignore

    let e = now ()
    let ecc0, ecc1, ecc2  = cc 0, cc 1, cc 2

    v, (e - b), ecc0 - bcc0, ecc1 - bcc1, ecc2 - bcc2

  let arrayTest n =
    Array.init (n + 1) id
    |> Array.map    int64
    |> Array.filter (fun v -> v % 2L = 0L)
    |> Array.map    ((+) 1L)
    |> Array.sum

  let imperativeTest n =
    let rec loop s i =
      if i >= 0L then
        if i % 2L = 0L then
          loop (s + i + 1L) (i - 1L)
        else
          loop s (i - 1L)
      else
        s
    loop 0L (int64 n)

  let linqTest n =
    (((Enumerable.Range(0, n + 1)).Select int64).Where(fun v -> v % 2L = 0L)).Select((+) 1L).Sum()

  let listTest n =
    List.init (n + 1) id
    |> List.map     int64
    |> List.filter  (fun v -> v % 2L = 0L)
    |> List.map     ((+) 1L)
    |> List.sum

#if USE_NESSOS
  let nessosTest n =
    Stream.initInfinite id
    |> Stream.take    (n + 1)
    |> Stream.map     int64
    |> Stream.filter  (fun v -> v % 2L = 0L)
    |> Stream.map     ((+) 1L)
    |> Stream.sum
#endif

  let pullTest n =
    PullStream.range 0 n
    |> PullStream.map     int64
    |> PullStream.filter  (fun v -> v % 2L = 0L)
    |> PullStream.map     ((+) 1L)
    |> PullStream.sum

  let pushTest n =
    PushStream.range 0 n
    |> PushStream.map     int64
    |> PushStream.filter  (fun v -> v % 2L = 0L)
    |> PushStream.map     ((+) 1L)
    |> PushStream.sum

  let seqTest n =
    Seq.init (n + 1) id
    |> Seq.map    int64
    |> Seq.filter (fun v -> v % 2L = 0L)
    |> Seq.map    ((+) 1L)
    |> Seq.sum

  let perfTest (path : string) =
    let testCases =
      [|
        "array"       , arrayTest       
        "imperative"  , imperativeTest  
        "linq"        , linqTest        
        "list"        , listTest        
        "seq"         , seqTest         
#if USE_NESSOS
        "nessos"      , nessosTest      
#endif
        "pull"        , pullTest        
        "push"        , pushTest        
      |]
    use out                   = new System.IO.StreamWriter (path)
    let write (msg : string)  = out.WriteLine msg
    let writef fmt            = FSharp.Core.Printf.kprintf write fmt

    write "Name\tTotal\tOuter\tInner\tElapsed\tCC0\tCC1\tCC2\tResult"

    let total   = 10000000
    let outers = [| 10; 1000; 1000000 |]
    for outer in outers do
      let inner = total / outer
      for name, a in testCases do
        printfn "Running %s with total=%d, outer=%d, inner=%d ..." name total outer inner
        let v, ms, cc0, cc1, cc2 = time outer (fun () -> a inner)
        printfn "  ... %d ms, cc0=%d, cc1=%d, cc2=%d, result=%A" ms cc0 cc1 cc2 v
        writef "%s\t%d\t%d\t%d\t%d\t%d\t%d\t%d\t%d" name total outer inner ms cc0 cc1 cc2 v

[<EntryPoint>]
let main argv =
  System.Environment.CurrentDirectory <- System.AppDomain.CurrentDomain.BaseDirectory
  PerfTest.perfTest "perf.tsv"
  0


Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow