2016-09-06 1 views
7

틀림없이 나는 사과와 사과를 사과와 정확하게 비교하고 있는지 확실하지 않습니다. 그러나 나는 차이의 bigness에 대해 특히 놀랍습니다. 차이가있을 경우, 차이가있을 것으로 예상됩니다.F #에서 기능 구성이 배관보다 60 % 더 느린 이유는 무엇입니까?

can often be expressed as function composition and vice versa을 배관, 나는 컴파일러가 너무 알고 가정 것이다, 그래서 약간의 실험을 시도 :

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

테스트를이 FSI에 (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 # 최신 버전을 사용하고 있습니다. 내가 생성 된 IL에서 무엇을 말할 수있는 F 번호 내부, 대한 깊은 지식없이

답변

9

:

> #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 (The 첫 번째 테이블은 단지 하나의 작성/파이프 런 인 2 표, 나는 비슷한 있음) 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

은 (최적화가 꺼져있는 경우 그 중 많은) pipeappend* 모든 호출은 인라인 될 것입니다 반면 compose이, 람다를 얻을 것입니다.

pipe 기능에 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 
compose 기능에 IL 생성

: 나는 FSI와 예를 시도하고 뚜렷한 차이가 발견되지

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

이것은 흥미로운 사실입니다.작곡을 사용하기 전에 나는 "많은 람다"세대를 보았습니다. 하지만이 차이는 예상했던 것보다 훨씬 큽니다. 더 많은 일리노이가 반드시 적은 성능을 의미하는 것은 아닙니다. 이것이 왜 그런 식으로 행동하는지 궁금합니다. 내 생각 엔 JIT 컴파일러는 작성 시나리오에서 클로저를 효과적으로 최적화 할 수 없다는 것입니다. – Abel

+0

JIT : er에는 제한된 시간, 메모리 및 지식이 있습니다. 내 경험에 비추어 볼 때 전체 론적 최적화에 의존 할 수는 없습니다. 사용하지 않는 변수, 인라인 메소드 (가상 변수가 아닌 경우) 및 루프 해제를 제거 할 수 있지만 그 점이 저에게 다가갑니다. F # 컴파일러는 훨씬 더 많은 정보를 사용할 수 있으며 원칙적으로보다 효율적인 IL을 작성할 수 있어야합니다. – FuleSnabel

+0

"많은 람다"는 최적화 없이만 발생합니다. Ringil의 대답에 대한 내 의견을 참조하십시오. –

관련 문제