2016-09-06 2 views
7

Zugegeben, ich bin mir nicht sicher, ob ich hier Äpfel mit Äpfeln oder Äpfeln richtig mit Birnen vergleiche. Aber ich bin besonders überrascht über die Größe des Unterschieds, wo ein geringerer Unterschied, wenn überhaupt, erwartet werden würde.Warum ist die Funktionszusammensetzung in F # um 60% so viel langsamer als beim Piping?

can often be expressed as function composition and vice versa Rohrleitungen, und ich würde davon ausgehen, der Compiler, dass auch weiß, so habe ich versucht, ein kleines Experiment:

// 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" 

Testing dies in FSI (64 Bit aktiviert ist, --optimize Fahne) ergibt:

> 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 =() 

Ein kleiner Unterschied wäre verständlich, aber das ist ein Faktor 1,6 (60%) Leistungseinbußen.

Ich würde eigentlich erwarten, dass der Großteil der Arbeit in der StringBuilder passieren wird, aber anscheinend hat der Overhead der Zusammensetzung ziemlich viel Einfluss.

Ich weiß, dass in den meisten praktischen Situationen dieser Unterschied vernachlässigbar sein wird, aber wenn Sie große formatierte Textdateien (wie Protokolldateien) wie in diesem Fall schreiben, hat es eine Auswirkung.

Ich verwende die neueste Version von F #.

Antwort

9

ich dein Beispiel mit FSI ausprobiert und fand keinen nennenswerten Unterschied:

> #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 =() 

Messen in BenchmarkDotNet (Die erste Tabelle ist nur eine einzige compose/Rohrleitung, die zweite Tabelle es 500000 mal) tut, fand ich etwas ähnliches:

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 | 

Es kann sein, dass Unterschiede Sie messen sind GC bezogen. Versuchen Sie, eine GC-Sammlung vor/nach Ihrer Zeit zu erzwingen.

Das heißt, an der source code für den Rohr Betreiber suchen:

let inline (|>) x f = f x 

und den Vergleich gegen die Zusammensetzung Betreiber:

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

scheint es, dass die Zusammensetzung Operator Lambda wird die Schaffung klar zu machen, Funktionen, die zu mehr Zuordnungen führen sollten. Dies zeigt sich auch in den BenchmarkDotNet-Läufen. Dies könnte auch der Grund für den Leistungsunterschied sein, den Sie sehen.

+0

Danke, sehr interessante Vergleiche. Vielleicht verwenden Sie den Server-GC und ich habe den normalen Single-Thread-GC? Ich weiß nicht, wie ich das für FSI konfigurieren soll. Ich sollte die kompilierten Versionen vergleichen. Ich weiß zu schätzen, dass zumindest auf Ihrem System die Unterschiede vernachlässigbar sind, so wie es sein sollte. – Abel

+0

Ich verwende keine speziellen Flags für FSI außer '--optimize', das ist die von Ihnen erwähnte. Ich führe auch fsianycpu.exe, falls das wichtig ist. – Ringil

+1

@Ringil Ich bin nicht einverstanden mit Lambdas. Ja, sie werden in nicht optimiertem Code erstellt. Aber mit Optimierungen sehe ich nur zwei Lambdas, nicht neun. Alles andere wird inline geschaltet. Ich denke, das Endergebnis sollte sein, dass der Compiler härtere Zeit hat, das Inlining im Falle der Zusammensetzung herauszufinden, als im Fall von Rohrleitungen. –

6

Ohne tiefes Wissen über F # Interna, was ich aus dem erzeugten IL sagen kann ist, dass compose lambdas ergeben (und viele von ihnen, wenn Optimierungen ausgeschaltet sind), während in pipe alle Anrufe append* wird inlined werden.

generiert IL für pipe Funktion:

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 

generiert IL für compose Funktion:

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

Dies ist faszinierend.Ich habe die Erzeugung von "vielen Lambdas" schon vorher gesehen, als ich Komposition verwendete. Aber dieser Unterschied ist viel größer als ich erwartet hatte. Mehr IL bedeutet nicht unbedingt weniger Leistung. Ich bin immer noch neugierig, warum das so funktioniert. Meine Vermutung ist, dass der JIT-Compiler die Closures im Compose-Szenario nicht effektiv optimieren kann. – Abel

+0

Das JIT: er hat nur begrenzte Zeit, Speicher und Wissen. Meiner Erfahrung nach können wir uns nicht auf ganzheitliche Optimierungen verlassen. Es kann unbenutzte Variablen, Inline-Methoden (außer virtuell) und Schleifen entfernen, aber das scheint mir. Der F # -Compiler verfügt über wesentlich mehr Informationen und sollte prinzipiell in der Lage sein, effizientere IL zu schreiben. – FuleSnabel

+0

Die "vielen lambdas" passieren nur ohne Optimierungen. Sehen Sie meinen Kommentar zu Ringils Antwort. –

Verwandte Themen