F#
F#パフォーマンスのヒントとテクニック
サーチ…
効率的な反復のためのテール再帰の使用
多くの開発者は、命令型言語から、 F#
がbreak
、 continue
または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-loop
。 ILSpy
を使用すると、関数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で動作しているときのテスト結果:
私たちは劇的な違いを見て、予期せず悪い結果を出すものもあります。
悪いケースを見てみましょう:
リスト
// 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)));
これはまずSeq
を1..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;
}
Seq
は1..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
を使用する必要があります。
他のケースでは、ほぼ同様の処理が行われます。
パフォーマンスは大きなテスト実行のために低下する理由は、呼び出しのオーバーヘッドということであるaction
私たちが少なく仕事をして成長しているaction
。
0
ルーピングすると、CPUレジスタを節約できるためパフォーマンス上の利点が得られることがありますが、この場合CPUには余裕を持ってレジスタがあるため、違いがないようです。
結論
測定は重要です。そうしないと、これらの選択肢はすべて同じだと思うかもしれませんが、いくつかの選択肢は他の選択肢よりも270倍も遅いからです。
検証ステップでは、実行可能ファイルをリバースエンジニアリングすることで、私たちがなぜ私たちが期待したパフォーマンスを得たか、または得られなかったのかを説明します。さらに、検証は、適切な測定を行うことが難しい場合のパフォーマンスの予測に役立ちます。
常にパフォーマンスを予測することは難しい。測定する。常にパフォーマンスの前提を確認する。
異なるF#データパイプラインの比較
F#
には、 List
、 Seq
、 Array
など、データパイプラインを作成するための多くのオプションがあります。
メモリ使用量とパフォーマンスの観点から、どのデータパイプラインが望ましいですか?
これに答えるために、異なるパイプラインを使用してパフォーマンスとメモリ使用量を比較します。
データパイプライン
オーバーヘッドを測定するために、処理されるアイテムごとの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
のサイズは変えますが、作業の総数は同じにします。
データパイプラインの代替
以下の選択肢を比較します。
- 命令コード
- 配列(非遅延)
- リスト(非遅延)
- LINQ(レイジープルストリーム)
- Seq(レイジープルストリーム)
- ネッソス(レイジープル/プッシュストリーム)
- PullStream(単純なプルストリーム)
- PushStream(シンプルなプッシュストリーム)
データパイプラインではありませんが、CPUがコードをどのように実行するかに最も近いので、 Imperative
コードと比較します。データパイプラインのパフォーマンスオーバーヘッドを測定できるように、その結果を計算するための最速の方法です。
Array
とList
完全な計算Array
/ List
ように、我々はメモリのオーバーヘッドを期待して、各ステップで。
LINQ
とSeq
はどちらもlazy pullストリームであるIEnumerable<'T>
基づいています(プルとは、コンシューマストリームがプロデューサストリームからデータを引き出していることを意味します)。したがって、パフォーマンスとメモリの使用率は同じであると考えています。
Nessos
はプッシュ&プル両方をサポートする高性能ストリームライブラリです(Java Stream
)。
PullStreamとPushStreamは、 Pull
& Push
ストリームの単純化された実装です。
実行結果:F#4.0 - .NET 4.6.1 - x64
バーは経過時間を示し、低い方が良い。有用な作業の総量はすべてのテストで同じですので、結果は同等です。これはまた、実行が少ないほど大きなデータセットを意味することを意味します。
測定時にはいつものように興味深い結果が見られます。
-
List
パフォーマンスの低さは、大規模なデータセットの他の選択肢と比較されます。これは、GC
やキャッシュのローカリティが悪いためです。 -
Array
パフォーマンスが予想以上に向上しました。 -
LINQ
はSeq
よりも優れていますが、これは両方ともIEnumerable<'T>
基づいているため予期しないことです。しかし、Seq
内部的にはすべてのアルゴリズムに対する一般的な実装をベースにしていますが、LINQ
は特殊なアルゴリズムを使用しています。 -
Push
はPull
よりも優れた性能を発揮します。これは、プッシュデータパイプラインのチェック数が少ないために予想されます - シンプルな
Push
データパイプラインはNessos
匹敵します。しかし、Nessos
はプルと並列処理をサポートしています。 - 小規模なデータパイプラインでは、パイプラインがオーバーヘッドを設定するため、
Nessos
のパフォーマンスが低下します。 - 予想通り、
Imperative
コードは最高
実行中のGCコレクション数:F#4.0 - .NET 4.6.1 - x64
バーは、試験中のGC
収集カウントの総数を示し、低い方が良い。これは、データパイプラインによって作成されるオブジェクトの数の測定値です。
測定時にはいつものように興味深い結果が見られます。
-
List
は本質的にノードの単一のリンクされたリストであるため、List
はArray
より多くのオブジェクトを作成することが予想されます。配列は連続したメモリ領域です。 - 基本となる数字を見ると、
List
&Array
は2つの世代のコレクションを強制します。この種のコレクションは高価です。 -
Seq
はコレクションの驚くべき量を引き起こしています。この点で、List
よりも驚くほど悪いです。 -
LINQ
、Nessos
、Push
およびPull
、ほとんど実行されないコレクションをトリガーします。しかし、オブジェクトはGC
最終的に実行されるように割り当てられます。 -
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