Pracujemy na średniej wielkości bazie kodu C ++ (10Mloc), która dzięki naszym wysiłkom optymalizacyjnym staje się jednolicie powolna .
Ta podstawa kodu to zestaw bibliotek, które łączymy, aby je uruchomić. Kiedy opracowano ogólne ramy komunikacji tych bibliotek, nacisk położono na wydajność, a później, gdy dodano więcej części, ogólne ramy nie uległy znacznej zmianie. Optymalizacja została przeprowadzona w razie potrzeby i w miarę ewolucji naszego sprzętu. Dzięki temu droga wczesna decyzja stała się widoczna dopiero znacznie później. Jesteśmy teraz w punkcie, w którym dalsze optymalizacje są znacznie droższe, ponieważ wymagałyby przepisania dużych części bazy kodu. Zbliżamy się do niepożądanego minimum lokalnego, ponieważ wiemy, że w zasadzie kod powinien być w stanie działać znacznie szybciej.
Czy istnieją jakieś udane metodologie, które pomagają zdecydować, które kroki przejmą ewolucję bazy kodu w kierunku globalnie optymalnego rozwiązania, które nie są łatwo mylone przez łatwe możliwości optymalizacji?
EDYTOWAĆ
Aby odpowiedzieć na pytanie, w jaki sposób obecnie profilujemy:
Tak naprawdę mamy tylko 2 różne scenariusze, w których można użyć tego kodu, oba są krępująco równoległe. Profilowanie odbywa się zarówno przy uśrednieniu czasu zegara ściennego na dużej próbce danych wejściowych, jak i bardziej szczegółowych przebiegach (koszty instrukcji, nieprzewidziane oddziały i problemy z buforowaniem). Działa to dobrze, ponieważ działamy wyłącznie na naszych wyjątkowo homogenicznych maszynach (klaster kilku tysięcy identycznych maszyn). Ponieważ zazwyczaj wszystkie nasze maszyny są zajęte przez większość czasu, działają szybciej, więc możemy spojrzeć na dodatkowe nowe rzeczy. Problem polega oczywiście na tym, że gdy pojawią się nowe warianty danych wejściowych, mogą zostać ukarani za spóźnienie, ponieważ usunęliśmy najbardziej oczywiste mikroefektywności w przypadku innych przypadków użycia, co może zawęzić liczbę „optymalnie działających” scenariuszy.
źródło
sloc
. Nazwałem to „umiarkowanym rozmiarem”, ponieważ nie mam pojęcia, co uważa się za „duże”.Odpowiedzi:
Nie wiem o ogólnym podejściu do tego problemu, ale w przeszłości sprawdzały się dla mnie dwa dość podobne podejścia: z powodu braku lepszych warunków nazwałem je wiązaniem i optymalizacją poziomą .
Metoda grupowania jest próbą zastąpienia dużej liczby krótkich, szybkich operacji pojedynczą, wolniej działającą, wysoce wyspecjalizowaną operacją, która ostatecznie daje ten sam rezultat.
Przykład: Po wyprofilowaniu jednej szczególnie powolnej operacji naszego wizualnego edytora reguł nie znaleźliśmy „nisko wiszącego owocu”: nie było ani jednej operacji, która zajmowałaby więcej niż 2% czasu wykonania, ale operacja jako całość wydawała się powolna. Odkryliśmy jednak, że redaktor wysyła dużą liczbę małych żądań do serwera. Mimo że redaktor szybko przetwarzał poszczególne odpowiedzi, liczba interakcji żądanie / odpowiedź miała efekt multiplikatywny, więc całkowity czas operacji trwał kilka sekund. Po dokładnym skatalogowaniu interakcji edytora podczas tej długotrwałej operacji dodaliśmy nowe polecenie do interfejsu serwera. Dodatkowe polecenie było bardziej wyspecjalizowane, ponieważ akceptowało dane wymagane do wykonania podzbioru krótkich operacji, zbadał zależności danych, aby ustalić ostateczny zestaw danych do zwrócenia, i dostarczył odpowiedź zawierającą informacje potrzebne do wykonania wszystkich pojedynczych drobnych operacji podczas jednej podróży na serwer. Nie skróciło to czasu przetwarzania w naszym kodzie, ale zmniejszyło bardzo znaczne opóźnienia z powodu usunięcia wielu drogich podróży między klientem a serwerem.
Optymalizacja pozioma to technika pokrewna, gdy eliminujesz „powolność”, która jest cienko rozłożona na wiele komponentów systemu za pomocą określonej funkcji środowiska wykonawczego.
Przykład: Po profilowaniu długotrwałej operacji odkryliśmy, że wykonujemy wiele wywołań przez granicę domeny aplikacji (jest to wysoce specyficzne dla platformy .NET). Nie mogliśmy wyeliminować żadnego z połączeń i nie mogliśmy zebrać ich razem: przychodziły one w różnym czasie z bardzo różnych sekcji naszego systemu, a rzeczy, o które prosili, zależały od wyników zwróconych z wcześniejszych żądań. Każde połączenie wymagało serializacji i deserializacji stosunkowo niewielkiej ilości danych. Ponownie, poszczególne połączenia były krótkotrwałe, ale bardzo duże. W rezultacie zaprojektowaliśmy schemat, który prawie całkowicie uniknął serializacji, zastępując go przekazaniem wskaźnika przez granicę domeny aplikacji. To była wielka wygrana, ponieważ wiele zgłoszeń z zupełnie niezwiązanych klas natychmiast stało się znacznie szybszych w wyniku zastosowania jednegorozwiązanie poziome .
źródło
Po rozpoczęciu przepisywania musisz zrobić kilka rzeczy inaczej.
Pierwszy. I najważniejsze. Przestań „optymalizować”. „Optymalizacja” nie ma większego znaczenia. Jak widzieliście, tylko hurtowe przepisywanie ma znaczenie.
W związku z tym.
Druga. Poznaj konsekwencje każdej struktury danych i wyboru algorytmu.
Trzeci. Uczyń faktyczny wybór struktury danych i algorytmu kwestią „późnego wiązania”. Projektuj interfejsy, które mogą mieć jedną z kilku implementacji używanych za interfejsem.
To, co teraz robisz (przepisywanie), powinno być o wiele, o wiele mniej bolesne, jeśli masz zdefiniowany zestaw interfejsów, który pozwala na hurtową zmianę struktury danych lub algorytmu.
źródło
++
i+=1
będzie nieistotne i prawie niemożliwe do zmierzenia. To jest coś, co musisz przetrwać .Miłą praktyczną sztuczką jest użycie zestawu testów jednostkowych jako zestawu testów wydajności .
Następujące podejście działało dobrze w moich bazach kodu:
Jeśli nadal to wszystko robisz, z czasem średnia wydajność bazy kodu powinna organicznie wzrosnąć.
Możliwe byłoby również śledzenie historycznych czasów testowych i rysowanie wykresów wydajności oraz regresji punktowych w czasie przy średniej wydajności. Nigdy nie przejmowałem się tym głównie dlatego, że jest to trochę trudne, aby upewnić się, że porównujesz się z podobnymi, gdy zmieniasz i dodajesz nowe testy, ale może to być interesujące ćwiczenie, jeśli wydajność jest dla Ciebie wystarczająco ważna.
źródło
Odpowiedź @dasblinkenlight wskazuje na bardzo częsty problem, szczególnie w przypadku dużych baz kodu (z mojego doświadczenia). Mogą występować poważne problemy z wydajnością, ale nie są one zlokalizowane . Jeśli prowadzisz profiler, nie ma procedur, które same w sobie zwracają uwagę. (Zakładając, że spojrzysz na procent czasu włącznie, który obejmuje callees. Nawet nie przejmuj się „czasem własnym”).
W rzeczywistości w tym przypadku rzeczywisty problem nie został znaleziony przez profilowanie, ale przez szczęśliwy wgląd.
Mam studium przypadku, dla którego jest krótki pokaz slajdów w formacie PDF , szczegółowo ilustrujący ten problem i sposób jego obsługi. Podstawową kwestią jest to, że ponieważ kod jest znacznie wolniejszy, niż mógłby być, oznacza to (z definicji) nadmiar czasu spędzany na robieniu czegoś, co można usunąć.
Jeśli przyjrzysz się losowym próbkom stanu programu, zobaczysz, że wykonuje ono czynności wymienne ze względu na procent czasu. Jest całkiem możliwe, że aktywność wymienna nie ogranicza się do jednej funkcji, a nawet wielu funkcji. Nie lokalizuje w ten sposób.
To nie jest „gorący punkt”.
Jest to opis, który robisz, co widzisz, co zdarza się być prawdziwe przez duży procent czasu. Ułatwia to odkrycie, ale to, czy jest łatwe do naprawienia, zależy od tego, ile to wymaga przepisania.
(Często pojawia się krytyka tego podejścia, że liczba próbek jest zbyt mała, aby mogła być ważna statystycznie. Odpowiada to na slajdzie 13 pliku PDF. W skrócie - tak, w „pomiarze” potencjalnych oszczędności istnieje duża niepewność, ale 1) spodziewana wartość tych potencjalnych oszczędności pozostaje zasadniczo niezmieniona, oraz 2) gdy potencjalne oszczędności $ x $ przekłada się na współczynnik przyspieszenia o 1 $ / (1-x) $, jest ona silnie przekrzywiona w kierunku wysokich (korzystnych) czynników.)
źródło