Chciałbym zapytać ludzi doświadczonych w pracy z systemami w skali Visual Studio: co sprawia, że są powolni? Czy to warstwa po warstwie abstrakcji jest niezbędna do utrzymania bazy kodu w zakresie możliwości ludzkiego rozumienia? Czy to właśnie ilość kodu musi zostać uruchomiona? Czy jest to współczesna tendencja do oszczędzania czasu przez programistę przy (ogromnie ogromnym) wydatku w dziale cykli zegara / wykorzystania pamięci?
Myślę, że zgadłeś kilka z nich, ale chciałbym zaoferować coś, co uważam za największy czynnik, pracując na dość dużej bazie kodu (nie jestem pewien, czy jest tak duży jak Visual Studio - był w milionach linii kodu kategoria i około tysiąca wtyczek) przez około 10 lat i obserwowane są zjawiska.
Jest również nieco mniej kontrowersyjny, ponieważ nie wchodzi w interfejsy API, funkcje językowe ani nic podobnego. Dotyczą one „kosztów”, które mogą zapoczątkować debatę, a nie „wydatków”, a ja chcę się skoncentrować na „wydatkach”.
Luźna koordynacja i dziedzictwo
Zauważyłem, że luźna koordynacja i długa spuścizna prowadzą do gromadzenia dużej ilości odpadów.
Na przykład w tej bazie kodu znalazłem około stu struktur przyspieszenia, z których wiele jest zbędnych.
Chcielibyśmy drzewa drzewa KD do przyspieszania jednego silnika fizyki, drugiego dla nowego silnika fizyki, który często działał równolegle ze starym, mielibyśmy dziesiątki implementacji oktetów dla różnych algorytmów siatki, innego drzewa KD do renderowania , zbieranie itp. itd. Są to duże, nieporęczne struktury drzewa używane do przyspieszania wyszukiwania. Każdy z nich może zabrać setki megabajtów do gigabajtów pamięci dla danych wejściowych bardzo średniej wielkości. Nie zawsze były tworzone i używane przez cały czas, ale w dowolnym momencie 4 lub 5 z nich mogło być jednocześnie w pamięci.
Teraz wszystkie z nich przechowywały dokładnie te same dane, aby przyspieszyć ich wyszukiwanie. Możesz to sobie wyobrazić jako analogiczną starą bazę danych, która przechowuje wszystkie swoje pola jednocześnie w 20 różnych nadmiarowych mapach / słownikach / drzewach B +, uporządkowanych identycznie według tych samych kluczy i przeszukując je wszystkie przez cały czas. Teraz zajmujemy 20 razy więcej pamięci i przetwarzania.
Ponadto, ze względu na nadmiarowość, nie ma czasu na optymalizację żadnego z nich z ceną serwisową, która jest z tym związana, a nawet gdybyśmy to zrobili, miałby tylko 5% efektu, który idealnie by zrobił.
Co powoduje to zjawisko? Luźna koordynacja była przyczyną numer jeden, którą widziałem. Wielu członków zespołu często pracuje w swoich izolowanych ekosystemach, rozwijając lub wykorzystując struktury danych stron trzecich, ale nie używając tych samych struktur, z których korzystali inni członkowie zespołu, nawet jeśli byli rażącymi duplikatami tych samych obaw.
Co powoduje utrzymywanie się tego zjawiska? Dziedzictwo i kompatybilność były numerem jeden, ponieważ widziałem. Ponieważ ponieśliśmy już koszty wdrożenia tych struktur danych, a od tych rozwiązań zależały duże ilości kodu, często zbyt ryzykowne było ich skonsolidowanie do mniejszej liczby struktur danych. Mimo że wiele z tych struktur danych było wysoce redundantnych koncepcyjnie, nie zawsze były one identyczne w swoich projektach interfejsów. Zastąpienie ich byłoby dużą, ryzykowną zmianą, w przeciwieństwie do po prostu pozwalania im zużywać pamięć i czas przetwarzania.
Wydajność pamięci
Zazwyczaj użycie pamięci i szybkość są zwykle powiązane przynajmniej na poziomie hurtowym. Często można wykryć wolne oprogramowanie po tym, jak zapycha pamięć. Nie zawsze jest prawdą, że więcej pamięci prowadzi do spowolnienia, ponieważ liczy się pamięć „gorąca” (jaka pamięć jest dostępna przez cały czas - jeśli program używa dużej ilości pamięci, ale tylko 1 megabajt jest używany przez cały czas) czas, to nie jest tak wielka sprawa pod względem szybkości).
Możesz więc często wykryć potencjalne wieprze na podstawie zużycia pamięci. Jeśli aplikacja zabiera dziesiątki do setek megabajtów pamięci podczas uruchamiania, prawdopodobnie nie będzie bardzo wydajna. Dziesiątki megabajtów mogą wydawać się małe, gdy mamy gigabajty pamięci DRAM, ale największe i najwolniejsze pamięci podręczne procesora nadal znajdują się w wąskim zakresie megabajtów, a najszybsze wciąż w zakresie kilobajtów. W rezultacie program, który używa 20 megabajtów tylko do uruchomienia i nic nie robi, w rzeczywistości nadal zużywa dość „dużo” pamięci z punktu widzenia sprzętowej pamięci podręcznej procesora, zwłaszcza jeśli wszystkie 20 megabajtów tej pamięci będzie dostępne wielokrotnie i często w trakcie działania programu.
Rozwiązanie
Dla mnie rozwiązaniem jest poszukiwanie bardziej skoordynowanych, mniejszych zespołów do tworzenia produktów, takich, które mogą w pewien sposób śledzić swoje „wydatki” i unikać „kupowania” tych samych produktów w kółko.
Koszt
Zajmę się bardziej kontrowersyjną stroną „kosztu” tylko trochę z małym zjawiskiem „wydawania”, które zaobserwowałem. Jeśli w końcu pojawia się język z nieuniknioną ceną dla obiektu (na przykład taką, która zapewnia odbicie w czasie wykonywania i nie może wymusić ciągłego przydziału dla serii obiektów), ta cena jest droga tylko w kontekście bardzo ziarnistego elementu, takiego jak pojedynczy Pixel
lub Boolean
.
Jednak widzę dużo kodu źródłowego dla programów, które radzą sobie z dużym obciążeniem (np. Radzenie sobie z setkami tysięcy do milionów Pixel
lub Boolean
instancji) płacącymi ten koszt na tak szczegółowym poziomie.
Programowanie obiektowe może to zaostrzyć. Jednak nie jest to koszt „obiektów” per se, ani nawet OOP z winy, to po prostu, że takie koszty są płacone na tak szczegółowym poziomie drobnego elementu, który zostanie utworzony przez miliony.
To inne zjawiska związane z „kosztem” i „wydawaniem”, które obserwuję. Koszt to pensy, ale pensy sumują się, jeśli kupujemy milion puszek z napojem osobno, zamiast negocjować z producentem na zakup hurtowy.
Rozwiązaniem dla mnie jest zakup „masowy”. Przedmioty są doskonale w porządku, nawet w językach, w których każdy ma pewną cenę grosza, pod warunkiem, że koszt ten nie jest płacony milion razy osobno za analogiczny ekwiwalent napoju gazowanego.
Przedwczesna optymalizacja
Nigdy nie podobało mi się użyte tutaj sformułowanie Knuth, ponieważ „przedwczesna optymalizacja” rzadko przyspiesza rzeczywiste programy produkcyjne. Niektórzy interpretują to jako „wczesną optymalizację”, gdy Knuth miał na myśli bardziej „optymalizację bez odpowiedniej wiedzy / doświadczenia, aby poznać jego prawdziwy wpływ na oprogramowanie”. Co więcej, praktyczny efekt prawdziwej przedwczesnej optymalizacji często powoduje spowolnienie oprogramowania , ponieważ pogorszenie możliwości konserwacji oznacza, że nie ma czasu na optymalizację najważniejszych ścieżek, które są naprawdę ważne .
To ostatnie zjawisko, które zaobserwowałem, gdy programiści starali się zaoszczędzić grosze na zakupie jednej puszki napoju gazowanego, nigdy więcej nie do kupienia lub, co gorsza, domu, marnowali cały czas na szczypanie groszy niezrozumienie ich kompilatora lub architektury sprzętu), gdy miliardy dolarów wydawano gdzie indziej.
Czas jest bardzo skończony, więc próba optymalizacji absolutów bez posiadania odpowiednich informacji kontekstowych często pozbawia nas możliwości optymalizacji miejsc, które naprawdę mają znaczenie, a zatem pod względem praktycznym powiedziałbym, że „przedwczesna optymalizacja powoduje, że oprogramowanie jest znacznie wolniejsze. „
Problem polega na tym, że istnieją typy programistów, którzy wezmą to, co napisałem powyżej o obiektach i spróbują ustanowić standard kodowania, który zakazuje programowania obiektowego lub czegoś takiego szalonego. Skuteczna optymalizacja to skuteczne ustalanie priorytetów i absolutnie bezwartościowe, jeśli toniemy w morzu problemów związanych z konserwacją.