2012-02-07 12 views
6

Szukałem sposobu na prawidłowe kodowanie algorytmiczne za pomocą .NET z wszystkimi zaletami nowoczesnych języków (np. Lubię silne sprawdzanie typu, przeciążanie operatorów, lambdy, ogólne algorytmy). Zwykle piszę moje algorytmy (głównie prozessing obrazu) w C++. Ponieważ F # jako język wydaje się dość interesujący, trochę grałem, ale wydaje mi się to bardzo powolne. Jako najbardziej prostego testu Właśnie zrobiłem jakieś manipulacje Array -> Zwiększenie jasności obrazu:Optymalizacja kodu F # czy jest naprawdę tak wolno?

let r1 = rgbPixels |> Array.map (fun x -> x + byte(10)) 

Wydaje się być czynnikiem co najmniej 8 razy wolniej niż w porównaniu C++ realizacji - co gorsza dla bardziej złożonych algorytmów na przykład Splot 2D. Czy jest jakiś szybszy sposób, czy też brakuje mi jakichkolwiek konkretnych ustawień kompilatora (tak, budowanie wersji z optymalizacją na ...)? Jestem skłonny zapłacić za ładną i wysoką abstrację, ale takie obciążenie nie jest miłe (muszę zrównoleglić na 8 rdzeniach, aby to zrekompensować :)) - przynajmniej niszczy motywację do dalszej nauki ... Moje inne opcją byłoby opuścić moje cięższe algos w C++ i interfejs ze sprzężonym C++, ale to nie jest miłe, ponieważ obsługa zarządzanego opakowania byłaby sporym obciążeniem.

+1

Możliwe, że zostałby wstawiony, ale można zacząć od zastąpienia wywołania "bajtem" przez '10uy'. – Daniel

+0

Czy możesz umieścić swoją implementację C++ na [ideone] (http://ideone.com/), więc mamy coś do pracy? – Daniel

+0

Czy [te wyniki] (http://ideone.com/Z4Gvj) są porównywalne z twoimi? – Daniel

Odpowiedz

6

Jeśli martwisz się wydajnością, jedną z ważnych rzeczy do zapamiętania jest to, że F # domyślnie niczego nie mutuje. Wymaga to kopiowania w wielu naiwnych implementacjach algorytmów, takich jak ten, który opisałeś.

EDYCJA: Nie mam pojęcia dlaczego, ale proste testy poniższego kodu zapewniają gorsze wyniki dla Array.map. Pamiętaj, aby profilować dowolny algorytm próbowany podczas wykonywania tego rodzaju optymalizacji. Jednak uzyskałem bardzo podobne wyniki między for i map.

Array.map tworzy nową tablicę dla wyniku operacji, zamiast tego chcesz Array.iteri.

rgbPixels |> Array.iteri (fun i x -> rgbPixels.[i] <- x + 10uy) 

Należy pamiętać, że może to być opakowane w swoim własnym modułem poniżej

module ArrayM = 
    let map f a = a |> Array.iteri (fun i x -> a.[i] <- f x) 

Niestety jest to zło konieczne, ponieważ jednym z głównych najemców programowania funkcyjnego jest, aby trzymać się niezmiennych obiektów tyle jak zezwoli na to twój algorytm, a następnie po zakończeniu przejdź do mutacji, w której wydajność jest krytyczna. Jeśli wiesz, że Twoja skuteczność jest krytyczna od początku, musisz zacząć od tego rodzaju pomocy.

Należy również pamiętać, że istnieje prawdopodobnie biblioteka, która zapewnia tę funkcję, po prostu nie wiem o tym ręcznie.

+1

Albo, jeszcze szybciej: ' dla i = 0 do rgbPixels.Length-1 do rgbPixels. [i] <- rgbPixels. [i] + 10uy'. – Daniel

+0

@Daniel: Starałem się trzymać możliwie jak najbliżej oryginalnego kodu. Zakładam, że i tak funkcja jest opisana przez JIT. – Guvante

+0

@ Guwane: Testowałem z FSI: pętla zajmuje mniej niż połowę czasu ... prawdopodobnie ze względu na brak sprawdzania granic, jak wspomniał kvb. – Daniel

5

Myślę, że to na pewno powiedzieć, że idiomatyczne F # będą często nie pasują do wydajności optymalnej C++ do manipulacji tablicy, z kilku powodów:

  1. Array dostępy są porównywane granice tablicy w .NET, aby zapewnić bezpieczeństwo pamięci. Kompilator JIT CLR jest w stanie wymusić kontrole granic pod kątem jakiegoś stereotypowego kodu, ale zazwyczaj wymaga to użycia pętli for z wyraźnymi ograniczeniami, a nie bardziej idiomatycznymi konstrukcjami F #.
  2. Zwykle niewielka ilość kosztów związanych z używaniem abstrakcji, takich jak lambda (na przykład fun i -> ...). W ciasnych pętlach ten mały narzut może okazać się znaczący w porównaniu z pracą wykonywaną w ciele pętli.
  3. O ile mi wiadomo, kompilator JIT języka CLR nie korzysta z instrukcji SSE w takim stopniu, w jakim kompilatory C++ są w stanie.

Z drugiej strony księgi,

  1. Nigdy nie będzie mieć przepełnienie bufora w kodzie F #.
  2. Twój kod będzie łatwiej uzasadnić. Jako konsekwencja, dla danego poziomu całkowitej złożoności kodu, często można zaimplementować bardziej złożony algorytm w języku F # niż w C++.
  3. W razie potrzeby można napisać kod niesocjatywny, który będzie działał z szybkością zbliżoną do C++, a także istnieją możliwości współpracy z niebezpiecznym kodem C++, jeśli okaże się, że trzeba napisać C++, aby spełnić wymagania dotyczące wydajności.
+0

1. Z tą różnicą, że masz tę pętlę w Array.map 2. Można tego uniknąć poprzez wstawianie 3. Z drugiej strony równoległość jest znacznie łatwiejsza w językach funkcjonalnych (jak w przypadku dodawania pojedynczego dodatkowego wywołania funkcji), a operacje wektorowe mogą być technicznie dodawane w podobny sposób. –

+0

Całkowicie zgadzam się ze wszystkimi punktami powyżej i absolutnie jestem gotów poświęcić trochę wydajności (co było moją motywacją do rozpoczęcia gry z F #), ale zmierzone wyniki powyżej pokazują współczynnik 5 dla prostej operacji (to dlatego zgadłem, że Zrobiłem coś źle). Zbliżając się do paralelizmu: jeśli zacznę z 5 czynnikiem, będę trzymał 5 rdzeni zajęty i prawdopodobnie nadal byłby tam, gdzie byłem wcześniej z jednym rdzeniem w C++ ... – TheBigW

+0

@Maciej - Dla kodu, który musi pisać do tablicy żadna z funkcji wyższego rzędu w module 'Array' nie jest odpowiednikiem pętli for (to jest w' arr |> Array.iteri (fun iv -> arr. [i] <- v + 1) 'check do czytania zostaną usunięte, ale nadal będzie czek na pisanie przy każdej iteracji). – kvb

Powiązane problemy