2016-09-06 7 views
7

確かに、リンゴとリンゴとリンゴを正確に比較しているかどうかは不明です。しかし、私は特に、差があれば、その差が軽微な場合には、その驚くべき差に驚いています。F#の関数合成が配管よりも60%遅いのはなぜですか?

配管can often be expressed as function composition and vice versa、と私は、コンパイラがそれも知っているので、私は少し実験を試みたと仮定します:FSIにこの

// simplified example of some SB helpers: 
let inline bcreate() = new StringBuilder(64) 
let inline bget (sb: StringBuilder) = sb.ToString() 
let inline appendf fmt (sb: StringBuilder) = Printf.kbprintf (fun() -> sb) sb fmt 
let inline appends (s: string) (sb: StringBuilder) = sb.Append s 
let inline appendi (i: int) (sb: StringBuilder) = sb.Append i 
let inline appendb (b: bool) (sb: StringBuilder) = sb.Append b 

// test function for composition, putting some garbage data in SB 
let compose a =    
    (appends "START" 
    >> appendb true 
    >> appendi 10 
    >> appendi a 
    >> appends "0x" 
    >> appendi 65535 
    >> appendi 10 
    >> appends "test" 
    >> appends "END") (bcreate()) 

// test function for piping, putting the same garbage data in SB 
let pipe a = 
    bcreate() 
    |> appends "START" 
    |> appendb true 
    |> appendi 10 
    |> appendi a 
    |> appends "0x" 
    |> appendi 65535 
    |> appendi 10 
    |> appends "test" 
    |> appends "END" 

テストを(64ビットが有効になって、上--optimizeフラグ)が与える:

> for i in 1 .. 500000 do compose 123 |> ignore;; 
Real: 00:00:00.390, CPU: 00:00:00.390, GC gen0: 62, gen1: 1, gen2: 0 
val it : unit =() 
> for i in 1 .. 500000 do pipe 123 |> ignore;; 
Real: 00:00:00.249, CPU: 00:00:00.249, GC gen0: 27, gen1: 0, gen2: 0 
val it : unit =() 

小さな違いは理解できますが、これは1.6(60%)のパフォーマンス低下要因です。

私は実際には多くの作業がStringBuilderで起こることを期待していますが、明らかに合成のオーバーヘッドにはかなりの影響があります。

ほとんどの実用的な状況では、この違いはごくわずかですが、この場合のように大きな形式のテキストファイル(ログファイルなど)を書くと、影響があります。

私はF#の最新バージョンを使用しています。

答えて

9

私はFSIとあなたの例を試してみたし、目立った差は認められなかっ:

> #time 
for i in 1 .. 500000 do compose 123 |> ignore 

--> Timing now on 

Real: 00:00:00.229, CPU: 00:00:00.234, GC gen0: 32, gen1: 32, gen2: 0 
val it : unit =() 
> #time;; 

--> Timing now off 

> #time 
for i in 1 .. 500000 do pipe 123 |> ignore;;;; 

--> Timing now on 

Real: 00:00:00.214, CPU: 00:00:00.218, GC gen0: 30, gen1: 30, gen2: 0 
val it : unit =() 

それを測定するBenchmarkDotNet(最初の表は、単に1つのコン/パイプランである第二の表は、私は似たような見つかっ)500000回それをやっている:

Method | Platform |  Jit |  Median |  StdDev | Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op | 
-------- |--------- |---------- |------------ |----------- |--------- |------ |------ |------------------- | 
compose |  X64 | RyuJit | 319.7963 ns | 5.0299 ns | 2,848.50 |  - |  - |    182.54 | 
    pipe |  X64 | RyuJit | 308.5887 ns | 11.3793 ns | 2,453.82 |  - |  - |    155.88 | 
compose |  X86 | LegacyJit | 428.0141 ns | 3.6112 ns | 1,970.00 |  - |  - |    126.85 | 
    pipe |  X86 | LegacyJit | 416.3469 ns | 8.0869 ns | 1,886.00 |  - |  - |    121.86 | 

    Method | Platform |  Jit |  Median | StdDev | Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op | 
-------- |--------- |---------- |------------ |---------- |--------- |------ |------ |------------------- | 
compose |  X64 | RyuJit | 160.8059 ms | 4.6699 ms | 3,514.75 |  - |  - |  56,224,980.75 | 
    pipe |  X64 | RyuJit | 163.1026 ms | 4.9829 ms | 3,120.00 |  - |  - |  50,025,686.21 | 
compose |  X86 | LegacyJit | 215.8562 ms | 4.2769 ms | 2,292.00 |  - |  - |  36,820,936.68 | 
    pipe |  X86 | LegacyJit | 209.9219 ms | 2.5605 ms | 2,220.00 |  - |  - |  35,554,575.32 | 

それはあなたが測定され差異がGCに関連していることもあります。あなたのタイミングの前後にGC収集を強制してください。

let inline (|>) x f = f x 

と組成演算子と比較する:

let inline (>>) f g x = g(f x) 

は、それが明確な組成オペレータがラムダを作成されることを確認しているようだパイプ演算子のためsource codeを見て、言った

より多くの割り当てをもたらすはずです。これは、BenchmarkDotNetの実行でも見ることができます。それはあなたが見ているパフォーマンスの違いの原因かもしれません。

+0

ありがとう、非常に興味深い比較。サーバーGCを使用していて、通常のシングルスレッドGCがありますか?私はFSIのためにそれを設定する方法を知らない。私はコンパイルされたバージョンを比較する必要があります。私は、あなたのシステムでは、その違いが無視されるべきであることがわかっています。 – Abel

+0

私はあなたが言及したものである '--optimize'以外の特別なフラグをFSIに使用していません。私はfsianycpu.exeも重要な場合に備えて走っています。 – Ringil

+1

@Ringil私はラムダについて同意しない。はい、それらは最適化されていないコードで作成されます。しかし、最適化をオンにすると、9つではなく2つのラムダが表示されます。他のすべてはインライン化されます。私は、コンパイラが配管の場合よりも構成の場合にインライン展開を計算するのにもっと時間がかかることが最善でなければならないと考えています。 –

6

のF#の内部についての深い知識がなくても、私が生成されたILから伝えることができることはpipeappend*に対するすべての呼び出しがインライン化されるのに対し、(最適化がオフになっている場合は、それらの多く)composeは、ラムダが得られるということです。

compose機能のためのIL生成
Main.pipe: 
IL_0000: nop   
IL_0001: ldc.i4.s 40 
IL_0003: newobj  System.Text.StringBuilder..ctor 
IL_0008: ldstr  "START" 
IL_000D: callvirt System.Text.StringBuilder.Append 
IL_0012: ldc.i4.1  
IL_0013: callvirt System.Text.StringBuilder.Append 
IL_0018: ldc.i4.s 0A 
IL_001A: callvirt System.Text.StringBuilder.Append 
IL_001F: ldarg.0  
IL_0020: callvirt System.Text.StringBuilder.Append 
IL_0025: ldstr  "0x" 
IL_002A: callvirt System.Text.StringBuilder.Append 
IL_002F: ldc.i4  FF FF 00 00 
IL_0034: callvirt System.Text.StringBuilder.Append 
IL_0039: ldc.i4.s 0A 
IL_003B: callvirt System.Text.StringBuilder.Append 
IL_0040: ldstr  "test" 
IL_0045: callvirt System.Text.StringBuilder.Append 
IL_004A: ldstr  "END" 
IL_004F: callvirt System.Text.StringBuilder.Append 
IL_0054: ret 

pipe機能のためのIL生成

Main.compose: 
IL_0000: nop   
IL_0001: ldarg.0  
IL_0002: newobj  [email protected] 
IL_0007: stloc.1  
IL_0008: ldloc.1  
IL_0009: newobj  [email protected] 
IL_000E: stloc.0  
IL_000F: ldc.i4.s 40 
IL_0011: newobj  System.Text.StringBuilder..ctor 
IL_0016: stloc.2  
IL_0017: ldloc.0  
IL_0018: ldloc.2  
IL_0019: callvirt Microsoft.FSharp.Core.FSharpFunc<System.Text.StringBuilder,System.Text.StringBuilder>.Invoke 
IL_001E: ldstr  "END" 
IL_0023: callvirt System.Text.StringBuilder.Append 
IL_0028: ret 

[email protected]: 
IL_0000: nop   
IL_0001: ldarg.0  
IL_0002: ldfld  [email protected] 
IL_0007: ldarg.1  
IL_0008: call  [email protected] 
IL_000D: ldc.i4.s 0A 
IL_000F: callvirt System.Text.StringBuilder.Append 
IL_0014: ret   

[email protected]: 
IL_0000: ldarg.0  
IL_0001: call  Microsoft.FSharp.Core.FSharpFunc<System.Text.StringBuilder,System.Text.StringBuilder>..ctor 
IL_0006: ldarg.0  
IL_0007: ldarg.1  
IL_0008: stfld  [email protected] 
IL_000D: ret   

[email protected]: 
IL_0000: nop   
IL_0001: ldarg.0  
IL_0002: ldfld  [email protected] 
IL_0007: ldarg.1  
IL_0008: callvirt Microsoft.FSharp.Core.FSharpFunc<System.Text.StringBuilder,System.Text.StringBuilder>.Invoke 
IL_000D: ldstr  "test" 
IL_0012: callvirt System.Text.StringBuilder.Append 
IL_0017: ret   

[email protected]: 
IL_0000: ldarg.0  
IL_0001: call  Microsoft.FSharp.Core.FSharpFunc<System.Text.StringBuilder,System.Text.StringBuilder>..ctor 
IL_0006: ldarg.0  
IL_0007: ldarg.1  
IL_0008: stfld  [email protected] 
IL_000D: ret 
+0

これは興味深いです。私は作曲を使う前に "たくさんのラムダ"の世代を見てきました。しかし、この差は私が予想していたよりもかなり大きいです。しかし、ILが必ずしもパフォーマンスの低下を意味するわけではありません。私はまだこれがなぜそれを行うのか不思議です。私の推測では、JITコンパイラは合成シナリオでクロージャを効果的に最適化できないということです。 – Abel

+0

JIT:erには時間、記憶、知識が限られています。私の経験では、ホリスティックな最適化に頼ることはできません。それは未使用の変数、インラインメソッド(仮想でなければ)とアンロールループを排除することができますが、それは私にとってそうです。 F#コンパイラには、より多くの情報が用意されており、原理的にはより効率的なILを書くことができるはずです。 – FuleSnabel

+0

"たくさんのラムダ"は最適化なしでのみ発生します。 Ringilの答えに対する私のコメントを参照してください。 –

関連する問題