Zachowaj lokalne optymalizacje, spraw, aby były oczywiste, dobrze dokumentuj i ułatwiaj porównywanie zoptymalizowanych wersji ze sobą oraz z niezoptymalizowaną wersją, zarówno pod względem kodu źródłowego, jak i wydajności w czasie wykonywania.
Pełna odpowiedź
Jeśli takie optymalizacje są naprawdę ważne dla Twojego produktu, musisz wiedzieć nie tylko, dlaczego optymalizacje były wcześniej przydatne, ale także dostarczyć wystarczających informacji, aby pomóc programistom dowiedzieć się, czy będą one przydatne w przyszłości.
Idealnie, musisz zapisać testy wydajności w procesie kompilacji, aby dowiedzieć się, kiedy nowe technologie unieważniają stare optymalizacje.
Zapamiętaj:
Pierwsza zasada optymalizacji programu: nie rób tego.
Druga zasada optymalizacji programu (tylko dla ekspertów!): Nie rób tego jeszcze. ”
- Michael A. Jackson
Aby wiedzieć, czy nadszedł czas, należy przeprowadzić testy porównawcze i testy.
Jak wspomniałeś, największym problemem związanym z wysoce zoptymalizowanym kodem jest to, że trudno go utrzymać, więc w miarę możliwości musisz trzymać zoptymalizowane części oddzielnie od niezoptymalizowanych części. Niezależnie od tego, czy robisz to przez łączenie czasu kompilacji, wywołania funkcji wirtualnych środowiska wykonawczego, czy coś pośredniego, nie powinno to mieć znaczenia. Co powinno mieć znaczenie, że po uruchomieniu testów chcesz mieć możliwość testowania wszystkich wersji, którymi jesteś obecnie zainteresowany.
Byłbym skłonny zbudować system w taki sposób, że podstawowa niezoptymalizowana wersja kodu produkcyjnego mogłaby zawsze zostać wykorzystana do zrozumienia zamiaru kodu, a następnie zbudować różne zoptymalizowane moduły wraz z tym zawierającym zoptymalizowaną wersję lub wersje, wyraźnie dokumentując gdziekolwiek wersja zoptymalizowana różni się od linii podstawowej. Po uruchomieniu testów (jednostkowych i integracyjnych) uruchamia się go w niezoptymalizowanej wersji i na wszystkich aktualnie zoptymalizowanych modułach.
Przykład
Załóżmy na przykład, że masz funkcję szybkiej transformacji Fouriera . Być może masz podstawową, algorytmiczną implementację fft.c
i testy w fft_tests.c
.
Potem pojawia się Pentium i decydujesz się na implementację wersji z punktem stałym fft_mmx.c
przy użyciu instrukcji MMX . Później pentium 3 przyjdzie i zdecydować, aby dodać wersję, która używa Streaming SIMD Extensions w fft_sse.c
.
Teraz chcesz dodać CUDA , więc dodajesz fft_cuda.c
, ale przekonaj się, że dzięki testowemu zestawowi danych, którego używasz od lat, wersja CUDA jest wolniejsza niż wersja SSE! Przeprowadzasz analizę i dodajesz zestaw danych, który jest 100 razy większy, i otrzymujesz przyspieszenie, którego oczekujesz, ale teraz wiesz, że czas przygotowania do użycia wersji CUDA jest znaczący i że w przypadku małych zestawów danych powinieneś użyć algorytm bez tego kosztu konfiguracji.
W każdym z tych przypadków implementujesz ten sam algorytm, wszystkie powinny zachowywać się w ten sam sposób, ale będą działać z różnymi wydajnościami i prędkościami na różnych architekturach (jeśli w ogóle będą działać). Jednak z punktu widzenia kodu możesz porównać dowolną parę plików źródłowych, aby dowiedzieć się, dlaczego ten sam interfejs jest implementowany na różne sposoby i zwykle najłatwiej będzie odnieść się do oryginalnej niezoptymalizowanej wersji.
To samo dotyczy implementacji OOP, w której klasa bazowa, która implementuje niezoptymalizowany algorytm, oraz klasy pochodne implementują różne optymalizacje.
Ważne jest, aby zachować te same rzeczy, które są takie same , aby różnice były oczywiste .