OK, definiujesz problem tak, aby wydawało się, że nie ma zbyt wiele miejsca na ulepszenia. Z mojego doświadczenia jest to dość rzadkie. Próbowałem to wyjaśnić w artykule dr Dobbsa w listopadzie 1993 r., Rozpoczynając od konwencjonalnie dobrze zaprojektowanego nietrywialnego programu bez oczywistych strat i przeprowadzając go przez serię optymalizacji, aż jego czas naścienny został skrócony z 48 sekund do 1,1 sekundy, a rozmiar kodu źródłowego został zmniejszony czterokrotnie. Moje narzędzie diagnostyczne było takie . Sekwencja zmian była następująca:
Pierwszym znalezionym problemem było użycie klastrów list (obecnie nazywanych „iteratorami” i „klasami kontenerów”), które stanowią ponad połowę czasu. Zostały one zastąpione dość prostym kodem, co skróciło czas do 20 sekund.
Teraz największym pochłaniaczem czasu jest tworzenie większej liczby list. Procentowo nie był wcześniej tak duży, ale teraz jest tak, ponieważ większy problem został usunięty. Znajduję sposób, aby go przyspieszyć, a czas spada do 17 sekund.
Teraz trudniej jest znaleźć oczywistych winowajców, ale jest kilka mniejszych, z którymi mogę coś zrobić, a czas spada do 13 sekund.
Teraz chyba uderzyłem w ścianę. Próbki mówią mi dokładnie, co robi, ale nie mogę znaleźć niczego, co mógłbym poprawić. Następnie zastanawiam się nad podstawową konstrukcją programu, nad jego strukturą opartą na transakcjach i pytam, czy całe przeszukiwanie listy, które wykonuje, jest faktycznie wymagane przez wymagania problemu.
Następnie natknąłem się na przeprojektowanie, w którym kod programu jest generowany (za pomocą makr preprocesora) z mniejszego zestawu źródeł, i w którym program nie stale odkrywa rzeczy, o których programista wie, że są dość przewidywalne. Innymi słowy, nie „interpretuj” sekwencji rzeczy do zrobienia, „kompiluj” ją.
- To przeprojektowanie zostało wykonane, zmniejszając kod źródłowy czterokrotnie, a czas został skrócony do 10 sekund.
Teraz, ponieważ robi się tak szybko, trudno jest próbkować, więc daję mu 10 razy więcej do zrobienia, ale poniższe czasy są oparte na oryginalnym obciążeniu.
Więcej diagnozy ujawnia, że spędza czas na zarządzaniu kolejkami. Podszewka skraca czas do 7 sekund.
Teraz dużym poświęceniem czasu jest druk diagnostyczny, który robiłem. Spłucz to - 4 sekundy.
Teraz największymi odbiorcami czasu są połączenia z malloc i bezpłatne . Przetwarzaj obiekty - 2,6 sekundy.
Kontynuując próbkowanie, wciąż znajduję operacje, które nie są absolutnie konieczne - 1,1 sekundy.
Współczynnik całkowitego przyspieszenia: 43,6
Teraz nie ma dwóch podobnych programów, ale w oprogramowaniu innym niż zabawkowe zawsze widziałem taki postęp. Najpierw dostajesz rzeczy łatwe, a potem trudniejsze, aż dojdziesz do punktu malejących zysków. Wtedy zdobyta wiedza może doprowadzić do przeprojektowania, rozpoczynając nową rundę przyspieszeń, aż znów osiągniesz malejące zyski. Teraz jest to punkt, w którym może ona sensu zastanawiać się, czy ++i
czy i++
czy for(;;)
czy while(1)
są szybsze: rodzaje pytań widzę tak często na przepełnienie stosu.
PS Można się zastanawiać, dlaczego nie użyłem profilera. Odpowiedź jest taka, że prawie każdy z tych „problemów” był miejscem wywoływania funkcji, które precyzyjnie układa próbki. Profile nawet dzisiaj prawie nie wpadają na pomysł, że instrukcje i instrukcje połączeń są ważniejsze do zlokalizowania i łatwiejsze do naprawy niż całe funkcje.
Właściwie stworzyłem profiler, aby to zrobić, ale dla prawdziwej podejrzanej intymności z tym, co robi kod, nie ma substytutu dla wprawienia w to palców. Nie jest problemem, że liczba próbek jest niewielka, ponieważ żaden ze znalezionych problemów nie jest tak mały, że można je łatwo przeoczyć.
DODANO: jerryjvl poprosił o kilka przykładów. Oto pierwszy problem. Składa się z niewielkiej liczby oddzielnych wierszy kodu, co łącznie zajmuje ponad połowę czasu:
/* IF ALL TASKS DONE, SEND ITC_ACKOP, AND DELETE OP */
if (ptop->current_task >= ILST_LENGTH(ptop->tasklist){
. . .
/* FOR EACH OPERATION REQUEST */
for ( ptop = ILST_FIRST(oplist); ptop != NULL; ptop = ILST_NEXT(oplist, ptop)){
. . .
/* GET CURRENT TASK */
ptask = ILST_NTH(ptop->tasklist, ptop->current_task)
Używali klastra list ILST (podobnego do klasy listy). Są one wdrażane w zwykły sposób, a „ukrywanie informacji” oznacza, że użytkownicy klasy nie powinni dbać o to, jak zostały zaimplementowane. Kiedy napisano te wiersze (z około 800 wierszy kodu) nie przyszło mi do głowy, że mogą to być „wąskie gardła” (nienawidzę tego słowa). Są po prostu zalecanym sposobem robienia rzeczy. Łatwo powiedzieć z perspektywy czasu, że należy tego unikać, ale z mojego doświadczenia wynika, że wszystkie problemy z wydajnością są takie. Ogólnie rzecz biorąc, dobrze jest unikać tworzenia problemów z wydajnością. Jeszcze lepiej jest znaleźć i naprawić te, które zostały utworzone, nawet jeśli „należało ich unikać” (z perspektywy czasu).
Oto drugi problem w dwóch osobnych wierszach:
/* ADD TASK TO TASK LIST */
ILST_APPEND(ptop->tasklist, ptask)
. . .
/* ADD TRANSACTION TO TRANSACTION QUEUE */
ILST_APPEND(trnque, ptrn)
Są to budowanie list poprzez dołączanie przedmiotów na ich końcach. (Rozwiązaniem było zebranie elementów w tablice i zbudowanie list naraz.) Ciekawe jest to, że te wyciągi kosztują tylko (tj. Były na stosie wywołań) 3/48 pierwotnego czasu, więc nie były w fakt jest dużym problemem na początku . Jednak po usunięciu pierwszego problemu kosztują 3/20 czasu, a więc były teraz „większą rybą”. Ogólnie tak to wygląda.
Mogę dodać, że ten projekt był destylowany z prawdziwego projektu, któremu pomogłem. W tym projekcie problemy z wydajnością były znacznie bardziej dramatyczne (podobnie jak przyspieszenie), takie jak wywołanie procedury dostępu do bazy danych w wewnętrznej pętli, aby sprawdzić, czy zadanie zostało zakończone.
DODANO ODNIESIENIA: Kod źródłowy, zarówno oryginalny, jak i przeprojektowany, można znaleźć na stronie www.ddj.com , dla 1993 r., W pliku 9311.zip, plikach slug.asc i slug.zip.
EDYCJA 2011/11/26: Istnieje teraz projekt SourceForge zawierający kod źródłowy w Visual C ++ i szczegółowy opis jego dostrojenia. Przechodzi tylko przez pierwszą połowę opisanego powyżej scenariusza i nie zachowuje dokładnie tej samej sekwencji, ale wciąż otrzymuje przyspieszenie o 2-3 rzędy wielkości.