Czy kod o niskim opóźnieniu czasami musi być „brzydki”?


21

(Jest to skierowane głównie do tych, którzy mają konkretną wiedzę na temat systemów o niskim opóźnieniu, aby uniknąć odpowiedzi osób niepotwierdzonych).

Czy uważasz, że istnieje kompromis między pisaniem „ładnego” kodu obiektowego a pisaniem bardzo szybkiego kodu o niskim opóźnieniu? Na przykład unikanie funkcji wirtualnych w C ++ / narzutu polimorfizmu itp. - ponowne pisanie kodu, który wygląda paskudnie, ale jest bardzo szybki itp.?

Ma rację - kogo to obchodzi, jeśli wygląda brzydko (pod warunkiem, że można go utrzymać) - jeśli potrzebujesz prędkości, potrzebujesz prędkości?

Chciałbym usłyszeć od ludzi, którzy pracowali w takich obszarach.


1
@ user997112: Bliski powód jest oczywisty. Mówi: „Oczekujemy, że odpowiedzi będą poparte faktami, referencjami lub konkretną wiedzą specjalistyczną, ale to pytanie prawdopodobnie zachęci do debaty, argumentów, ankiet lub rozszerzonej dyskusji. Niekoniecznie oznacza to, że są poprawne, ale to był koniec powód wybrany przez wszystkich trzech bliskich wyborców
Robert Harvey,

Anegdotycznie powiedziałbym, że powodem, dla którego to pytanie przyciąga ścisłe głosy, jest to, że może być postrzegane jako cienko zawoalowany rant (chociaż nie sądzę, że tak jest).
Robert Harvey,

8
Wyciągnę szyję: trzeci głos zamknąłem jako „mało konstruktywny”, ponieważ myślę, że pytający właściwie odpowiada na własne pytanie. „Piękny” kod, który nie działa wystarczająco szybko, aby wykonać zadanie, nie spełnił wymagań dotyczących opóźnienia. „Brzydki” kod, który działa wystarczająco szybko, może być łatwiejszy w utrzymaniu dzięki dobrej dokumentacji. Jak mierzysz piękno lub brzydotę to temat na inne pytanie.
Blrfl,

1
Kod źródłowy LMAX's Disruptor nie jest zbyt brzydki. Istnieją pewne elementy „do diabła z modelem bezpieczeństwa Java” (klasa niebezpieczna) i niektóre modyfikacje specyficzne dla sprzętu (zmienne wypełnione wierszem pamięci podręcznej), ale jest to bardzo czytelny IMO.
James

5
@ Carson63000, user1598390 i ktokolwiek inny jest zainteresowany: jeśli pytanie zostanie zamknięte, nie krępuj się zapytać o zamknięcie na naszej stronie Meta , omawianie zamknięcia w komentarzach nie ma sensu, zwłaszcza zamknięcie, które nie nastąpiło . Pamiętaj też, że każde zamknięte pytanie można otworzyć ponownie, to nie koniec świata. Z wyjątkiem oczywiście, jeśli Majowie mieli rację, w takim przypadku miło było was poznać!
yannis,

Odpowiedzi:


31

Czy uważasz, że istnieje kompromis między pisaniem „ładnego” kodu obiektowego a pisaniem kodu o bardzo niskim opóźnieniu?

Tak.

Dlatego istnieje fraza „przedwczesna optymalizacja”. Istnieje po to, aby zmusić programistów do pomiaru ich wydajności i zoptymalizować tylko ten kod, który zmieni różnicę w wydajności, jednocześnie rozsądnie projektując architekturę aplikacji od samego początku, aby nie spadła pod dużym obciążeniem.

W ten sposób, w maksymalnym możliwym zakresie, możesz zachować swój ładny, dobrze skonstruowany, obiektowy kod i optymalizować tylko przy pomocy brzydkiego kodu te małe, ważne części.


15
„Spraw, by działało, a następnie spraw, aby było szybkie”. Ta odpowiedź obejmuje w zasadzie wszystko, co chciałem powiedzieć, czytając pytanie.
Carson63000,

13
Dodam „Mierz, nie zgaduj”
Martijn Verburg,

1
Uważam, że warto poświęcić trochę czasu na podstawowe unikanie pracy na bieżąco, o ile nie odbywa się to kosztem czytelności. Utrzymywanie zwięzłości, czytelności i robienie tylko oczywistych rzeczy, które muszą zrobić, prowadzi do wielu pośrednich długoterminowych wygranych perfów, takich jak inni programiści, którzy wiedzą, co do cholery zrobić z twoim kodem, aby nie powielali wysiłku ani nie przyjmowali złych założeń o tym, jak to działa.
Erik Reppen

1
W przypadku „przedwczesnej optymalizacji” - nadal obowiązuje, nawet jeśli zoptymalizowany kod będzie równie „miły” jak niezoptymalizowany kod. Chodzi o to, aby nie tracić czasu na dążenie do prędkości / czegokolwiek, czego nie trzeba osiągnąć. W rzeczywistości optymalizacja nie zawsze polega na szybkości i zapewne istnieje coś takiego jak niepotrzebna optymalizacja dla „piękna”. Twój kod nie musi być wielkim dziełem sztuki, aby był czytelny i łatwy w utrzymaniu.
Steve314,

I sekundę @ Steve314. Jestem liderem w zakresie wydajności produktu i często znajduję bardzo skomplikowany kod, którego pochodzenie mogę prześledzić do pewnego rodzaju optymalizacji wydajności. Uproszczenie tego kodu często ujawnia znaczną poprawę wydajności. Jeden taki przykład zmienił się w 5-krotną poprawę wydajności, kiedy ją uprościłem (redukcja netto tysięcy linii kodu). Najwyraźniej nikt nie poświęcił czasu na pomiar i po prostu dokonał przedwczesnej optymalizacji tego, co według nich prawdopodobnie będzie wolnym kodem .
Brandon,

5

Tak, podanym przeze mnie przykładem nie jest C ++ vs. Java, ale Zgromadzenie vs. COBOL, bo to wiem.

Oba języki są bardzo szybkie, ale nawet COBOL po skompilowaniu zawiera o wiele więcej instrukcji, które są umieszczone w zestawie instrukcji, które niekoniecznie muszą tam być, w przeciwieństwie do samodzielnego pisania tych instrukcji w asemblerze.

Ten sam pomysł można zastosować bezpośrednio do pytania o pisanie „brzydko wyglądającego kodu” w porównaniu do używania dziedziczenia / polimorfizmu w C ++. Uważam, że konieczne jest pisanie brzydko wyglądającego kodu, jeśli użytkownik końcowy potrzebuje przedziałów czasu transakcji poniżej sekundy, naszym zadaniem jako programistów jest zapewnienie im tego, bez względu na to, jak to się stanie.

To powiedziawszy, swobodne stosowanie komentarzy znacznie zwiększa funkcjonalność programatora i łatwość konserwacji, bez względu na to, jak brzydki jest kod.


3

Tak, istnieje kompromis. Rozumiem przez to, że kod, który jest szybszy i bardziej brzydszy, nie jest koniecznie lepszy - ilościowe korzyści płynące z „szybkiego kodu” należy wyważyć w stosunku do złożoności utrzymania zmian kodu potrzebnych do osiągnięcia tej prędkości.

Kompromis wynika z kosztów biznesowych. Bardziej złożony kod wymaga bardziej wykwalifikowanych programistów (i programistów z bardziej ukierunkowanym zestawem umiejętności, takich jak ci z architekturą procesorów i wiedzą projektową), zajmuje więcej czasu na odczytanie i zrozumienie kodu oraz naprawę błędów. Koszt biznesowy opracowania i utrzymania takiego kodu może wynosić od 10x do 100x w stosunku do normalnie napisanego kodu.

Ten koszt konserwacji jest uzasadniony w niektórych branżach , w których klienci są skłonni zapłacić bardzo wysoką premię za bardzo szybkie oprogramowanie.

Niektóre optymalizacje prędkości zapewniają lepszy zwrot z inwestycji (ROI) niż inne. Mianowicie, niektóre techniki optymalizacji mogą być stosowane z mniejszym wpływem na łatwość konserwacji kodu (zachowując strukturę wyższego poziomu i czytelność niższego poziomu) w porównaniu do normalnie napisanego kodu.

W związku z tym właściciel firmy powinien:

  • Spójrz na koszty i korzyści,
  • Wykonuj pomiary i obliczenia
    • Poproś programistę o zmierzenie prędkości programu
    • Poproś programistę o oszacowanie czasu programowania potrzebnego do optymalizacji
    • Dokonaj własnych oszacowań dotyczących wzrostu przychodów z szybszego oprogramowania
    • Niech architekci oprogramowania lub kierownicy ds. Kontroli jakości ocenią jakościowo wady wynikające ze zmniejszonej intuicyjności i czytelności kodu źródłowego
  • I postaw na priorytet nisko wiszące owoce optymalizacji oprogramowania.

Te kompromisy są wysoce specyficzne dla okoliczności.

Nie można optymalnie ustalić bez udziału menedżerów i właścicieli produktów.

Są one bardzo specyficzne dla platform. Na przykład procesory stacjonarne i mobilne mają różne względy. Aplikacje serwerowe i klienckie mają również różne uwagi.


Tak, ogólnie jest prawdą, że szybszy kod wygląda inaczej niż normalnie napisany kod. Odczytywanie innego kodu zajmie więcej czasu. To, czy oznacza to brzydotę, leży w oczach patrzącego.

Techniki, z którymi mam do czynienia, to: (bez próby zdobycia jakiegokolwiek poziomu wiedzy) optymalizacja krótkich wektorów (SIMD), drobnoziarnista równoległość zadań, wstępna alokacja pamięci i ponowne użycie obiektów.

SIMD zazwyczaj ma poważny wpływ na czytelność na niskim poziomie, nawet jeśli zwykle nie wymaga zmian strukturalnych na wyższym poziomie (pod warunkiem, że interfejs API został zaprojektowany z myślą o zapobieganiu wąskim gardłom).

Niektóre algorytmy można łatwo przekształcić w SIMD (kłopotliwa wektoryzacja). Niektóre algorytmy wymagają więcej zmian układu obliczeń, aby móc korzystać z SIMD. W skrajnych przypadkach, takich jak równoległość falowego SIMD, aby skorzystać, należy napisać całkowicie nowe algorytmy (i możliwe do opatentowania implementacje).

Drobnoziarnista równoległość zadań wymaga przestawienia algorytmów na grafy przepływu danych i wielokrotnego stosowania funkcjonalnej (obliczeniowej) dekompozycji do algorytmu, dopóki nie można uzyskać żadnych dodatkowych korzyści z marży. Zdekomponowane etapy są zazwyczaj połączone w stylu kontynuacji, koncepcja zapożyczona z programowania funkcjonalnego.

Przez rozkład funkcjonalny (obliczeniowy) algorytmy, które normalnie mogły zostać zapisane w liniowej i koncepcyjnie przejrzystej sekwencji (wiersze kodu, które można wykonać w tej samej kolejności, w jakiej zostały zapisane) muszą zostać rozbite na fragmenty i podzielone na wiele funkcji lub zajęcia. (Zobacz obiektywizację algorytmu poniżej.) Ta zmiana znacznie utrudni innym programistom, którzy nie są zaznajomieni z procesem projektowania dekompozycji, który spowodował powstanie takiego kodu.

Aby taki kod był możliwy do utrzymania, autorzy takiego kodu muszą napisać skomplikowaną dokumentację algorytmu - daleko poza typem komentowania kodu lub diagramów UML wykonanych dla normalnie napisanego kodu. Jest to podobne do sposobu, w jaki naukowcy piszą swoje prace naukowe.


Nie, szybki kod nie musi być sprzeczny z obiektowością.

Innymi słowy, możliwe jest wdrożenie bardzo szybkiego oprogramowania, które nadal jest obiektowe. Jednak w kierunku dolnej części tej implementacji (na poziomie orzechów i śrub, gdzie występuje większość obliczeń), projektowanie obiektów może znacznie różnić się od projektów uzyskanych z projektowania obiektowego (OOD). Konstrukcja niższego poziomu ma na celu zobiektywizowanie algorytmu.

Kilka zalet programowania obiektowego (OOP), takich jak enkapsulacja, polimorfizm i kompozycja, wciąż można czerpać z niskopoziomowego algorytmizowania obiektów. Jest to główne uzasadnienie używania OOP na tym poziomie.

Większość korzyści z projektowania obiektowego (OOD) zostaje utracona. Co najważniejsze, nie ma intuicyjności w projektowaniu niskiego poziomu. Inny programista nie może nauczyć się pracy z kodem niższego poziomu bez uprzedniego pełnego zrozumienia, w jaki sposób algorytm został przekształcony i rozłożony w pierwszej kolejności, a tego zrozumienia nie można uzyskać z wynikowego kodu.


2

Tak, czasami kod musi być „brzydki”, aby działał w wymaganym czasie, ale cały kod nie musi być brzydki. Wydajność należy przetestować i profilować wcześniej, aby znaleźć fragmenty kodu, które muszą być „brzydkie”, a te sekcje należy zanotować z komentarzem, aby przyszli twórcy wiedzieli, co jest celowo brzydkie, a co lenistwem. Jeśli ktoś pisze dużo źle zaprojektowanego kodu, który twierdzi, że ma wpływ na wydajność, spraw, by to udowodnił.

Szybkość jest tak samo ważna, jak każde inne wymaganie programu, podanie błędnych poprawek w pocisku kierowanym jest równoważne zapewnieniu poprawnych poprawek po uderzeniu. Utrzymanie jest zawsze sprawą drugorzędną w stosunku do działającego kodu.


2

Niektóre badania, które widziałem fragmenty wskazują, że czysty, łatwy do odczytania kod jest często szybszy niż bardziej złożony, trudny do odczytania kod. Po części wynika to ze sposobu zaprojektowania optymalizatorów. Zwykle znacznie lepiej optymalizują zmienną w rejestrze, niż robią to samo z pośrednim wynikiem obliczeń. Długie sekwencje przypisań przy użyciu jednego operatora prowadzące do ostatecznego wyniku można zoptymalizować lepiej niż długie skomplikowane równanie. Nowsze optymalizatory mogą zmniejszyć różnicę między czystym a skomplikowanym kodem, ale wątpię, czy to wyeliminowały.

Inne optymalizacje, takie jak rozwijanie pętli, można w razie potrzeby dodawać w czysty sposób.

Każdej optymalizacji dodanej w celu poprawy wydajności powinien towarzyszyć odpowiedni komentarz. Powinno to zawierać stwierdzenie, że zostało dodane jako optymalizacja, najlepiej ze miarami wydajności przed i po.

Odkryłem, że reguła 80/20 dotyczy zoptymalizowanego kodu. Zasadniczo nie optymalizuję niczego, co nie zajmuje co najmniej 80% czasu. Następnie staram się (i zwykle osiągam) 10-krotny wzrost wydajności. To poprawia wydajność około 4-krotnie. Większość wdrożonych przeze mnie optymalizacji nie czyniła kodu znacznie mniej „pięknym”. Twój przebieg może się różnić.


2

Jeśli przez brzydki masz na myśli trudny do odczytania / zrozumienia na poziomie, na którym inni programiści będą go ponownie używać lub będą musieli go zrozumieć, to powiedziałbym, że elegancki, łatwy do odczytania kod prawie zawsze ostatecznie da ci wzrost wydajności na dłuższą metę w aplikacji, którą musisz utrzymywać.

W przeciwnym razie czasami jest wystarczająco dużo wygranej wydajności, aby warto było brzydko umieścić w pięknym pudełku z interfejsem zabójcy, ale z mojego doświadczenia jest to dość rzadki dylemat.

Pomyśl o podstawowym unikaniu pracy na bieżąco. Zachowaj tajemne sztuczki na wypadek, gdyby pojawił się problem z wydajnością. A jeśli musisz napisać coś, co ktoś może zrozumieć tylko dzięki znajomości konkretnej optymalizacji, zrób co możesz, aby uczynić to brzydkie łatwym do zrozumienia z ponownego użycia twojego kodu. Kod, który wykonuje żałośnie rzadko, nigdy tego nie robi, ponieważ programiści zbyt intensywnie zastanawiają się nad tym, co odziedziczy następny facet, ale jeśli częste zmiany są jedyną stałą aplikacji (większość aplikacji internetowych z mojego doświadczenia), sztywny / nieelastyczny kod, który jest trudny do zmodyfikowania jest praktycznie błaganiem o pojawienie się panicznych bałaganów w całej bazie kodu. Czysty i chudy jest lepszy dla wydajności na dłuższą metę.


Chciałbym zasugerować dwie zmiany: (1) Są miejsca, w których potrzebna jest prędkość. Sądzę, że w tych miejscach warto uczynić interfejs łatwiejszym do zrozumienia, niż ułatwić implementację , ponieważ ta ostatnia może być znacznie trudniejsza. (2) „Kod, który wykonuje niefortunnie rzadko, robi to…”, który chciałbym przeformułować jako „Silny nacisk na elegancję i prostotę kodu rzadko jest przyczyną niefortunnego działania. To pierwsze jest nawet ważniejsze, jeśli częste zmiany spodziewane są ... ”
rwong,

Implementacja była złym wyborem słów w rozmowie OOPish. Miałem na myśli łatwość ponownego użycia i edytowanie. # 2, właśnie dodałem zdanie, aby ustalić, że 2 jest zasadniczo punktem, o którym mówiłem.
Erik Reppen

1

Złożone i brzydkie to nie to samo. Kod, który ma wiele specjalnych przypadków, jest zoptymalizowany, aby wyłapywać każdą ostatnią kroplę wydajności, i który na początku wygląda na plątaninę połączeń i zależności, w rzeczywistości może być bardzo starannie zaprojektowany i całkiem piękny, gdy go zrozumiesz. Rzeczywiście, jeśli wydajność (mierzona opóźnieniem lub czymś innym) jest wystarczająco ważna, aby uzasadnić bardzo złożony kod, kod musi być dobrze zaprojektowany. Jeśli tak nie jest, nie możesz być pewien, że cała ta złożoność jest naprawdę lepsza niż prostsze rozwiązanie.

Brzydki kod jest dla mnie niedbały, źle rozważany i / lub niepotrzebnie skomplikowany. Nie sądzę, byś chciał, aby którakolwiek z tych funkcji kodu działała.


1

Czy uważasz, że istnieje kompromis między pisaniem „ładnego” kodu obiektowego a pisaniem bardzo szybkiego kodu o niskim opóźnieniu? Na przykład unikanie funkcji wirtualnych w C ++ / narzutu polimorfizmu itp. - ponowne pisanie kodu, który wygląda paskudnie, ale jest bardzo szybki itp.?

Pracuję w dziedzinie, która jest nieco bardziej skoncentrowana na przepustowości niż na opóźnieniu, ale jest to bardzo ważne z punktu widzenia wydajności i powiedziałbym „trochę” .

Problemem jest jednak to, że tak wielu ludzi błędnie rozumie swoje wyniki. Nowicjusze często źle rozumieją wszystko, a cały ich koncepcyjny model „kosztu obliczeniowego” wymaga przerobienia, przy czym jedyną możliwą rzeczą jest poprawność algorytmiczna. Półprodukty źle się mylą. Eksperci nie rozumieją pewnych rzeczy.

Mierzenie za pomocą dokładnych narzędzi, które mogą dostarczać takich danych, jak błędy pamięci podręcznej i nieprzewidywalne oddziały, to wszystko to, co zapewnia kontrolę wszystkim osobom o dowolnym poziomie wiedzy w tej dziedzinie.

Mierzenie jest również tym, co wskazuje, czego nie należy optymalizować . Eksperci często spędzają mniej czasu na optymalizacji niż nowicjusze, ponieważ optymalizują naprawdę zmierzone punkty aktywne i nie próbują optymalizować dzikich dźgnięć w ciemności w oparciu o przeczucia co może być powolne (co w skrajnej formie może kusić jednego do mikrooptymalizacji tylko o każdej innej linii w bazie kodu).

Projektowanie pod kątem wydajności

Poza tym kluczem do projektowania pod kątem wydajności jest część projektowa , podobnie jak projektowanie interfejsu. Jednym z problemów związanych z niedoświadczeniem jest to, że istnieje wczesna zmiana bezwzględnych wskaźników implementacji, takich jak koszt wywołania funkcji pośredniej w pewnym uogólnionym kontekście, tak jakby koszt (który jest lepiej zrozumiany w bezpośrednim znaczeniu z punktu widzenia optymalizatora widoku, a nie rozgałęziony punkt widzenia) jest powodem, aby unikać go w całej bazie kodu.

Koszty są względne . Podczas gdy pośrednie wywołanie funkcji wiąże się z kosztami, np. Wszystkie koszty są względne. Jeśli płacisz ten koszt jednorazowo, by wywołać funkcję, która przechodzi przez miliony elementów, martwienie się tym kosztem jest jak spędzanie godzin na targowaniu się o grosze na zakup produktu wartego miliard dolarów, tylko po to, aby nie kupować tego produktu, ponieważ był o jeden grosz za drogi.

Prostszy projekt interfejsu

Aspekt wydajności interfejsu związany z projektowaniem często dąży wcześniej do podniesienia tych kosztów na wyższy poziom. Zamiast płacić na przykład koszty abstrakcji dla pojedynczej cząstki, możemy na przykład przesunąć ten koszt do poziomu układu cząstek / emitera, skutecznie przekształcając cząstkę w szczegół implementacyjny i / lub po prostu surowe dane z tej kolekcji cząstek.

Dlatego projektowanie obiektowe nie musi być niezgodne z projektowaniem pod kątem wydajności (opóźnienia lub przepustowości), ale mogą istnieć pokusy w języku, który skupia się na tym, aby modelować coraz mniejsze obiekty ziarniste, i tam najnowszy optymalizator nie może Wsparcie. Nie może robić rzeczy takich jak łączenie klasy reprezentującej pojedynczy punkt w sposób, który zapewnia wydajną reprezentację SoA dla wzorców dostępu do pamięci oprogramowania. Zbiór punktów z projektem interfejsu modelowanym na poziomie zgrubności daje taką możliwość i pozwala na iterację w kierunku coraz bardziej optymalnych rozwiązań w razie potrzeby. Taki projekt jest przeznaczony do pamięci masowej *.

* Zwróć uwagę na nacisk na pamięć , a nie na dane , ponieważ praca w obszarach krytycznych pod względem wydajności przez długi czas będzie miała tendencję do zmiany twojego widoku typów danych i struktur danych oraz zobaczenia, jak łączą się z pamięcią. Drzewo wyszukiwania binarnego nie skupia się już wyłącznie na złożoności logarytmicznej w takich przypadkach, jak potencjalnie rozbieżne i nieprzyjazne dla pamięci podręcznej fragmenty pamięci dla węzłów drzew, chyba że jest to wspomagane przez stały alokator. Widok nie odrzuca złożoności algorytmicznej, ale widzi, że nie jest już niezależny od układów pamięci. Zaczyna się także widzieć, że iteracje pracy są bardziej na temat iteracji dostępu do pamięci. *

Wiele projektów o kluczowym znaczeniu dla wydajności może w rzeczywistości być bardzo zgodnych z koncepcją projektów interfejsów wysokiego poziomu, które są łatwe do zrozumienia i użytkowania przez ludzi. Różnica polega na tym, że „wysoki poziom” w tym kontekście dotyczyłby masowej agregacji pamięci, interfejsu modelowanego dla potencjalnie dużych zbiorów danych oraz implementacji pod maską, która może być dość niskiego poziomu. Wizualną analogią może być samochód, który jest naprawdę wygodny, łatwy w prowadzeniu i prowadzeniu oraz bardzo bezpieczny podczas jazdy z prędkością dźwięku, ale jeśli otworzysz maskę, w środku jest niewiele demonów oddychających ogniem.

Przy bardziej zgrubnej konstrukcji łatwiejszy jest również sposób na zapewnienie bardziej wydajnych wzorców blokowania i wykorzystanie równoległości w kodzie (wielowątkowość jest wyczerpującym tematem, który pominę tutaj).

Pula pamięci

Krytycznym aspektem programowania o niskim opóźnieniu będzie prawdopodobnie bardzo wyraźna kontrola pamięci w celu poprawy lokalizacji odniesienia, a także po prostu ogólna szybkość przydzielania i zwalniania pamięci. Pamięć puli niestandardowego alokatora faktycznie odzwierciedla ten sam sposób myślenia, który opisaliśmy. Jest przeznaczony dla luzem ; jest zaprojektowany na grubym poziomie. Wstępnie przydziela pamięć w dużych blokach i gromadzi pamięć już przydzieloną w małych porcjach.

Pomysł jest dokładnie taki sam, jak wypychanie kosztownych rzeczy (alokowanie fragmentu pamięci do alokatora ogólnego przeznaczenia, np.) Na bardziej zgrubny i grubszy poziom. Pula pamięci została zaprojektowana do pracy z pamięcią masową .

Typy systemów Segreguj pamięć

Jedną z trudności związanych ze szczegółowym projektowaniem obiektowym w dowolnym języku jest to, że często chce wprowadzić wiele drobnych typów i struktur danych zdefiniowanych przez użytkownika. Te typy mogą następnie chcieć być przydzielane w małych kawałkach, jeśli są dynamicznie przydzielane.

Typowym przykładem w C ++ byłyby przypadki, w których wymagany jest polimorfizm, w których naturalną pokusą jest przydzielenie każdej instancji podklasy względem alokatora pamięci ogólnego przeznaczenia.

To ostatecznie rozbija prawdopodobnie sąsiadujące ze sobą układy pamięci na małe, bity kawałki i fragmenty rozproszone w całym zakresie adresowania, co przekłada się na więcej błędów stron i braków pamięci podręcznej.

Pola wymagające reakcji o najniższym opóźnieniu, bez zacinania się i deterministycznej reakcji są prawdopodobnie jedynym miejscem, w którym punkty aktywne nie zawsze sprowadzają się do pojedynczego wąskiego gardła, w którym niewielkie nieefektywności mogą rzeczywiście „akumulować” się (coś, co wielu ludzi sobie wyobraża) dzieje się niepoprawnie z profilerem, aby utrzymać je w ryzach, ale w polach opartych na opóźnieniach mogą zdarzyć się rzadkie przypadki, w których kumulują się niewielkie nieefektywności). A najczęstszymi przyczynami takiej akumulacji mogą być: nadmierna alokacja małych fragmentów pamięci w całym miejscu.

W językach takich jak Java pomocne może być użycie większej liczby tablic zwykłych starych typów danych, gdy jest to możliwe, dla wąskich obszarów (obszarów przetwarzanych w ciasnych pętlach), takich jak tablica int(ale nadal za obszernym interfejsem wysokiego poziomu) zamiast, powiedzmy , obiektów ArrayListzdefiniowanych przez użytkownika Integer. Pozwala to uniknąć segregacji pamięci, która zwykle towarzyszy temu ostatniemu. W C ++ nie musimy tak bardzo degradować struktury, jeśli nasze wzorce alokacji pamięci są wydajne, ponieważ typy zdefiniowane przez użytkownika mogą być alokowane w sposób ciągły, a nawet w kontekście ogólnego kontenera.

Łączenie pamięci z powrotem razem

Rozwiązaniem tutaj jest sięgnięcie po niestandardowy alokator dla jednorodnych typów danych, a być może nawet dla jednorodnych typów danych. Kiedy małe typy danych i struktury danych są spłaszczone do bitów i bajtów w pamięci, przyjmują one jednorodny charakter (aczkolwiek z pewnymi różnymi wymaganiami dotyczącymi wyrównania). Kiedy nie patrzymy na nie z nastawienia na pamięć, system typów języków programowania „chce” podzielić / segregować potencjalnie sąsiadujące regiony pamięci na małe, maleńkie porozrzucane fragmenty.

Stos wykorzystuje skupienie skoncentrowane na pamięci, aby tego uniknąć i potencjalnie przechowywać w nim wszelkie możliwe mieszane kombinacje instancji typów zdefiniowanych przez użytkownika. Lepsze wykorzystanie stosu to świetny pomysł, gdy jest to możliwe, ponieważ jego górna część prawie zawsze znajduje się w linii pamięci podręcznej, ale możemy również zaprojektować alokatory pamięci, które naśladują niektóre z tych cech bez wzorca LIFO, łącząc pamięć w różnych typach danych w ciągłe części nawet w przypadku bardziej złożonych wzorców alokacji pamięci i dezalokacji.

Nowoczesny sprzęt zaprojektowano tak, aby osiągał szczyt w przetwarzaniu ciągłych bloków pamięci (wielokrotny dostęp do tej samej linii pamięci podręcznej, tej samej strony, np.). Słowo kluczowe jest przylegające, ponieważ jest to korzystne tylko wtedy, gdy istnieją otaczające dane zainteresowania. Tak więc kluczową kwestią (a jednocześnie trudnością) w wydajności jest ponowne scalenie segregowanych fragmentów pamięci z powrotem w ciągłe bloki, które są dostępne w całości (wszystkie dane otaczające są istotne) przed eksmisją. Największą przeszkodą może być tutaj bogaty system typów szczególnie zdefiniowanych przez użytkownika w językach programowania, ale zawsze możemy sięgnąć i rozwiązać problem za pomocą niestandardowego alokatora i / lub bardziej obszernych projektów, gdy jest to właściwe.

Brzydki

Trudno powiedzieć „brzydkie”. To subiektywna metryka, a ktoś, kto pracuje w bardzo krytycznym dla wydajności polu, zacznie zmieniać swoje pojęcie „piękna” na takie, które jest bardziej zorientowane na dane i skupia się na interfejsach przetwarzających rzeczy masowo.

Niebezpieczny

„Niebezpieczne” może być łatwiejsze. Ogólnie rzecz biorąc, wydajność zwykle chce sięgać w kierunku kodu niższego poziomu. Na przykład implementacja alokatora pamięci jest niemożliwa bez sięgania poniżej typów danych i pracy na niebezpiecznym poziomie surowych bitów i bajtów. W rezultacie może pomóc skupić się na starannej procedurze testowania w tych podsystemach o kluczowym znaczeniu dla wydajności, skalując dokładność testowania z poziomem zastosowanych optymalizacji.

Piękno

Wszystko to byłoby jednak na poziomie szczegółowości wdrożenia. Zarówno weterani nastawieni na dużą skalę, jak i krytyczni pod względem wydajności, „piękno” dąży raczej do projektowania interfejsów niż do szczegółów implementacji. Poszukiwanie „pięknych”, użytecznych, bezpiecznych i wydajnych interfejsów zamiast implementacji staje się wykładniczo wyższym priorytetem ze względu na pęknięcia sprzężenia i kaskady, które mogą wystąpić w obliczu zmiany projektu interfejsu. Implementacje można zamienić w dowolnym momencie. Zazwyczaj iterujemy w kierunku wydajności w razie potrzeby i zgodnie z pomiarami. Kluczem w projekcie interfejsu jest modelowanie na wystarczająco grubym poziomie, aby pozostawić miejsce na takie iteracje bez uszkodzenia całego systemu.

W rzeczywistości sugerowałbym, że weteran skupiający się na rozwoju krytycznym pod względem wydajności często będzie koncentrował się głównie na bezpieczeństwie, testowaniu, łatwości konserwacji, tylko uczniu SE w ogóle, ponieważ baza kodu na dużą skalę, która ma wiele wyników - krytyczne podsystemy (systemy cząstek, algorytmy przetwarzania obrazu, przetwarzanie wideo, sprzężenie audio, raytracery, silniki siatkowe itp.) będą musiały zwracać szczególną uwagę na inżynierię oprogramowania, aby uniknąć utonięcia w koszmarze konserwacji. To nie przypadek, że często najbardziej zadziwiająco wydajne produkty mogą zawierać najmniej błędów.

TL; DR

W każdym razie to moje podejście do tematu, począwszy od priorytetów w obszarach naprawdę krytycznych pod względem wydajności, co może zmniejszyć opóźnienia i powodować niewielką nieefektywność, i to, co faktycznie stanowi „piękno” (patrząc na rzeczy najbardziej produktywnie).


0

Nie inaczej, ale oto co robię:

  1. Napisz to czyste i łatwe do utrzymania.

  2. Wykonaj diagnostykę wydajności i napraw problemy, które ci podpowiada, a nie te, które zgadujesz. Gwarantujemy, że będą się różnić od oczekiwań.

Możesz wykonać te poprawki w sposób, który jest nadal przejrzysty i łatwy do utrzymania, ale będziesz musiał dodać komentarz, aby osoby, które spojrzą na kod, wiedziały, dlaczego to zrobiłeś. Jeśli tego nie zrobisz, cofną to.

Czy to jest kompromis? Naprawdę tak nie uważam.


0

Możesz pisać brzydki kod, który jest bardzo szybki, a także pisać piękny kod, tak szybki jak brzydki kod. Wąskim gardłem nie będzie piękno / organizacja / struktura twojego kodu, ale wybrane techniki. Na przykład, czy używasz gniazd nieblokujących? Czy używasz projektu jednowątkowego? Czy używasz kolejki bez blokady do komunikacji między wątkami? Czy produkujesz śmieci dla GC? Czy wykonujesz blokującą operację we / wy w wątku krytycznym? Jak widać, nie ma to nic wspólnego z pięknem.


0

Co jest ważne dla użytkownika końcowego?

  • Wydajność
  • Funkcje / funkcjonalność
  • Projekt

Przypadek 1: Zoptymalizowany zły kod

  • Trudna konserwacja
  • Trudno go odczytać, jeśli jest projektem typu open source

Przypadek 2: Niezoptymalizowany dobry kod

  • Łatwa konserwacja
  • Złe doświadczenie użytkownika

Rozwiązanie?

Łatwe, optymalizujące fragmenty kodu o krytycznym działaniu

na przykład:

Program składający się z 5 metod , 3 z nich służą do zarządzania danymi, 1 do odczytu dysku, drugi do zapisu na dysku

Te 3 metody zarządzania danymi wykorzystują dwie metody We / Wy i są od nich zależne

Zoptymalizowalibyśmy metody We / Wy.

Powód: metody I / O są mniej prawdopodobne, że zostaną zmienione, ani nie wpływają na projekt aplikacji, i w sumie wszystko w tym programie zależy od nich, a zatem wydają się krytyczne pod względem wydajności, użyjemy dowolnego kodu, aby je zoptymalizować .

Oznacza to, że otrzymujemy dobry kod i łatwą do zarządzania konstrukcję programu, jednocześnie utrzymując go przez optymalizację niektórych części kodu

Myślę..

Myślę, że zły kod utrudnia ludziom optymalizację po polsku, a małe błędy mogą nawet pogorszyć sprawę, więc dobry kod dla początkującego / początkującego byłby lepszy, gdyby dobrze napisał ten brzydki kod.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.