2012-06-09 19 views
13

Reading Począwszy F # - Robert Pickering skupiłem się na następujący akapit:CLR vs wyjątkiem OCaml napowietrznych

Programatory pochodzące ze środowisk OCaml należy być ostrożnym przy wykorzystujące wyjątki w F #. Z powodu architektury CLR, wyrzucenie wyjątku jest dość drogie - całkiem droższe niż w OCaml. Jeśli rzucisz wiele wyjątków, dokładnie zapisz swój kod pod numerem , aby zdecydować, czy są one opłacalne. Jeśli koszty są zbyt wysokie, popraw poprawny kod.

Dlaczego, z powodu CLR, wyrzucenie wyjątku jest droższe, jeśli F# niż w OCaml? A jaki jest najlepszy sposób na poprawne poprawienie kodu w tym przypadku?

+1

Była [dyskusja na temat lekkości wyjątków OCaml] (http://stackoverflow.com/questions/8564025/ocaml-internals-exceptions) –

+0

@JeffreyScofield dziękuję, jednak bardziej interesują mnie aspekty dotyczące CLR . – gliderkite

Odpowiedz

7

Wcześniej wyjaśnił, dlaczego wyjątki .NET zachowują się inaczej niż wyjątki OCaml. Ogólnie, wyjątki .NET są odpowiednie tylko dla wyjątkowych sytuacji i są przeznaczone do tego celu. OCaml ma bardziej lekki model i dlatego są wykorzystywane do realizacji niektórych schematów sterowania.

Aby dać konkretny przykład, w OCaml można użyć wyjątku do wykonania przerwania pętli. Załóżmy na przykład, że masz funkcję test, która sprawdza, czy liczba jest numerem, który chcemy. Poniższe iteracje nad numerami od 1 do 100 i zwrotów pierwszy pasujących numer:

// Simple exception used to return the result 
exception Returned of int 

try 
    // Iterate over numbers and throw if we find matching number 
    for n in 0 .. 100 do 
    printfn "Testing: %d" n 
    if test n then raise (Returned n) 
    -1     // Return -1 if not found 
with Returned r -> r // Return the result here 

zaimplementować to bez wyjątków, masz dwie opcje. Można napisać rekurencyjną funkcję, która ma takie samo zachowanie - jeśli zadzwonisz find 0 (i to jest kompilowany do zasadniczo tego samego kodu IL, jak przy użyciu return n wewnątrz for pętli w C#):

let rec find n = 
    printfn "Testing: %d" n 
    if n > 100 then -1 // Return -1 if not found 
    elif test n then n // Return the first found result 
    else find (n + 1) // Continue iterating 

Kodowanie za pomocą funkcji rekurencyjnych możliwe być nieco dłuższy, ale możesz również użyć standardowych funkcji udostępnianych przez bibliotekę F #. Jest to często najlepszy sposób na przepisanie kodu, który używałby wyjątków OCaml do sterowania przepływem. W tym przypadku, można napisać:

// Find the first value matching the 'test' predicate 
let res = seq { 0 .. 100 } |> Seq.tryFind test 
// This returns an option type which is 'None' if the value 
// was not found and 'Some(x)' if the value was found. 
// You can use pattern matching to return '-1' in the default case: 
match res with 
| None -> -1 
| Some n -> n 

Jeśli nie jesteś zaznajomiony z typów opcji, a następnie zapoznać się z jakimś materiałem wprowadzającym. F# wikibook has a good tutorial i MSDN documentation has useful examples też.

Użycie odpowiedniej funkcji z modułu Seq często powoduje, że kod jest znacznie krótszy, więc jest to preferowane. Może być nieco mniej wydajne niż użycie rekurencji bezpośrednio, ale w większości przypadków nie musisz się tym martwić.

EDIT: Byłem ciekawy rzeczywistej wydajności. Wersja korzystająca z Seq.tryFind jest bardziej wydajna, jeśli dane wejściowe są leniwie generowane w sekwencji seq { 1 .. 100 } zamiast listy [ 1 .. 100 ] (ze względu na koszty przydzielania listy). Z tych zmian, a test funkcji zwracającej 25 elementu, czas potrzebny do uruchomienia kodu 100000 razy na moim komputerze jest:

exceptions 2.400sec 
recursion 0.013sec 
Seq.tryFind 0.240sec 

Jest to niezwykle trywialny przykład, więc myślę, że rozwiązanie z wykorzystaniem Seq nie będzie generalnie uruchomić 10 razy wolniej niż równoważny kod napisany przy użyciu rekursji. Zwolnienie prawdopodobnie wynika z alokacji dodatkowych struktur danych (obiekt reprezentujący sekwencję, zamknięcia, ...), a także z powodu dodatkowego pośrednictwa (kod wymaga licznych wywołań metod wirtualnych, zamiast zwykłych operacji numerycznych i skoków). Jednak wyjątki są jeszcze bardziej kosztowne i nie powodują, że kod jest krótszy lub bardziej czytelny w żaden sposób ...

+0

+1 Muszę zdecydowanie przeczytać twoją książkę. Tylko jedno, dlaczego użycie funkcji Seq może być mniej wydajne niż użycie rekursji bezpośrednio? – gliderkite

+0

@gliderkite Uruchomiłem prymitywny test i dodałem informacje o wydajności. –

12

Wyjątki w CLR są bardzo bogate i zapewniają wiele szczegółów. Rico Mariani opublikował (stary, ale wciąż aktualny) wpis na the cost of exceptions w CLR, który zawiera szczegóły tego.

Jako takie, wyjątek w CLR ma wyższy koszt względny niż w niektórych innych środowiskach, w tym OCaml.

Jaki jest najlepszy sposób poprawnej korekty kodu w tym przypadku?

Jeśli można oczekiwać wyjątkiem zostać podniesiona w normalnych, nie-wyjątkowych okolicznościach, można przemyśleć swój algorytm i API w sposób, który pozwala uniknąć wyjątek całkowicie. Spróbuj podać alternatywny interfejs API, na przykład, aby przetestować tę okoliczność przed wywołaniem wyjątku.

+1

Typ "opcja" jest tutaj bardzo istotny. – ildjarn

+0

@ildjarn Tak, dokładnie. Często można użyć 'Brak' zamiast używać wyjątków. –

+0

Przykro mi, nie wiem "Brak" i "opcja". Czy mógłbyś rozwinąć te istotne aspekty? – gliderkite

8

Dlaczego, z powodu CLR, wyrzucenie wyjątku jest droższe, jeśli F # niż w OCaml?

OCaml został silnie zoptymalizowany pod kątem wykorzystania wyjątków jako przepływu sterowania. Natomiast wyjątki w .NET nie zostały w ogóle zoptymalizowane.

Należy zauważyć, że różnica w wydajności jest ogromna. Wyjątki są około 600 × szybsze w OCaml niż w F #. Nawet C++ jest około 6 × wolniej niż OCaml pod tym względem, zgodnie z moimi testami porównawczymi.

Mimo że wyjątki .NET rzekomo zapewniają więcej (OCaml zapewnia ślady stosów, czego więcej chcesz?) Nie mogę wymyślić żadnego powodu, dla którego powinny być one tak powolne, jak są.

Jaki jest najlepszy sposób poprawnej korekty kodu w tym przypadku?

W języku F # należy wpisać funkcje "całkowite". Oznacza to, że twoja funkcja powinna zwracać wartość typu złącza, wskazującą rodzaj wyniku, np. normalny lub wyjątkowy.

W szczególności wzywa do find należy zastąpić wywołań tryFind które zwracają wartość typu option który jest albo Some wartość lub None jeśli kluczowym elementem nie był obecny w kolekcji.

+0

'Nie mogę wymyślić żadnego powodu, dla którego powinieneś być tak powolny jak oni." Dokładnie o to pytam. Link [Koszt wyjątków] (http://blogs.msdn.com/b/ricom/archive/2003/12/19/44697.aspx) opublikowany przez Reeda Copseya wyjaśnia w bardzo prosty sposób, dlaczego to spowolnienie. – gliderkite

+0

@gliderkite Ten artykuł stwierdza, że ​​.NET działa wolno i opisuje obejścia (omija tę część .NET), ale nie sądzę, że opisuje to dlaczego. Myślę, że odpowiedź brzmi po prostu, że Microsoft nigdy nie zadał sobie trudu, by ją zoptymalizować. –

+0

'Nie sądzę, że opisuje to dlaczego' niestety tak jest, ale nie sądzę, że Microsoft nigdy nie zadał sobie trudu zoptymalizowania go, jaki zysk mógłby uzyskać w ten sposób? – gliderkite