Podejście do tego, aby baza kodu stała się jednolicie powolna

11

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.

Benjamin Bannier
źródło
10
10Mloc to tak naprawdę duży projekt
23овић
1
Liczony jest przez 10 milionów loc (prefiks SI) sloc. Nazwałem to „umiarkowanym rozmiarem”, ponieważ nie mam pojęcia, co uważa się za „duże”.
Benjamin Bannier
5
całkiem pewne, że 10 milionów jest co najmniej duże wszędzie i prawdopodobnie ogromne większość miejsc.
Ryathal
1
Wspaniale, dzięki @honk Dla 10M LOC brzmi to tak, jakbyś optymalizował na bardzo niskim poziomie, prawie na poziomie sprzętowym? Tradycyjne OOP („tablica struktur” AOS) jest strasznie nieefektywne w pamięciach podręcznych. Czy próbowałeś zmienić układ klas na SOA (strukturę tablic), aby punkty danych, nad którymi pracuje kod, były spójne w pamięci? Przy tak wielu maszynach masz problemy z komunikacją lub synchronizacją pochłaniającą czas? Ostatnie pytanie, czy masz do czynienia z dużą ilością danych przesyłanych strumieniowo, czy jest to głównie problem skomplikowanych operacji na zestawach danych?
Patrick Hughes,
1
Kiedy masz tak dużo kodu, szanse zaczynają się od doskonałej do bet-your-life, że istnieją duże potencjalne przyspieszenia nielokalnego rodzaju, o którym wspomniałem. Nie ma znaczenia, czy istnieją tysiące wątków / procesów. Niektóre przypadkowe pauzy albo palcami dla ciebie, albo udowodnią, że się mylę.
Mike Dunlavey,

Odpowiedzi:

9

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 .

dasblinkenlight
źródło
Dziękujemy za podzielenie się wrażeniami. Są to przydatne optymalizacje, o których należy pamiętać. Ponadto, ponieważ przenoszą problematyczne części w inne miejsce, w przyszłości będą o wiele lepiej kontrolować. W pewnym sensie wprowadzili w życie to, co powinno się wydarzyć, teraz tylko z powodu twardych danych.
Benjamin Bannier
3

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.

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.

S.Lott
źródło
1
Dziękuję za odpowiedź. Podczas gdy nadal musimy optymalizować (który dla mnie mieści się poniżej 1. i 2.), bardzo podoba mi się zorganizowane myślenie 3. Dzięki zdefiniowaniu struktury danych, algorytmu i dostępu późno i wyraźnie, można być w stanie poradzić sobie z wieloma problemy, przed którymi stoimy. Dzięki za wprowadzenie go w spójny język.
Benjamin Bannier
W rzeczywistości nie musisz optymalizować. Po uzyskaniu prawidłowej struktury danych optymalizacja okaże się stratą wysiłku. Profilowanie pokaże, gdzie masz niewłaściwą strukturę danych i zły algorytm. Wygłupianie się z różnicą wydajności pomiędzy ++i +=1będzie nieistotne i prawie niemożliwe do zmierzenia. To jest coś, co musisz przetrwać .
S.Lott,
1
Nie wszystkie złe algorytmy można znaleźć na podstawie samego rozumowania. Raz na jakiś czas trzeba usiąść i profilować. Jest to jedyny sposób, aby dowiedzieć się, czy początkowe przypuszczenie było prawidłowe. To jedyny sposób oszacowania rzeczywistych kosztów (BigO + const).
Benjamin Bannier
Profilowanie ujawni złe algorytmy. Całkowicie poprawne. To wciąż nie jest „optymalizacja”. To wciąż korekta fundamentalnej wady projektowej spowodowanej przeze mnie zmianą projektu. Optymalizacja (poprawianie, dostrajanie itp.) Rzadko będzie widoczna dla profilowania.
S.Lott
3

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:

  1. Upewnij się, że zasięg testu jednostkowego jest dobry (już to zrobiłeś, prawda?)
  2. Upewnij się, że środowisko uruchomieniowe testu raportuje środowisko wykonawcze dla każdego testu . Jest to ważne, ponieważ chcesz dowiedzieć się, gdzie dzieje się wolne działanie.
  3. Jeśli test działa powoli, użyj go jako sposobu na zanurkowanie i optymalizację celu w tym obszarze . Optymalizację przed zidentyfikowaniem problemu z wydajnością można uznać za przedwczesną, dlatego wielką zaletą tego podejścia jest to, że najpierw uzyskuje się konkretne dowody niskiej wydajności. W razie potrzeby podziel test na mniejsze testy, które porównują różne aspekty, abyś mógł określić, gdzie jest główny problem.
  4. Jeśli test przebiega bardzo szybko, zwykle jest to dobre, chociaż można rozważyć uruchomienie testu w pętli z różnymi parametrami. To czyni go lepszym testem wydajności, a także zwiększa zasięg testu przestrzeni parametrów.
  5. Napisz kilka dodatkowych testów ukierunkowanych konkretnie na wydajność, np. Czas transakcji od końca do końca lub czas na ukończenie 1000 aplikacji reguł. Jeśli masz określone niefunkcjonalne wymagania dotyczące wydajności (np. Czas reakcji <300 ms), spowoduj niepowodzenie testu, jeśli trwa on zbyt długo.

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.

mikera
źródło
podoba mi się technika - sprytna - jakkolwiek, jest to mikrooptymalizacja i poprawi się do lokalnego minimum - nie rozwiąże problemów architektonicznych, które pozwolą ci osiągnąć globalne minimum
jasonk
@jasonk - masz absolutną rację. Chociaż dodam, że czasami może dostarczyć dowodów, które należy wykazać, dlaczego konkretna zmiana architektoniczna jest uzasadniona .....
mikera
1

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.)

Mike Dunlavey
źródło
Dziękuję za odpowiedź. Nie wierzymy w statystyczne pobieranie próbek i stosowanie oprzyrządowania z valgrind. To daje nam dobre oszacowanie zarówno kosztu własnego, jak i „integracyjnego” większości rzeczy, które robimy.
Benjamin Bannier
@honk: Racja. Niestety, oprzyrządowanie nadal ma pomysł, że problemy z wydajnością są zlokalizowane, więc można je znaleźć, mierząc ułamek czasu spędzonego na procedurach itp. Z pewnością możesz uruchomić valgrind itp., Ale jeśli chcesz mieć prawdziwy wgląd w wydajność, sprawdź pokaz slajdów .
Mike Dunlavey