Czy stopniowa zmiana metodologii pisania kodu wpłynęła na wydajność systemu? I czy powinno mnie to obchodzić?


35

TD; DR:

Było trochę zamieszania co do tego, o co pytałem, więc oto najważniejszy pomysł na pytanie:

Zawsze chciałem, aby pytanie było takie, jakie jest. Być może początkowo nie wyraziłem tego dobrze. Ale intencja zawsze była „ modułowa, oddzielona, ​​luźno sprzężona, odsprzężona, zrefaktoryzowany kod ” znacznie wolniejszy z samej swojej natury niż „ monolityczna pojedyncza jednostka, rób wszystko w jednym miejscu, jeden plik, ściśle powiązany ” kod. Reszta to tylko szczegóły i różne przejawy tego, na które natknąłem się wtedy, teraz, czy później. Na pewno jest wolniejszy na pewną skalę. Podobnie jak nie defragmentowany dysk, musisz odbierać elementy zewsząd. Jest wolniejszy. Na pewno. Ale czy powinno mnie to obchodzić?

Pytanie nie dotyczy…

nie chodzi o mikrooptymalizację, przedwczesną optymalizację itp. Nie chodzi o „optymalizację tej czy innej części na śmierć”.

Co to jest?

Chodzi o ogólną metodologię, techniki i sposoby myślenia o pisaniu kodu, które pojawiły się z czasem:

  • „wstrzyknij ten kod do swojej klasy jako zależność”
  • „napisz jeden plik na klasę”
  • „oddziel widok od bazy danych, kontrolera, domeny”.
  • nie pisz jednorodnego pojedynczego kodu spaghetti, ale pisz wiele osobnych modułowych komponentów, które współpracują ze sobą

Chodzi o sposób i styl kodu, który jest obecnie - w ciągu tej dekady - postrzegany i zalecany w większości ram, zalecany na konwencjach, przekazywany przez społeczność. Jest to zmiana myślenia z „monolitycznych bloków” na „mikrousługi”. A do tego przychodzi cena pod względem wydajności i narzutu na poziomie maszyny, a także narzutu na poziomie programisty.

Pierwotne pytanie brzmi:

W dziedzinie informatyki zauważyłem znaczącą zmianę myślenia w zakresie programowania. Często spotykam się z radą, która wygląda następująco:

  • napisz mniejszy pod względem funkcjonalnym kod (w ten sposób bardziej testowalny i konserwowalny)
  • przekształć istniejący kod na coraz mniejsze fragmenty kodu, aż większość metod / funkcji będzie miała tylko kilka wierszy i nie będzie jasne, jaki jest ich cel (co tworzy więcej funkcji w porównaniu z większym blokiem monolitycznym)
  • pisz funkcje, które wykonują tylko jedną rzecz - rozdzielenie problemów itp. (co zwykle tworzy więcej funkcji i więcej ramek na stosie)
  • tworzyć więcej plików (jedna klasa na plik, więcej klas do celów dekompozycji, do celów warstw, takich jak MVC, architektura domeny, wzorce projektowe, OO itp., co tworzy więcej wywołań systemu plików)

Jest to zmiana w porównaniu do „starych” lub „przestarzałych” lub „przestarzałych” praktyk kodowania, w których istnieją metody obejmujące 2500 linii, a duże klasy i boskie obiekty robią wszystko.

Moje pytanie brzmi:

gdy wywołanie sprowadza się do kodu maszynowego, 1 i 0, instrukcji montażu, talerzy HDD, czy powinienem się w ogóle martwić, że mój doskonale odseparowany klasowo kod OO z różnymi refaktoryzowanymi funkcjami i metodami od małych do małych generuje też dużo dodatkowych kosztów ogólnych?

Detale

Chociaż nie jestem do końca zaznajomiony z tym, w jaki sposób kod OO i jego wywołania metod są obsługiwane w ASM, a także w jaki sposób wywołania DB i wywołania kompilatora przekładają się na ruchome ramię siłownika na talerzu HDD, mam pewien pomysł. Zakładam, że każde dodatkowe wywołanie funkcji, wywołanie obiektu lub wywołanie „#include” (w niektórych językach) generuje dodatkowy zestaw instrukcji, zwiększając w ten sposób objętość kodu i dodając różne koszty związane z okablowaniem kodu, bez dodawania rzeczywistego „użytecznego” kodu . Wyobrażam sobie również, że dobre optymalizacje można przeprowadzić w ASM, zanim zostanie on faktycznie uruchomiony na sprzęcie, ale ta optymalizacja może zrobić tylko tyle.

Stąd moje pytanie - ile narzutu (w przestrzeni i szybkości) robi dobrze oddzielony kod (kod podzielony na setki plików, klas i wzorców projektowych itp.) W rzeczywistości w porównaniu do posiadania „jednej dużej metody, która zawiera wszystko w jednym pliku monolitycznym ”, z powodu tego narzutu?

AKTUALIZACJA dla jasności:

Zakładam, że przyjęcie tego samego kodu i podzielenie go, przeredagowanie go, rozdzielenie go na coraz więcej funkcji i obiektów oraz metod i klas spowoduje, że coraz więcej parametrów będzie przekazywanych między mniejszymi częściami kodu. Ponieważ na pewno kod refaktoryzacji musi podtrzymywać działanie wątku, a to wymaga przekazania parametrów. Więcej metod lub więcej klas lub więcej wzorców projektowych metod fabrycznych powoduje więcej narzutu związanego z przekazywaniem różnych bitów informacji w większym stopniu niż w przypadku pojedynczej klasy monolitycznej lub metody.

Gdzieś powiedziano (zacytuj TBD), że do 70% całego kodu składa się z instrukcji MOV ASM - ładowanie rejestrów CPU odpowiednimi zmiennymi, a nie faktyczne obliczenia. W moim przypadku ładujesz czas procesora instrukcjami PUSH / POP, aby zapewnić powiązanie i przekazywanie parametrów między różnymi częściami kodu. Im mniejsze są fragmenty kodu, tym bardziej wymagane jest ogólne „powiązanie”. Obawiam się, że to powiązanie zwiększa rozdęcie i spowolnienie oprogramowania i zastanawiam się, czy powinienem się tym przejmować i ile, jeśli w ogóle, ponieważ obecne i przyszłe generacje programistów, którzy budują oprogramowanie na następny wiek , będzie musiał żyć i korzystać z oprogramowania zbudowanego przy użyciu tych praktyk.

AKTUALIZACJA: Wiele plików

Piszę teraz nowy kod, który powoli zastępuje stary kod. W szczególności zauważyłem, że jedną ze starych klas był plik linii ~ 3000 (jak wspomniano wcześniej). Teraz staje się zestawem 15-20 plików znajdujących się w różnych katalogach, w tym w plikach testowych, a nie w frameworku PHP, którego używam do łączenia niektórych rzeczy. Nadchodzi także więcej plików. Jeśli chodzi o dyskowe operacje we / wy, ładowanie wielu plików jest wolniejsze niż ładowanie jednego dużego pliku. Oczywiście nie wszystkie pliki są ładowane, są ładowane w razie potrzeby, istnieją opcje buforowania dysku i buforowania pamięci, ale nadal uważam, że loading multiple fileswymaga to więcej przetwarzania niż loading a single filedo pamięci. Dodam to do mojej troski.

AKTUALIZACJA: Zależność Wstrzyknij wszystko

Wracając do tego po chwili .. Myślę, że moje pytanie zostało źle zrozumiane. A może postanowiłem źle zrozumieć niektóre odpowiedzi. Nie mówię o mikrooptymalizacji, ponieważ niektóre odpowiedzi zostały wyszczególnione (przynajmniej myślę, że nazywanie tego, co mówię o mikrooptymalizacji jest mylące), ale o ruchu „kodu refaktoryzacji w celu poluzowania ścisłego sprzężenia” jako całości , na każdym poziomie kodu. Niedawno przyjechałem z Zend Con, gdzie ten styl kodu był jednym z głównych punktów i głównych elementów konwencji. Oddziel logikę od widoku, widok od modelu, model od bazy danych, a jeśli możesz, odsprzęgnij dane od bazy danych. Zależność - wstrzykuje wszystko, co czasami oznacza po prostu dodanie kodu okablowania (funkcje, klasy, płyta kotłowa), który nic nie robi, ale służy jako punkt łączenia / zaczepu, w większości przypadków łatwo podwajając rozmiar kodu.

AKTUALIZACJA 2: Czy „dzielenie kodu na więcej plików” znacząco wpływa na wydajność (na wszystkich poziomach przetwarzania)

Jak filozofia compartmentalize your code into multiple fileswpływa na dzisiejsze obliczenia (wydajność, wykorzystanie dysku, zarządzanie pamięcią, zadania związane z przetwarzaniem procesora)?

mówię o

Przed...

W hipotetycznej, ale dość realnej, nie tak odległej przeszłości, możesz łatwo napisać jeden blok mono pliku, który ma model i widok oraz kontroler spaghetti lub nie-kodowany spaghetti, ale uruchamia wszystko, gdy jest już załadowany. Robiąc pewne testy porównawcze w przeszłości za pomocą kodu C, odkryłem, że O wiele szybsze jest ładowanie pojedynczego pliku 900 Mb do pamięci i przetwarzanie go w dużych porcjach niż ładowanie wiązki mniejszych plików i przetwarzanie ich w mniejszy posiłek części wykonujące tę samą pracę w końcu.

.. I teraz*

Dzisiaj patrzę na kod, który pokazuje księgę, która ma takie funkcje jak ... jeśli element jest „zamówieniem”, blok HTML pokazujący zamówienie. Jeśli element zamówienia można skopiować, wydrukuj blok HTML, który wyświetla ikonę i parametry HTML za nim, umożliwiając wykonanie kopii. Jeśli element można przenieść w górę lub w dół, wyświetl odpowiednie strzałki HTML. Itd. Mogę, dzięki Zend Framework, tworzyćpartial()wywołań, co w zasadzie oznacza „wywołanie funkcji, która pobiera parametry i wstawia je do osobnego pliku HTML, który również wywołuje”. W zależności od tego, jak szczegółowo chcę uzyskać, mogę utworzyć osobne funkcje HTML dla najmniejszych części księgi. Jeden dla strzałki w górę, strzałki w dół, jeden dla „czy mogę skopiować ten element” itp. Łatwe tworzenie kilku plików w celu wyświetlenia niewielkiej części strony. Biorąc mój kod i zakulisowy kod Zend Framework, system / stos prawdopodobnie wywołuje blisko 20-30 różnych plików.

Co?

Interesują mnie aspekty, zużycie urządzenia, które jest tworzone przez dzielenie kodu na wiele mniejszych osobnych plików.

Na przykład ładowanie większej liczby plików oznacza umieszczenie ich w różnych miejscach systemu plików oraz w różnych miejscach fizycznego dysku twardego, co oznacza więcej czasu na wyszukiwanie i czytanie dysku twardego.

Dla procesora prawdopodobnie oznacza to większe przełączanie kontekstu i ładowanie różnych rejestrów.

W tym podbloku (aktualizacja nr 2) bardziej interesuje mnie, w jaki sposób użycie wielu plików do wykonania tych samych zadań, które można wykonać w jednym pliku, wpływa na wydajność systemu.

Korzystanie z interfejsu API Zend Form vs prosty HTML

Użyłem Zend Form API z najnowszymi i najlepszymi nowoczesnymi praktykami OO, aby zbudować formularz HTML z weryfikacją, przekształcając się POSTw obiekty domeny.

Wykonanie go zajęło mi 35 plików.

35 files = 
    = 10 fieldsets x {programmatic fieldset + fieldset manager + view template} 
    + a few supporting files

Wszystkie z nich można zastąpić kilkoma prostymi plikami HTML + PHP + JS + CSS, być może 4 lekkimi plikami.

Czy lepiej? Czy warto? ... Wyobraź sobie, że ładujesz 35 plików + liczne pliki biblioteki Zend Zramework, które sprawiają, że działają, a 4 proste pliki.


1
Niesamowite pytanie. Przeprowadzę testy porównawcze (zajmij mi około dnia, aby znaleźć dobre przypadki). Jednak zwiększenie prędkości w tym stopniu wiąże się z ogromnymi kosztami dla czytelności i kosztów rozwoju. Domyślam się, że rezultatem jest znikomy wzrost wydajności.
Dan Sabin

7
@ Dan: Czy możesz umieścić go w swoim kalendarzu, aby przetestować kod po 1, 5 i 10 latach konserwacji. Jeśli pamiętam, sprawdzę wyniki :)
mattnz

1
Tak, to prawdziwy kicker. Myślę, że wszyscy się zgadzamy, że mniej wyszukiwań, a wywołania funkcji są szybsze. Jednak nie mogę sobie wyobrazić przypadku, w którym wolałbym takie rzeczy, jak utrzymanie i łatwe szkolenie nowych członków zespołu.
Dan Sabin

2
Aby wyjaśnić, czy masz określone wymagania dotyczące prędkości dla swojego projektu. A może po prostu chcesz, aby Twój kod „działał szybciej!” Jeśli to ten drugi, nie martwiłbym się tym, kod, który jest wystarczająco szybki, ale łatwy w utrzymaniu, jest o wiele lepszy niż kod, który jest szybszy niż wystarczająco szybki, ale jest bałaganem.
Cormac Mulhall

4
Pomysł unikania wywołań funkcji ze względu na wydajność jest dokładnie takim szaleństwem, w którym Dijkstra upierał się w swoim słynnym cytacie o przedwczesnej optymalizacji. Poważnie, nie mogę
RibaldEddie

Odpowiedzi:


10

Moje pytanie brzmi: kiedy wywołanie sprowadza się do kodu maszynowego, 1 i 0, instrukcji montażu, czy powinienem się w ogóle martwić, że mój rozdzielony klasami kod z różnorodnymi funkcjami od małych do małych generuje zbyt wiele dodatkowego obciążenia?

MOJA odpowiedź brzmi: tak, powinieneś. Nie dlatego, że masz wiele małych funkcji (dawno temu narzut funkcji wywoływania był dość znaczny i możesz spowolnić swój program, wykonując milion małych wywołań w pętlach, ale dziś kompilatory wstawią je dla ciebie i to, co pozostało, jest zajęte ostrożne algorytmy przewidywania procesora, więc nie przejmuj się tym), ale ponieważ wprowadzisz koncepcję zbytniego nakładania warstw na swoje programy, gdy ich funkcjonalność jest zbyt mała, aby rozsądnie zaprzątać sobie głowę. Jeśli masz większe komponenty, możesz być pewny, że nie wykonują one tej samej pracy w kółko, ale możesz sprawić, że twój program będzie tak drobiazgowo drobiazgowy, że możesz nie być w stanie naprawdę zrozumieć ścieżek wywoływania, i w rezultacie coś z tego to ledwo działa (i jest ledwo możliwe do utrzymania).

Na przykład pracowałem w miejscu, które pokazało mi projekt referencyjny dla usługi internetowej z 1 metodą. Projekt składał się z 32 plików .cs - dla jednej usługi internetowej! Uznałem, że jest to zbyt skomplikowane, mimo że każda część była niewielka i łatwa do zrozumienia sama w sobie, gdy chodziło o opisanie całego systemu, szybko musiałem prześledzić połączenia tylko po to, aby zobaczyć, co u diabła robi (tam było też zbyt wiele abstrakcji, jak można się spodziewać). Moja zastępcza usługa internetowa miała 4 pliki .cs.

nie mierzyłem wydajności, ponieważ sądzę, że byłby mniej więcej taki sam, ale mogę zagwarantować, że moja będzie znacznie tańsza w utrzymaniu. Kiedy wszyscy mówią o tym, że czas programisty jest ważniejszy niż czas procesora, wtedy stwórz złożone potwory, które kosztują dużo czasu programisty zarówno w fazie projektowania, jak i konserwacji, musisz się zastanawiać, że usprawiedliwiają złe zachowanie.

Gdzieś powiedziano (zacytuj TBD), że do 70% całego kodu składa się z instrukcji MOV ASM - ładowanie rejestrów CPU odpowiednimi zmiennymi, a nie faktyczne obliczenia.

Że jest co robić Procesory choć poruszają bitów z pamięci do rejestru, dodać lub odjąć je, a następnie umieścić je z powrotem do pamięci. Całe obliczenia sprowadzają się do tego. Pamiętaj, że kiedyś miałem bardzo wielowątkowy program, który spędzał większość swojego czasu na przełączaniu kontekstu (tj. Zapisywaniu i przywracaniu stanu rejestru wątków), niż pracował na kodzie wątku. Prosta blokada w niewłaściwym miejscu naprawdę popsuła wydajność i był to również tak niewinny kod.

Moja rada brzmi: znajdź rozsądny środek między skrajnościami, które sprawią, że Twój kod będzie wyglądał dobrze dla innych ludzi, i przetestuj system, aby sprawdzić, czy działa dobrze. Użyj funkcji systemu operacyjnego, aby upewnić się, że działa tak, jak można się tego spodziewać z procesorem, pamięcią, dyskiem i IO sieci.


Myślę, że teraz mówi to do mnie najwięcej. Sugeruje mi, że dobrym punktem wyjścia jest rozpoczęcie od koncepcji mapowania umysłu do fragmentów kodu podczas podążania za pewnymi pojęciami (np. DI), zamiast zajmowania się ezoterycznym rozkładem kodu i szaleństwem (tj. DI wszystko, czy potrzebujesz to czy nie).
Dennis

1
Osobiście uważam, że bardziej „nowoczesny” kod jest nieco łatwiej profilować ... Myślę, że im łatwiejszy do utrzymania kawałek kodu, jest również łatwiej profilować, ale jest pewien limit, w którym dzielenie rzeczy na małe kawałki czyni je mniej konserwowalne ...
AK_

25

Bez szczególnej ostrożności mikrooptymalizacja, taka jak te, prowadzi do niemożliwego do utrzymania kodu.

Początkowo wygląda to na dobry pomysł, profiler mówi ci, że kod jest szybszy, a V & V / Test / QA nawet mówi, że działa. Wkrótce zostaną znalezione błędy, zmienią się wymagania i zostaną poproszone o ulepszenia, które nigdy nie były brane pod uwagę.

W trakcie życia projektu kod ulega degradacji i staje się mniej wydajny. Utrzymywalny kod stanie się bardziej wydajny niż jego niemożliwy do utrzymania odpowiednik, ponieważ obniży się wolniej. Powodem jest to, że kod buduje entropię po zmianie -

Nieusuwalny kod szybko zawiera więcej martwego kodu, zbędne ścieżki i powielanie. Prowadzi to do większej liczby błędów, tworząc cykl degradacji kodu - w tym jego wydajności. Wkrótce programiści nie mają pewności, że wprowadzane zmiany są poprawne. Spowalnia to ich, czyni ostrożność i ogólnie prowadzi do jeszcze większej entropii, ponieważ odnoszą się tylko do szczegółów, które widzą

Utrzymywalny kod, z małymi modułami i testami jednostkowymi, jest łatwiejszy do zmiany, kod, który nie jest już potrzebny, łatwiej jest zidentyfikować i usunąć. Złamany kod jest również łatwiejszy do zidentyfikowania, można go naprawić lub zastąpić z pewnością.

W końcu sprowadza się to do zarządzania cyklem życia i nie jest tak proste jak „to jest szybsze, więc zawsze będzie szybsze”.

Przede wszystkim powolny poprawny kod jest nieskończenie szybszy niż szybki niepoprawny kod.


Dziękuję Aby przybliżyć to nieco temu, dokąd zmierzam, nie mówię o mikrooptymalizacji, ale o globalnym posunięciu w kierunku pisania mniejszego kodu, obejmującego więcej wstrzykiwania zależności, a tym samym więcej zewnętrznych elementów funkcjonalnych i ogólnie więcej „ruchomych części” kodu że wszystkie muszą być ze sobą połączone, aby mogły działać. Wydaje mi się, że na poziomie sprzętowym powoduje to zwiększenie puchu / złącza / przejścia zmiennej / MOV / PUSH / POP / CALL / JMP. Widzę też wartość w przejściu na czytelność kodu, chociaż kosztem poświęcenia cykli obliczeniowych na poziomie sprzętowym dla „puchu”.
Dennis

10
Unikanie wywołań funkcji ze względu na wydajność jest absolutnie mikrooptymalizacją! Poważnie. Nie mogę wymyślić lepszego przykładu mikrooptymalizacji. Jakie masz dowody na to, że różnica w wydajności ma znaczenie dla rodzaju oprogramowania, które piszesz? Wygląda na to, że ich nie masz.
RibaldEddie

17

W moim rozumieniu, jak wskazano w tekście, na niższych poziomach kodu, takich jak C ++, może to mieć znaczenie, ale mówię CAN lekko.

Witryna podsumowuje - nie ma łatwej odpowiedzi . To zależy od systemu, zależy od tego, co robi twoja aplikacja, zależy od języka, zależy od kompilatora i optymalizacji.

Na przykład C ++, wbudowany może zwiększyć wydajność. Wiele razy może nic nie robić, a nawet obniżać wydajność, ale osobiście nigdy nie spotkałem się z tym, choć słyszałem o opowiadaniach. Inline to tylko sugestia kompilatora do optymalizacji, którą można zignorować.

Są szanse, że jeśli opracowujesz programy na wyższym poziomie, koszty ogólne nie powinny stanowić problemu, jeśli w ogóle istnieje taki program. Kompilatory są obecnie bardzo inteligentne i i tak powinny sobie z tym poradzić. Wielu programistów ma kod do życia: nigdy nie ufaj kompilatorowi. Jeśli dotyczy to ciebie, to nawet niewielkie optymalizacje, które uważasz za ważne, mogą być ważne. Pamiętaj jednak, że każdy język różni się pod tym względem. Java wykonuje wbudowane optymalizacje automatycznie w czasie wykonywania. W Javascripcie inline dla twojej strony internetowej (w przeciwieństwie do oddzielnych plików) stanowi ulepszenie i każda milisekunda dla strony internetowej może się liczyć, ale to bardziej problem z IO.

Ale w programach niższego poziomu, w których programista może dużo pracować z kodem maszynowym razem z czymś takim jak C ++, podsłuch może mieć znaczenie. Gry są dobrym przykładem tego, gdzie potok procesora ma kluczowe znaczenie, szczególnie na konsolach, a coś takiego jak inline może się tu nieco przydać.

Dobra lektura na stronie : http://www.gotw.ca/gotw/033.htm


Patrząc z perspektywy mojego pytania, nie jestem tak skupiony na inlinizacji per seh, ale na „skodyfikowanym okablowaniu”, które zajmuje proces procesora, magistrali i we / wy łącząc różne fragmenty kodu. Zastanawiam się, czy jest jakiś punkt, w którym jest 50% lub więcej kodu okablowania i 50% faktycznego kodu, który chciałbyś uruchomić. Wyobrażam sobie, że jest dużo puchu nawet w najściślejszym kodzie, jaki można napisać, i wydaje się to faktem. Znaczna część faktycznego kodu działającego na poziomie bitów i bajtów to logistyka - przenoszenie wartości z jednego miejsca do drugiego, przeskakiwanie do jednego lub drugiego miejsca, a czasami tylko dodawanie ...
Dennis

... odejmowanie lub inna funkcja biznesowa. Podobnie jak rozwijanie pętli może przyspieszyć niektóre pętle z powodu mniejszego narzutu przeznaczonego na inkrementację zmiennych, pisanie większych funkcji prawdopodobnie może zwiększyć prędkość, pod warunkiem, że skonfigurowano dla niego przypadek użycia. Moje obawy są tu bardziej ogólne, ponieważ widzę wiele porad dotyczących pisania mniejszych fragmentów kodu, zwiększa to okablowanie, jednocześnie poprawiając czytelność (miejmy nadzieję) i kosztem wzdęć na poziomie mikro.
Dennis

3
@Dennis - jedną rzeczą do rozważenia jest to, że w języku OO może istnieć BARDZO mała korelacja między tym, co pisze programista (a + b), a generowanym kodem (prosty dodatek dwóch rejestrów? Najpierw przenosi z pamięci? Casting, następnie wywołania funkcji do operatora obiektu +?). Tak więc „małe funkcje” na poziomie programisty mogą być czymś innym niż małym, gdy zostaną przekształcone w kod maszynowy.
Michael Kohne

2
@ Dennis Mogę powiedzieć, że pisanie kodu ASM (bezpośrednio, nieskompilowany) dla systemu Windows przebiega w stylu „mov, mov, invoke, mov, mov, invoke”. Ponieważ invoke jest makrem wykonującym wywołanie pchane / pops ... Od czasu do czasu wykonujesz wywołania funkcji we własnym kodzie, ale jest ono mniejsze niż wszystkie wywołania systemu operacyjnego.
Brian Knoblauch,

12

Jest to zmiana w porównaniu ze „starymi” lub „złymi” praktykami kodu, w których istnieją metody obejmujące 2500 wierszy, a duże klasy robią wszystko.

Nie sądzę, żeby ktokolwiek myślał, że to dobra praktyka. I wątpię, by ludzie, którzy to zrobili, zrobili to ze względu na wydajność.

Myślę, że słynny cytat Donalda Knutha jest tutaj bardzo istotny:

Powinniśmy zapomnieć o małej wydajności, powiedzmy około 97% czasu: przedwczesna optymalizacja jest źródłem wszelkiego zła.

Tak więc, w 97% twojego kodu, po prostu stosuj dobre praktyki, pisz małe metody (jak mała jest kwestia opinii, nie sądzę, że wszystkie metody powinny zawierać tylko kilka wierszy) itp. Dla pozostałych 3%, gdzie wydajność ma znaczenie, zmierzyć to. A jeśli pomiary pokazują, że posiadanie wielu małych metod znacznie spowalnia kod, powinieneś połączyć je w większe metody. Ale nie pisz nieusuwalnego kodu tylko dlatego, że może być szybszy.


11

Trzeba uważać, aby słuchać doświadczonych programistów, a także bieżącego myślenia. Ludzie, którzy od lat mają do czynienia z ogromnym oprogramowaniem, mogą coś wnieść.

Z mojego doświadczenia wynika, że ​​prowadzi to do spowolnienia i nie są one małe. Są to rzędy wielkości:

  • Założenie, że każda linia kodu zajmuje mniej więcej tyle samo czasu, co każda inna. Na przykład cout << endlkontra a = b + c. Pierwszy trwa tysiące razy dłużej niż drugi. Stackexchange ma wiele pytań o formie „Próbowałem różnych sposobów optymalizacji tego kodu, ale wydaje się, że to nie robi różnicy, dlaczego nie?” kiedy w środku znajduje się stare stare wywołanie funkcji.

  • Założenie, że po wywołaniu dowolnej funkcji lub metody jest oczywiście konieczne. Funkcje i metody są łatwe do wywołania, a wywołanie jest zwykle dość wydajne. Problem polega na tym, że są jak karty kredytowe. Kuszą cię, aby wydawać więcej, niż naprawdę chcesz, i ukrywają to, co wydałeś. Co więcej, duże oprogramowanie ma warstwy na warstwach abstrakcji, więc nawet jeśli na każdej warstwie jest tylko 15% marnotrawstwa, ponad 5 warstw łączy się ze współczynnikiem spowolnienia wynoszącym 2. Odpowiedzią na to nie jest usunięcie funkcjonalności lub napisanie większe funkcje, to zdyscyplinowanie się, aby mieć się na baczności przed tym problemem i być gotowym i móc go wykorzenić .

  • Galopująca ogólność. Wartość abstrakcji polega na tym, że pozwala ona robić więcej przy mniejszym kodzie - przynajmniej taka jest nadzieja. Ten pomysł może zostać doprowadzony do skrajności. Problem ze zbyt dużą ogólnością polega na tym, że każdy problem jest specyficzny, a kiedy rozwiązujesz go za pomocą ogólnych abstrakcji, niekoniecznie są one w stanie wykorzystać określone właściwości twojego problemu. Na przykład widziałem sytuację, w której wykorzystano fantazyjną klasę kolejki priorytetowej, która mogłaby być wydajna przy dużych rozmiarach, gdy długość nigdy nie przekraczała 3!

  • Galopująca struktura danych. OOP jest bardzo użytecznym paradygmatem, ale nie zachęca do minimalizacji struktury danych - raczej zachęca do próby ukrycia jego złożoności. Na przykład, istnieje koncepcja „powiadomienia”, w której jeśli baza danych A zostanie w jakiś sposób zmodyfikowana, A wyda powiadomienie, aby B i C również mogły się zmodyfikować, aby zachować spójność całego zestawu. Może to rozprzestrzeniać się na wielu warstwach i ogromnie zwiększyć koszt modyfikacji. Jest więc całkiem możliwe, że zmiana na A może wkrótce zostać cofniętalub zmienił się na kolejną modyfikację, co oznacza, że ​​wysiłek włożony w próbę utrzymania spójności zestawu musi być wykonany ponownie. Problem polega na tym, że istnieje prawdopodobieństwo błędów we wszystkich modułach obsługi powiadomień, cykliczności itp. O wiele lepiej jest starać się utrzymać znormalizowaną strukturę danych, aby wszelkie zmiany musiały zostać wprowadzone tylko w jednym miejscu. Jeśli nie da się uniknąć danych nienormalizowanych, lepiej jest mieć okresowe przejścia, aby naprawić niespójność, niż udawać, że można je zachować spójnie z krótką smyczą.

... kiedy wymyślę więcej, dodam to.


8

Krótka odpowiedź brzmi „tak”. Ogólnie kod będzie nieco wolniejszy.

Ale czasami odpowiednie refaktoryzowanie OO ujawni optymalizacje, które przyspieszą kod. Pracowałem nad jednym projektem, w którym stworzyliśmy złożony algorytm Java o wiele więcej OO, z odpowiednimi strukturami danych, modułami pobierającymi itp. Zamiast niechlujnie zagnieżdżonych tablic obiektów. Ale dzięki lepszej izolacji i ograniczeniu dostępu do struktur danych byliśmy w stanie zmienić gigantyczne tablice Doubles (z zerami dla pustych wyników) na bardziej zorganizowane tablice Doubles, z NaN dla pustych wyników. Dało to 10-krotny wzrost prędkości.

Dodatek: Ogólnie rzecz biorąc, mniejszy, lepiej ustrukturyzowany kod powinien być bardziej podatny na wielowątkowość, najlepszy sposób na uzyskanie dużych przyspieszeń.


1
Nie rozumiem, dlaczego przełączanie z Doubles na doubles wymaga lepszego kodu.
sick

Wow, opinia negatywna? Oryginalny kod klienta nie obsługiwał Double.NaNs, ale sprawdzał, czy wartości null reprezentują puste wartości. Po restrukturyzacji mogliśmy sobie z tym poradzić (poprzez enkapsulację) z modułami pobierającymi wyniki różnych algorytmów. Oczywiście, mogliśmy przepisać kod klienta, ale było to łatwiejsze.
user949300

Dla przypomnienia, nie byłem tym, który głosował za tą odpowiedzią.
sick

7

Programowanie polega między innymi na kompromisach . W związku z tym skłaniam się do odpowiedzi tak, może być wolniej. Ale pomyśl o tym, co otrzymasz w zamian. Uzyskiwanie czytelnego, wielokrotnego użytku i łatwo modyfikowalnego kodu łatwo przełamuje wszelkie możliwe wady.

Jak wspomniano @ user949300 , łatwiej jest dostrzec obszary, które można ulepszyć algorytmicznie lub architektonicznie przy takim podejściu; poprawa tych jest zwykle o wiele bardziej korzystna i skuteczna niż brak możliwości narzucenia funkcji lub wywołania funkcji (co, jak już sądzę, to tylko hałas).


Wyobrażam sobie również, że można dokonać dobrych optymalizacji ASM, zanim zostanie on faktycznie uruchomiony na sprzęcie.

Ilekroć coś takiego mi przychodzi do głowy, pamiętam, że dziesięciolecia spędzone przez najmądrzejszych ludzi pracujących na kompilatorach prawdopodobnie poprawiają narzędzia takie jak GCC znacznie lepiej niż ja w generowaniu kodu maszynowego. Jeśli nie pracujesz nad czymś związanym z mikrokontrolerami, sugeruję, abyś się tym nie martwił.

Zakładam, że dodanie coraz większej liczby funkcji oraz coraz większej liczby obiektów i klas w kodzie spowoduje, że coraz więcej parametrów będzie przekazywanych między mniejszymi częściami kodu.

Zakładając, że optymalizacja jest stratą czasu, potrzebujesz faktów na temat wydajności kodu. Znajdź, gdzie programujesz, przez większość czasu dzięki specjalistycznym narzędziom, zoptymalizuj to, iteruj.


Podsumowując wszystko; pozwól kompilatorowi wykonać swoją pracę, skupiając się na ważnych rzeczach, takich jak ulepszanie algorytmów i struktur danych. Wszystkie wzorce, o których wspomniałeś w swoim pytaniu, istnieją, aby ci w tym pomóc, użyj ich.

PS: Te dwa przemówienia Crockforda pojawiły się w mojej głowie i myślę, że są one trochę powiązane. Pierwszym z nich jest super krótka historia CS (którą zawsze dobrze jest znać w każdej nauce); a drugie dotyczy tego, dlaczego odrzucamy dobre rzeczy.


Najbardziej podoba mi się ta odpowiedź. Ludzie są okropni w odgadywaniu kompilatora i strzelaniu do wąskich gardeł. oczywiście należy pamiętać o złożoności wielkookresowej, ale duża 100-liniowa metoda spaghetti kontra wywołanie metody fabrycznej + niektóre wirtualne wysyłanie metod nigdy nie powinno być dyskusją. w tym przypadku wydajność wcale nie jest interesująca. duży plus za to, że „optymalizacja” bez konkretnych faktów i pomiarów jest stratą czasu.
sara,

4

Uważam, że zidentyfikowane trendy wskazują na prawdę o tworzeniu oprogramowania - czas programisty jest droższy niż czas procesora. Do tej pory komputery stały się tylko szybsze i tańsze, ale splątany bałagan aplikacji może zająć setki, jeśli nie tysiące roboczogodzin. Biorąc pod uwagę koszty wynagrodzeń, korzyści, powierzchni biurowej itp., Bardziej opłacalne jest posiadanie kodu, który może działać nieco wolniej, ale jego zmiana jest szybsza i bezpieczniejsza.


1
Zgadzam się, ale urządzenia mobilne stają się tak popularne, że uważam je za duży wyjątek. Mimo że moc obliczeniowa rośnie, nie można zbudować aplikacji na iPhone'a i oczekiwać, że będzie można dodać pamięć tak jak na serwerze internetowym.
JeffO

3
@JeffO: Nie zgadzam się - urządzenia mobilne z czterordzeniowymi procesorami są teraz normalne, wydajność (szczególnie, ponieważ wpływa na żywotność baterii), podczas gdy problem jest mniej ważny niż stabilność. Wolny telefon komórkowy lub tablet otrzymuje kiepskie recenzje, a niestabilny zostaje zabity. Aplikacje mobilne są bardzo dynamiczne, zmieniają się prawie codziennie - źródło musi nadążać.
mattnz

1
@JeffO: Jeśli chodzi o wartość, maszyna wirtualna używana w systemie Android jest dość zła. Według testów porównawczych może być o rząd wielkości wolniejszy niż natywny kod (podczas gdy najlepsze w rasie są zwykle nieco wolniejsze). Zgadnij co. Nikogo to nie obchodzi. Pisanie aplikacji jest szybkie, a CPU siedzi tam i kręci kciukami, czekając na dane użytkownika 90% czasu.
Jan Hudec

Wydajność to coś więcej niż surowe testy wydajności procesora. Mój telefon z Androidem działa dobrze, z wyjątkiem sytuacji, gdy AV skanuje aktualizację, a następnie wydaje się, że zawiesza się dłużej niż lubię, i jest to czterordzeniowy model RAM 2 Gb! Dziś przepustowość (sieciowa lub pamięciowa) jest prawdopodobnie głównym wąskim gardłem. Twój superszybki procesor prawdopodobnie kręci kciukami przez 99% czasu, a ogólne wrażenia są nadal słabe.
gbjbaanb

4

Cóż, ponad 20 lat temu, które, jak sądzę, nie nazywacie nowymi, a nie „starymi lub złymi”, regułą było utrzymywanie funkcji na tyle małych, aby zmieściły się na wydrukowanej stronie. Mieliśmy wtedy drukarki igłowe, więc liczba linii została nieco ustalona, ​​ogólnie tylko jedna lub dwie możliwości wyboru liczby linii na stronę ... zdecydowanie mniej niż 2500 linii.

Zadajesz wiele stron problemu, łatwość konserwacji, wydajność, testowalność, czytelność. Im bardziej skłaniasz się w stronę wydajności, tym mniejszy w utrzymaniu i czytelny będzie kod, więc musisz znaleźć swój poziom komfortu, który może i będzie różny dla każdego programisty.

Jeśli chodzi o kod generowany przez kompilator (kod maszynowy, jeśli chcesz), im większa funkcja, tym większa szansa na konieczność rozlewania wartości pośrednich w rejestrach na stos. Gdy używane są ramki stosu, zużycie stosu jest większe. Im mniejsze funkcje, tym większa szansa, że ​​dane pozostaną bardziej w rejestrach, a mniejsza zależność od stosu. Naturalnie potrzebne są mniejsze kawałki stosu na funkcję. Ramki stosu mają zalety i wady wydajności. Więcej mniejszych funkcji oznacza więcej ustawień funkcji i czyszczenia. Oczywiście zależy to również od tego, jak skompilujesz, jakie możliwości dajesz kompilatorowi. Możesz mieć 250 funkcji 10-liniowych zamiast jednej funkcji 2500-liniowej, jednej funkcji 2500-liniowej, którą kompilator uzyska, jeśli będzie mógł / zdecyduje się zoptymalizować całość. Ale jeśli weźmiesz te 250 10 funkcji liniowych i rozłożysz je na 2, 3, 4, 250 osobnych plików, skompiluj każdy plik osobno, to kompilator nie będzie w stanie zoptymalizować prawie tyle martwego kodu, ile mógłby mieć. Najważniejsze jest to, że są wady i zalety obu i nie jest możliwe narzucenie ogólnej zasady na ten lub inny sposób.

Rozsądne funkcje, coś, co dana osoba widzi na ekranie lub stronie (w rozsądnej czcionce), to coś, co można konsumować lepiej i rozumiejąc niż dość duży kod. Ale jeśli jest to tylko niewielka funkcja z wywołaniami wielu innych małych funkcji, które wywołują wiele innych małych funkcji, potrzebujesz kilku okien lub przeglądarek, aby zrozumieć ten kod, więc nie kupiłeś nic po stronie czytelności.

Uniksowym sposobem jest użycie mojego terminu, ładnie dopracowanych klocków Lego. Dlaczego miałbyś używać funkcji taśmy tak wiele lat po tym, jak przestaliśmy używać taśm? Ponieważ obiekt blob wykonał swoją pracę bardzo dobrze, a na odwrocie możemy zastąpić interfejs taśmy interfejsem pliku i skorzystać z zalet programu. Po co ponownie pisać oprogramowanie do nagrywania cdrom tylko dlatego, że scsi zniknęło, ponieważ dominujący interfejs zastąpiono ideem (aby później wrócić). Ponownie skorzystaj z dopracowanych podbloków i zastąp jeden koniec nowym blokiem interfejsu (zrozum też, że projektanci sprzętu po prostu dodali blok interfejsu do projektów sprzętowych, aby w niektórych przypadkach sprawić, że napęd SCSI ma interfejs SCI ide. SCS. , zbuduj polerowane klocki Lego o rozsądnych rozmiarach, każdy o ściśle określonym celu i dobrze określonych wejściach i wyjściach. możesz owinąć testy wokół tych klocków Lego, a następnie wziąć ten sam blok i zawinąć interfejs użytkownika i interfejsy systemu operacyjnego wokół tego samego bloku i bloku, teoretycznie dobrze przetestowane i dobrze zrozumiane, nie trzeba debugować, dodano tylko nowe nowe bloki na każdym końcu. tak długo, jak wszystkie bloki są dobrze zaprojektowane, a funkcjonalność dobrze zrozumiana, możesz zbudować bardzo wiele rzeczy przy minimalnym lub jakimkolwiek kleju. tak jak w przypadku niebieskich i czerwonych oraz czarno-żółtych klocków Lego o znanych rozmiarach i kształtach, możesz zrobić wiele rzeczy. tak długo, jak wszystkie bloki są dobrze zaprojektowane, a funkcjonalność dobrze zrozumiana, możesz zbudować bardzo wiele rzeczy przy minimalnym lub jakimkolwiek kleju. podobnie jak w przypadku niebieskich i czerwonych oraz czarno-żółtych klocków Lego o znanych rozmiarach i kształtach możesz zrobić wiele rzeczy. tak długo, jak wszystkie bloki są dobrze zaprojektowane, a funkcjonalność dobrze zrozumiana, możesz zbudować bardzo wiele rzeczy przy minimalnym lub jakimkolwiek kleju. tak jak w przypadku niebieskich i czerwonych oraz czarno-żółtych klocków Lego o znanych rozmiarach i kształtach, możesz zrobić wiele rzeczy.

Każda osoba jest inna, ich definicja jest dopracowana, dobrze zdefiniowana, przetestowana i czytelna. Na przykład profesor nie dyktuje zasad programowania, nie dlatego, że mogą one być złe dla ciebie jako profesjonalisty, ale w niektórych przypadkach ułatwiają czytanie i ocenianie kodu u asystenta profesora lub studenta ... Zawodowo równie prawdopodobne jest przekonanie się, że każda praca może mieć inne zasady z różnych powodów, zwykle jedna lub kilka osób u władzy ma swoje zdanie na temat czegoś, co jest dobre lub złe, a dzięki tej mocy mogą dyktować to, co robisz tam w ten sposób (albo rzucić palenie, zostać zwolnionym lub jakoś dojść do władzy). Reguły te są tak często oparte na opiniach, jak na faktach dotyczących czytelności, wydajności, testowalności, przenośności.


„Więc musisz znaleźć swój poziom komfortu, który może i będzie się różnić dla każdego programisty”. Myślę, że o poziomie optymalizacji powinny decydować wymagania dotyczące wydajności każdego fragmentu kodu, a nie to, co lubi każdy programista.
sick

Kompilator jest pewny, zakładając, że programista powiedział kompilatorowi, aby zoptymalizował kompilatory ogólnie domyślnie na nieoptymalizujący (w wierszu poleceń IDE może mieć inne domyślne, jeśli zostanie użyte). Ale programista może nie wiedzieć wystarczająco dużo, aby zoptymalizować rozmiar funkcji i przy tylu celach, gdzie staje się ona daremna? Wydajność strojenia ręcznego ma tendencję do wywierania negatywnego wpływu na czytelność, a w szczególności łatwość konserwacji, testowalność może iść w obie strony.
old_timer

2

Zależy jak inteligentny jest twój kompilator. Ogólnie rzecz biorąc, próba przechytrzenia optymalizatora jest złym pomysłem i może faktycznie pozbawić kompilatora możliwości optymalizacji. Na początek prawdopodobnie nie masz pojęcia, co potrafi, a większość tego, co robisz, wpływa na to, jak dobrze to robi.

Przedwczesna optymalizacja to pojęcie programistów próbujących to zrobić i kończących się trudnym do utrzymania kodem, który tak naprawdę nie był na krytycznej ścieżce tego, co próbowali zrobić. Na przykład często próbuję wycisnąć jak najwięcej procesora, gdy przez większość czasu twoja aplikacja jest faktycznie blokowana w oczekiwaniu na zdarzenia IO.

Najlepiej jest zakodować poprawność i użyć narzędzia do profilowania, aby znaleźć rzeczywiste wąskie gardła wydajności, i naprawić je, analizując, co znajduje się na ścieżce krytycznej i czy można to poprawić. Często kilka prostych poprawek przechodzi długą drogę.


0

[element czasu komputerowego]

Czy refaktoryzacja w kierunku luźniejszego sprzężenia i mniejszych funkcji wpływa na szybkość kodu?

Tak. Ale to do interpretatorów, kompilatorów i kompilatorów JIT należy usunięcie tego kodu „szew / okablowanie”, a niektórzy robią to lepiej niż inni, ale niektórzy nie.

Obawy związane z wieloma plikami zwiększają obciążenie we / wy, więc wpływa to również na szybkość (w czasie pracy na komputerze).

[element ludzkiej prędkości]

(I czy powinno mnie to obchodzić?)

nie, nie powinno cię to obchodzić. Komputery i obwody są obecnie bardzo szybkie, a inne czynniki, takie jak opóźnienie sieci, operacje we / wy bazy danych i buforowanie, przejmują kontrolę.

Zatem 2x - 4x spowolnienie w samym wykonywaniu kodu natywnego często zostanie zagłuszone przez te inne czynniki.

Jeśli chodzi o ładowanie wielu plików, często zajmują się tym różne rozwiązania buforowania. Załadowanie i scalenie za pierwszym razem może zająć więcej czasu, ale za każdym razem dla plików statycznych buforowanie działa tak, jakby ładowano pojedynczy plik. Buforowanie stanowi rozwiązanie problemu ładowania wielu plików.


0

ODPOWIEDŹ (na wypadek, gdybyś ją przegapił)

Tak, powinieneś się przejmować, ale dbaj o sposób pisania kodu, a nie o wydajność.

W skrócie

Nie dbaj o wydajność

W kontekście tego pytania zajmują się tym inteligentniejsze kompilatory i tłumacze

Dbaj o pisanie łatwego do utrzymania kodu

Kod, w którym koszty utrzymania są na poziomie rozsądnego zrozumienia dla ludzi. tzn. nie pisz 1000 mniejszych funkcji, co czyni kod niezrozumiałym, nawet jeśli rozumiesz każdą z nich, i nie pisz 1 boga funkcji obiektu, która jest zbyt duża, aby ją zrozumieć, ale pisz 10 dobrze zaprojektowanych funkcji, które mają sens dla człowieka i są łatwy w utrzymaniu.

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.