2014-07-05 13 views
12

Jedną z rzeczy, które uwielbiam w F #, jest prawdziwe słowo kluczowe inline. Jednak, mimo że pozwala pisać funkcje pierwszego rzędu, które wykonują to samo, co wklejone bloki kodu, rzeczy nie są tak różowe dla funkcji wyższego rzędu. RozważmyDlaczego nie można w pełni kompilować argumentów funkcji F # funkcji wyższego rzędu?

let inline add i = i+1 
let inline check i = if (add i) = 0 then printfn ""  
let inline iter runs f = for i = 0 to runs-1 do f i 
let runs = 100000000 
time(fun()->iter runs check) 1 
time(fun()->for i = 0 to runs-1 do check i) 1 

Wyniki są 244 ms dla iter i 61 ms do kontroli manualnych. Zagłębmy się w ILSpy. Odpowiednią funkcją dla wywołania bezpośredniego jest:

internal static void [email protected](Microsoft.FSharp.Core.Unit unitVar0) 
{ 
    for (int i = 0; i < 100000000; i++) 
    { 
     if (i + 1 == 0) 
     { 
      Microsoft.FSharp.Core.PrintfFormat<Microsoft.FSharp.Core.Unit, System.IO.TextWriter, Microsoft.FSharp.Core.Unit, Microsoft.FSharp.Core.Unit> format = new Microsoft.FSharp.Core.PrintfFormat<Microsoft.FSharp.Core.Unit, System.IO.TextWriter, Microsoft.FSharp.Core.Unit, Microsoft.FSharp.Core.Unit, Microsoft.FSharp.Core.Unit>(""); 
      Microsoft.FSharp.Core.PrintfModule.PrintFormatLineToTextWriter<Microsoft.FSharp.Core.Unit>(System.Console.Out, format); 
     } 
    } 
} 

Z numerem add. Odpowiednia funkcja iter jest

internal static void [email protected](Microsoft.FSharp.Core.Unit unitVar0) 
{ 
    for (int i = 0; i < 100000000; i++) 
    { 
     [email protected](i); 
    } 
} 
internal static void [email protected](int i) 
{ 
    if (i + 1 == 0) 
    { 
     Microsoft.FSharp.Core.PrintfFormat<Microsoft.FSharp.Core.Unit, System.IO.TextWriter, Microsoft.FSharp.Core.Unit, Microsoft.FSharp.Core.Unit> format = new Microsoft.FSharp.Core.PrintfFormat<Microsoft.FSharp.Core.Unit, System.IO.TextWriter, Microsoft.FSharp.Core.Unit, Microsoft.FSharp.Core.Unit, Microsoft.FSharp.Core.Unit>(""); 
     Microsoft.FSharp.Core.PrintfModule.PrintFormatLineToTextWriter<Microsoft.FSharp.Core.Unit>(System.Console.Out, format); 
     return; 
    } 
} 

I widzimy kara wydajność pochodzi z jednego dodatkowy poziom pośredni. Jak pokazuje test wydajności, ten kierunek nie jest również usuwany przez kompilator JIT. Czy istnieje powód, dla którego funkcje wyższego rzędu nie mogą być w pełni rozwinięte? To jest problem przy pisaniu jądra obliczeniowego.

Mój czas syntezatora (choć nie bardzo istotne tutaj) jest

let inline time func n = 
    func() |> ignore 
    GC.Collect() 
    GC.WaitForPendingFinalizers() 
    let stopwatch = Stopwatch.StartNew() 
    for i = 0 to n-1 do func() |> ignore 
    stopwatch.Stop() 
    printfn "Took %A ms" stopwatch.Elapsed.TotalMilliseconds 
+0

Proszę potwierdzić, że uruchomiłeś ten program w trybie Release bez dołączonego debuggera. Poza tym benchmark wydaje się ważny. Można go poprawić, zwiększając pracę o współczynnik 10, aby zmniejszyć wpływ jednorazowych kosztów. – usr

+0

@usr Tak, uruchomiłem go bez debuggera i skompilowałem w trybie Release. Nie ma wątpliwości, że różnica w wydajności jest realna, ponieważ można ją wywnioskować z kodu IL (z wyłączeniem optymalizacji JIT). – Arbil

+0

@Abbil Połączyłem to pytanie w jednym z wątków UserVoice w F # Language Design UserVoice o inlineing analysis: https://fslang.uservoice.com/forums/245727-f-language/suggestions/6137978-better-inlining-analysis i-heurystyczne algorytmy –

Odpowiedz

6

Wystarczy być jasne, F # kompilator inline każdą definicję, które zostały oznaczone jako inline. Chodzi o to, że obecne zachowanie inline nie jest zbyt użyteczne, gdy używa się funkcji inline jako argumentu wyższego rzędu. check może być wstawiony tylko po podaniu argumentu, a więc iter runs check jest traktowany jako iter runs (fun i -> check i). Następnie check dostaje inlined, powodując równowartości

iter runs (fun i -> if (add i) = 0 then printfn "") 

(jak widać w IL, nie ma wezwanie do check w wygenerowanym IL, ale nie jest to wezwanie do syntetycznego [email protected] ciała do tego lambda , co jest równoważne). iter zostaje również wprowadzony.

Powiedziawszy to, zgadzam się, że obecne zachowanie nie jest tak użyteczne, jak mogłoby być - kompilator może również wstawiać treść lambda do strony wywołania, co byłoby bezpieczne i poprawiłoby wydajność.

+4

Ściśle mówiąc, prawdą jest, że problem nie jest nieskróconą funkcją "sprawdzenia", ale nieopartą funkcją wywodzącą się z "sprawdzenia". To jednak, o ile mogę powiedzieć, nie jest charakterystyczne dla mojego przykładu, ale dzieje się tak w przypadku wszystkich wywołań funkcji wyższego rzędu.Dlatego też pod względem wydajności jest tożsame z argumentem funkcji, który nie jest inline. Dlaczego my (ci, którzy interesują się wysokowydajnym rozwojem/nauką/grą z F #) nie chcemy, aby niektóre z tych problemów zostały naprawione? Nr 1 to struct krotki, to nie jest. 2. Obecnie na fslang jest bardzo niewiele wniosków dotyczących przegłosowanych wyników. – Arbil

+1

Byłbym szczęśliwy, mogąc pójść z tobą na przyspieszoną akcję. Języki wysokiego poziomu są niesamowite, ale postawa "wydajności nie ma znaczenia" prowadzi do cyklu anty cnotowego do tego stopnia, że ​​ma to znaczenie. – jackmott

Powiązane problemy