Dlaczego warto stosować podejście OO zamiast gigantycznego „przełącznika”?

59

Pracuję w sklepie .Net, C # i mam współpracownika, który nalega, abyśmy używali gigantycznych instrukcji Switch w naszym kodzie z dużą ilością „przypadków”, a nie zorientowanych obiektowo. Jego argument konsekwentnie powraca do faktu, że instrukcja Switch kompiluje się do „tabeli skoków procesora” i dlatego jest najszybszą opcją (chociaż w innych sprawach nasz zespół mówi, że nie zależy nam na szybkości).

Szczerze mówiąc, nie mam argumentów przeciwko temu ... ponieważ nie wiem o czym, do cholery, on mówi.
Czy on ma rację?
Czy on tylko mówi swój tyłek?
Próbuję się tutaj nauczyć.

James P. Wright
źródło
7
Możesz sprawdzić, czy ma rację, używając czegoś takiego jak .NET Reflector, aby spojrzeć na kod asemblera i znaleźć „tabelę skoków procesora”.
FrustratedWithFormsDesigner
5
„Instrukcja przełączania kompiluje się do„ tabeli skoków procesora ”. Podobnie dzieje się w przypadku metody najgorszego przypadku ze wszystkimi funkcjami czysto wirtualnymi. Żadnych funkcji wirtualnych nie łączy się bezpośrednio. Czy zrzuciłeś kod do porównania?
S.Lott
64
Kod powinien być napisany dla LUDZI, a nie dla maszyn, inaczej zrobilibyśmy wszystko podczas montażu.
wałek klonowy
8
Jeśli on jest tak wielkim makaronem, zacytuj go: „Powinniśmy zapomnieć o małej wydajności, powiedzmy w około 97% przypadków: przedwczesna optymalizacja jest źródłem wszelkiego zła”.
DaveE
12
Konserwowalność. Czy są jeszcze jakieś pytania z odpowiedziami na jedno słowo?
Matt Ellen

Odpowiedzi:

48

Prawdopodobnie jest starym hakerem C i tak, mówi ze swojego tyłka. .Net to nie C ++; kompilator .Net jest coraz lepszy, a najmądrzejsze hacki przynoszą efekt przeciwny do zamierzonego, jeśli nie dzisiaj, to w następnej wersji. Preferowane są małe funkcje, ponieważ .NET JIT-y każda funkcja raz przed jej użyciem. Tak więc, jeśli niektóre przypadki nigdy nie zostaną trafione podczas cyklu życia programu, więc nie powstają żadne koszty przy ich kompilacji. W każdym razie, jeśli prędkość nie jest problemem, nie powinno być optymalizacji. Najpierw napisz dla programisty, a następnie dla kompilatora. Twój współpracownik nie będzie łatwo przekonać, więc udowodnię empirycznie, że lepiej zorganizowany kod jest w rzeczywistości szybszy. Wybrałbym jeden z jego najgorszych przykładów, przepisałem je w lepszy sposób, a następnie upewniłem się, że twój kod jest szybszy. Wybierz, jeśli musisz. Następnie uruchom go kilka milionów razy, profiluj i pokaż mu.

EDYTOWAĆ

Bill Wagner napisał:

Punkt 11: Zrozumienie przyciągania małych funkcji (skuteczne wydanie C # Second Edition) Pamiętaj, że tłumaczenie kodu C # na kod wykonywalny maszynowo jest procesem dwuetapowym. Kompilator C # generuje IL, która jest dostarczana w zestawach. Kompilator JIT generuje kod maszynowy dla każdej metody (lub grupy metod, gdy jest włączone wstawianie), w razie potrzeby. Małe funkcje znacznie ułatwiają kompilatorowi JIT amortyzację tego kosztu. Małe funkcje są również bardziej skłonne do kandydowania do wbudowania. To nie tylko drobiazg: Prostszy przepływ kontroli ma takie samo znaczenie. Mniej gałęzi kontrolnych w funkcjach ułatwia kompilatorowi JIT rejestrowanie zmiennych. Pisanie wyraźniejszego kodu jest nie tylko dobrą praktyką; w ten sposób tworzysz bardziej wydajny kod w czasie wykonywania.

EDYCJA 2:

Więc ... najwyraźniej instrukcja switch jest szybsza i lepsza niż kilka instrukcji if / else, ponieważ jedno porównanie jest logarytmiczne, a drugie liniowe. http://sequence-points.blogspot.com/2007/10/why-is-switch-statement-faster-than-if.html

Cóż, moim ulubionym podejściem do zamiany ogromnej instrukcji switch jest słownik (a czasem nawet tablica, jeśli włączam wyliczenia lub małe liczby całkowite), który odwzorowuje wartości na funkcje wywoływane w odpowiedzi na nie. Takie postępowanie zmusza do usunięcia wielu nieprzyjemnych wspólnych stanów spaghetti, ale to dobra rzecz. Oświadczenie o dużej zmianie zwykle jest koszmarem konserwacyjnym. Więc ... w przypadku tablic i słowników wyszukiwanie zajmie cały czas i nie będzie marnowania dodatkowej pamięci.

Nadal nie jestem przekonany, że instrukcja zamiany jest lepsza.

Praca
źródło
47
Nie martw się o udowodnienie tego szybciej. To przedwczesna optymalizacja. Milisekunda, którą możesz zaoszczędzić, jest niczym w porównaniu z indeksem, o którym zapomniałeś dodać do bazy danych, który kosztuje 200 ms. Walczysz złą bitwą.
Rein Henrichs,
27
@Jac co, jeśli on ma rację? Nie chodzi o to, że się myli, chodzi o to, że ma rację i to nie ma znaczenia .
Rein Henrichs
2
Nawet jeśli miał rację w około 100% przypadków, wciąż marnuje nasz czas.
Jeremy
6
Chcę oderwać oczy, próbując odczytać stronę, do której linkujesz.
AttackingHobo
3
Co jest z nienawiścią do C ++? Kompilatory C ++ również stają się coraz lepsze, a duże przełączniki są tak samo złe w C ++ jak w C # iz tego samego powodu. Jeśli jesteś otoczony przez byłych programistów C ++, którzy dają ci żal, to nie dlatego, że są programistami C ++, to dlatego, że są złymi programistami.
Sebastian Redl
39

O ile twój kolega nie jest w stanie udowodnić, że ta zmiana zapewnia rzeczywistą wymierną korzyść w skali całej aplikacji, jest gorsza od twojego podejścia (tj. Polimorfizmu), który faktycznie zapewnia taką korzyść: łatwość utrzymania.

Mikrooptymalizacji należy dokonać dopiero po zlikwidowaniu wąskich gardeł. Przedwczesna optymalizacja jest źródłem wszelkiego zła .

Prędkość jest kwantyfikowalna. Niewiele przydatnych informacji w „podejściu A jest szybsze niż podejście B”. Pytanie brzmi: „O ile szybciej? ”.

back2dos
źródło
2
Absolutnie prawdziwe. Nigdy nie twierdz, że coś jest szybsze, zawsze mierz. I mierz tylko wtedy, gdy ta część aplikacji stanowi wąskie gardło wydajności.
Kilian Foth
6
-1 dla „Przedwczesna optymalizacja jest źródłem wszelkiego zła”. Pokaż cały cytat, a nie tylko jedną część, która podważa opinię Knutha.
alternatywnie
2
@mathepic: Celowo nie przedstawiłem tego jako cytatu. To zdanie jest moją osobistą opinią, choć oczywiście nie moim dziełem. Chociaż można zauważyć, że faceci z c2 wydają się uważać właśnie tę część za podstawową mądrość.
back2dos
8
@alternative Pełny cytat Knutha „Nie ulega wątpliwości, że graal wydajności prowadzi do nadużyć. Programiści marnują mnóstwo czasu na myślenie lub martwienie się o szybkość niekrytycznych części swoich programów, a te próby wydajności faktycznie mają silny negatywny wpływ przy rozważaniu debugowania i konserwacji. Powinniśmy zapomnieć o niewielkiej wydajności, powiedzmy w około 97% przypadków: przedwczesna optymalizacja jest źródłem wszelkiego zła ”. Doskonale opisuje współpracownika OP. Back2dos IMHO dobrze podsumował cytat z „przedwczesna optymalizacja jest źródłem wszelkiego zła”
MarkJ
2
@ MarkJ 97% czasu
alternatywnie
27

Kogo to obchodzi, jeśli jest szybciej?

O ile nie piszesz oprogramowania działającego w czasie rzeczywistym, jest mało prawdopodobne, aby niewielka ilość przyspieszenia, jaką możesz uzyskać, robiąc coś w całkowicie szalony sposób, znacznie zmieni Twój klient. Nie chciałbym nawet walczyć z tym na froncie prędkości, ten facet najwyraźniej nie będzie słuchał żadnych argumentów na ten temat.

Utrzymanie jest jednak celem gry, a instrukcja zmiany giganta nie jest nawet w niewielkim stopniu możliwa do utrzymania, w jaki sposób wytłumaczysz różne ścieżki w kodzie nowym osobom? Dokumentacja musi być tak długa jak sam kod!

Ponadto masz całkowitą niezdolność do skutecznego testowania jednostkowego (zbyt wiele możliwych ścieżek, nie wspominając o prawdopodobnym braku interfejsów itp.), Co sprawia, że ​​Twój kod jest jeszcze trudniejszy w utrzymaniu.

[Po stronie zainteresowań: JITter działa lepiej na mniejszych metodach, więc instrukcje gigantycznych przełączników (i ich z natury duże metody) zaszkodzą twojej prędkości w dużych złożeniach, IIRC.]

Ed James
źródło
1
+ wielki przykład przedwczesnej optymalizacji.
ShaneC
Zdecydowanie to.
DeadMG
+1 za „oświadczenie gigantycznej zamiany nie jest nawet w niewielkim stopniu możliwe do utrzymania”
Korey Hinton
2
Oświadczenie gigantycznej zmiany jest znacznie łatwiejsze do zrozumienia dla nowego faceta: wszystkie możliwe zachowania są zebrane właśnie na ładnej, uporządkowanej liście. Wywołania pośrednie są niezwykle trudne do naśladowania, w najgorszym przypadku (wskaźnik funkcji) należy przeszukać całą bazę kodu pod kątem funkcji o poprawnym podpisie, a wywołania wirtualne są tylko trochę lepsze (poszukiwanie funkcji o właściwej nazwie i podpisie oraz związane z dziedziczeniem). Ale łatwość konserwacji nie polega na byciu tylko do odczytu.
Ben Voigt,
14

Odsuń się od instrukcji zamiany ...

Tego rodzaju instrukcje przełączania należy unikać jak zarazy, ponieważ naruszają one zasadę otwartej zamkniętej . Zmusza zespół do wprowadzania zmian w istniejącym kodzie, gdy trzeba dodać nową funkcjonalność, a nie tylko do dodawania nowego kodu.

Dakotah North
źródło
11
To przychodzi z zastrzeżeniem. Istnieją operacje (funkcje / metody) i typy. Kiedy dodajesz nową operację, musisz tylko zmienić kod w jednym miejscu dla instrukcji switch (dodaj jedną nową funkcję z instrukcją switch), ale musisz dodać tę metodę do wszystkich klas w przypadku OO (narusza otwarte / zasada zamknięta). Jeśli dodajesz nowe typy, musisz dotknąć każdej instrukcji switch, ale w przypadku OO wystarczy dodać jeszcze jedną klasę. Dlatego, aby podjąć świadomą decyzję, musisz wiedzieć, czy dodasz więcej operacji do istniejących typów, czy dodasz więcej typów.
Scott Whitlock,
3
Jeśli potrzebujesz dodać więcej operacji do istniejących typów w paradygmacie OO bez naruszania OCP, to uważam, że po to jest wzorzec odwiedzającego.
Scott Whitlock,
3
@Martin - zadzwoń, jeśli chcesz, ale jest to dobrze znany kompromis. Odsyłam cię do Clean Code autorstwa RC Martina. Powraca do swojego artykułu na temat OCP, wyjaśniając to, co nakreśliłem powyżej. Nie można jednocześnie projektować pod kątem wszystkich przyszłych wymagań. Musisz dokonać wyboru między tym, czy bardziej prawdopodobne jest dodanie większej liczby operacji, czy więcej typów. OO sprzyja dodawaniu typów. Możesz użyć OO, aby dodać więcej operacji, jeśli modelujesz operacje jako klasy, ale wydaje się, że to wchodzi w schemat odwiedzających, który ma swoje własne problemy (zwłaszcza ogólne).
Scott Whitlock,
8
@Martin: Czy napisałeś kiedyś parser? Dość często występują duże skrzynki przełączników, które włączają następny token w buforze lookahead. Państwo mogłoby zastąpić te przełączniki z funkcją wirtualnego wzywa do kolejnego powodu, ale to byłby koszmar maintainence. Jest to rzadkie, ale czasami skrzynka przełączników jest właściwie lepszym wyborem, ponieważ utrzymuje kod, który powinien być czytany / modyfikowany razem w bliskiej odległości.
nikie
1
@Martin: Użyłeś słów takich jak „nigdy”, „kiedykolwiek” i „Poppycock”, więc zakładałem, że mówisz o wszystkich przypadkach bez wyjątków, a nie tylko o najczęstszych przypadkach. (A BTW: ludzie nadal piszą ręcznie parsery. Na przykład parser CPython jest nadal pisany ręcznie, IIRC.)
nikie
8

Przeżyłem koszmar znany jako masywna skończona maszyna stanów manipulowana przez masowe instrukcje przełączników. Co gorsza, w moim przypadku FSM obejmował trzy biblioteki DLL C ++ i było całkiem jasne, że kod został napisany przez kogoś zorientowanego w C.

Dane, o które musisz dbać, to:

  • Szybkość dokonywania zmian
  • Szybkość znalezienia problemu, kiedy to się stanie

Zadanie polegające na dodaniu nowej funkcji do tego zestawu bibliotek DLL było w stanie przekonać kierownictwo, że przepisanie 3 bibliotek DLL zajmie mi tyle samo czasu, co właściwie zorientowanej obiektowo biblioteki DLL, tak jak dla mnie łatanie małp i jury przypisze rozwiązanie do tego, co już tam było. Przepisanie odniosło ogromny sukces, ponieważ nie tylko wspierało nową funkcjonalność, ale było znacznie łatwiejsze do rozszerzenia. W rzeczywistości zadanie, które normalnie zająłoby tydzień, aby upewnić się, że niczego nie zepsułeś, skończyłoby się kilka godzin.

A co z czasami wykonania? Nie było zwiększenia ani zmniejszenia prędkości. Aby być uczciwym, nasza wydajność została dławiona przez sterowniki systemowe, więc jeśli rozwiązanie obiektowe faktycznie byłoby wolniejsze, nie znalibyśmy go.

Co jest złego w masywnych instrukcjach przełączania dla języka OO?

  • Przepływ sterowania programem jest odbierany od obiektu, do którego należy, i umieszczany na zewnątrz obiektu
  • Wiele punktów kontroli zewnętrznej przekłada się na wiele miejsc, które należy przejrzeć
  • Nie jest jasne, gdzie jest przechowywany stan, szczególnie jeśli przełącznik znajduje się w pętli
  • Najszybsze porównanie to w ogóle brak porównania (można uniknąć konieczności wielu porównań z dobrym projektowaniem obiektowym)
  • Bardziej efektywne jest iterowanie po obiektach i zawsze wywoływanie tej samej metody na wszystkich obiektach, niż zmiana kodu na podstawie typu obiektu lub wyliczenia, które koduje ten typ.
Berin Loritsch
źródło
8

Nie kupuję argumentu dotyczącego wydajności; chodzi przede wszystkim o łatwość utrzymania kodu.

ALE: czasami gigantyczna instrukcja switch jest łatwiejsza do utrzymania (mniej kodu) niż kilka małych klas przesłaniających funkcje wirtualne abstrakcyjnej klasy bazowej. Na przykład, jeśli miałbyś zaimplementować emulator procesora, nie zaimplementowałbyś funkcjonalności każdej instrukcji w osobnej klasie - po prostu umieściłbyś ją w gigantycznym przełączniku na opcode, prawdopodobnie wywołując funkcje pomocnicze w celu uzyskania bardziej złożonych instrukcji.

Ogólna zasada: jeśli przełącznik jest w jakiś sposób wykonywany na TYPIE, prawdopodobnie powinieneś użyć funkcji dziedziczenia i funkcji wirtualnych. Jeśli przełączenie jest wykonywane dla WARTOŚCI o stałym typie (np. Kod operacji instrukcji, jak wyżej), można pozostawić go takim, jakim jest.

zvrba
źródło
5

Nie możesz mnie przekonać, że:

void action1()
{}

void action2()
{}

void action3()
{}

void action4()
{}

void doAction(int action)
{
    switch(action)
    {
        case 1: action1();break;
        case 2: action2();break;
        case 3: action3();break;
        case 4: action4();break;
    }
}

Jest znacznie szybszy niż:

struct IAction
{
    virtual ~IAction() {}
    virtual void action() = 0;
}

struct Action1: public IAction
{
    virtual void action()    { }
}

struct Action2: public IAction
{
    virtual void action()    { }
}

struct Action3: public IAction
{
    virtual void action()    { }
}

struct Action4: public IAction
{
    virtual void action()    { }
}

void doAction(IAction& actionObject)
{
    actionObject.action();
}

Dodatkowo wersja OO jest po prostu łatwiejsza w utrzymaniu.

Martin York
źródło
8
W przypadku niektórych rzeczy i mniejszych akcji wersja OO jest znacznie lepsza. Musi mieć jakąś fabrykę, aby przekształcić pewną wartość w stworzenie IAction. W wielu przypadkach o wiele bardziej czytelne jest po prostu włączenie tej wartości.
Zan Lynx,
@Zan Lynx: Twój argument jest zbyt ogólny. Utworzenie obiektu IAction jest tak samo trudne jak odzyskanie liczby całkowitej akcji nie trudniej i łatwiej. Abyśmy mogli odbyć prawdziwą rozmowę, nie będąc drogą do ogólnych. Rozważ kalkulator. Jaka jest tutaj różnica w złożoności? Odpowiedź wynosi zero. Ponieważ wszystkie działania zostały wcześniej utworzone. Otrzymujesz dane wejściowe od użytkownika i jest to już akcja.
Martin York
3
@Martin: Zakładasz aplikację kalkulatora GUI. Weźmy zamiast tego aplikację kalkulatora klawiatury napisaną dla C ++ w systemie wbudowanym. Teraz masz liczbę całkowitą kodu skanowania z rejestru sprzętowego. Co jest teraz mniej skomplikowane?
Zan Lynx,
2
@Martin: Nie widzisz, jak liczba całkowita -> tablica odnośników -> tworzenie nowego obiektu -> wywołanie funkcji wirtualnej jest bardziej skomplikowane niż liczba całkowita -> przełącznik -> funkcja? Jak tego nie widzisz?
Zan Lynx
2
@Martin: Może będę. W międzyczasie wyjaśnij, w jaki sposób uzyskać obiekt IAction do wywołania akcji () z liczby całkowitej bez tabeli odnośników.
Zan Lynx
4

Ma rację, że wynikowy kod maszynowy będzie prawdopodobnie bardziej wydajny. Kompilator essential przekształca instrukcję switch w zestaw testów i rozgałęzień, które będą względnie nielicznymi instrukcjami. Istnieje duża szansa, że ​​kod wynikający z bardziej abstrakcyjnych podejść będzie wymagał więcej instrukcji.

JEDNAK : Prawie na pewno jest tak, że twoja aplikacja nie musi martwić się o tego rodzaju mikrooptymalizację, w przeciwnym razie nie będziesz używać .net. W przypadku bardzo ograniczonych aplikacji wbudowanych lub pracochłonnych procesorów należy zawsze pozwolić kompilatorowi na optymalizację. Skoncentruj się na pisaniu czystego, łatwego do utrzymania kodu. To prawie zawsze ma o wiele większą wartość niż kilka dziesiątych nanosekundowych czasów wykonania.

Luke Graham
źródło
3

Jednym z głównych powodów używania klas zamiast instrukcji switch jest to, że instrukcje switch zwykle prowadzą do jednego ogromnego pliku, który ma dużo logiki. Jest to zarówno koszmar utrzymania, jak i problem z zarządzaniem źródłami, ponieważ musisz sprawdzić i edytować ten ogromny plik zamiast różnych mniejszych plików klasy

Homde
źródło
3

instrukcja switch w kodzie OOP jest silnym wskaźnikiem brakujących klas

wypróbuj obie strony i uruchom kilka prostych testów prędkości; są szanse, że różnica nie jest znacząca. Jeśli tak, a kod ma krytyczne znaczenie dla czasu, należy zachować instrukcję switch

Steven A. Lowe
źródło
3

Zwykle nienawidzę słowa „przedwczesna optymalizacja”, ale to cuchnie. Warto zauważyć, że Knuth użył tego słynnego cytatu w kontekście naciskania na użycie gotoinstrukcji w celu przyspieszenia kodu w krytycznych obszarach. To jest klucz: ścieżki krytyczne .

Sugerował użycie go gotodo przyspieszenia kodu, ale ostrzega przed programistami, którzy chcieliby robić tego rodzaju rzeczy w oparciu o przeczucia i przesądy dla kodu, który nawet nie jest krytyczny.

Faworyzowanie switchinstrukcji w jak największym stopniu jednolicie w całej bazie kodu (niezależnie od tego, czy obsługiwane jest duże obciążenie) jest klasycznym przykładem tego, co Knuth nazywa programistą „rozsądnym i głupim”, który spędza cały dzień walcząc o utrzymanie swojego „zoptymalizowanego” "kod, który zamienił się w koszmar debugowania w wyniku próby zaoszczędzenia groszy na kilogramach. Taki kod rzadko jest łatwy do utrzymania, a tym bardziej wydajny.

Czy on ma rację?

Ma rację z bardzo podstawowej perspektywy wydajności. Według mojej wiedzy żaden kompilator nie jest w stanie zoptymalizować kodu polimorficznego obejmującego obiekty i dynamiczne wysyłanie lepiej niż instrukcja switch. Nigdy nie skończysz z LUT lub tabelą skoków do kodu wstawionego z kodu polimorficznego, ponieważ taki kod zwykle służy jako bariera optymalizatora dla kompilatora (nie będzie wiedział, którą funkcję wywołać do czasu, w którym dynamiczna wysyłka występuje).

Bardziej przydatne jest nie myśleć o tym koszcie w kategoriach tabel skoków, ale bardziej w kategoriach bariery optymalizacji. W przypadku polimorfizmu wywołanie Base.method()nie pozwala kompilatorowi wiedzieć, która funkcja zostanie ostatecznie wywołana, jeśli methodjest wirtualna, nie jest zapieczętowana i może zostać zastąpiona. Ponieważ nie wie, która funkcja zostanie wywołana z wyprzedzeniem, nie może zoptymalizować wywołania funkcji i wykorzystać więcej informacji przy podejmowaniu decyzji optymalizacyjnych, ponieważ tak naprawdę nie wie, która funkcja zostanie wywołana czas kompilacji kodu.

Optymalizatory są w najlepszym momencie, gdy mogą zajrzeć do wywołania funkcji i dokonać optymalizacji, które albo całkowicie spłaszczą rozmówcę i odbiorcę, albo przynajmniej zoptymalizują rozmówcę, aby najskuteczniej współpracować z odbiorcą. Nie mogą tego zrobić, jeśli nie wiedzą, która funkcja zostanie wcześniej wywołana.

Czy on tylko mówi swój tyłek?

Wykorzystanie tego kosztu, który często wynosi grosze, w celu uzasadnienia przekształcenia go w jednolity standard kodowania jest ogólnie bardzo głupie, szczególnie w miejscach, które wymagają rozszerzenia. To jest najważniejsza rzecz, na którą należy zwrócić uwagę w przypadku oryginalnych przedwczesnych optymalizatorów: chcą przekształcić niewielkie problemy z wydajnością w standardy kodowania stosowane jednolicie w całej bazie kodu, bez względu na łatwość konserwacji.

Obrażam trochę cytat „stary haker C” użyty w przyjętej odpowiedzi, ponieważ jestem jednym z nich. Nie każdy, kto programuje od dziesięcioleci, poczynając od bardzo ograniczonego sprzętu, zmienił się w przedwczesny optymalizator. Ale ja też z nimi spotkałem. Ale te typy nigdy nie mierzą rzeczy takich jak nieprzewidywalność gałęzi lub bufory pamięci podręcznej, myślą, że wiedzą lepiej, i opierają swoje pojęcia nieefektywności w złożonej bazie kodu produkcyjnego opartej na przesądach, które nie są prawdziwe dzisiaj, a czasem nigdy nie są prawdziwe. Ludzie, którzy naprawdę pracowali w obszarach krytycznych pod względem wydajności, często rozumieją, że skuteczna optymalizacja jest skutecznym ustalaniem priorytetów, a próba uogólnienia standardu kodowania obniżającego łatwość konserwacji, aby zaoszczędzić grosze, jest bardzo nieefektywna.

Grosze są ważne, gdy masz tanią funkcję, która nie wykonuje tyle pracy, co nazywa się miliard razy w bardzo ciasnej, krytycznej dla wydajności pętli. W takim przypadku ostatecznie oszczędzamy 10 milionów dolarów. Nie warto golić groszy, gdy masz funkcję wywoływaną dwa razy, dla której samo ciało kosztuje tysiące dolarów. Nie jest rozsądnie spędzać czas na targowaniu się o grosze podczas zakupu samochodu. Warto targować się o grosze, jeśli kupujesz milion puszek sody od producenta. Kluczem do skutecznej optymalizacji jest zrozumienie tych kosztów we właściwym kontekście. Ktoś, kto próbuje zaoszczędzić grosze przy każdym zakupie i sugeruje, że wszyscy próbują targować się o grosze bez względu na to, co kupują, nie jest wykwalifikowanym optymistą.


źródło
2

Wygląda na to, że twój współpracownik bardzo martwi się wydajnością. Może się zdarzyć, że w niektórych przypadkach duża struktura obudowy / przełącznika będzie działać szybciej, ale mam nadzieję, że przeprowadzilibyście eksperyment, wykonując testy czasowe dla wersji OO i wersji przełącznika / skrzynki. Zgaduję, że wersja OO ma mniej kodu i jest łatwiejsza do naśladowania, zrozumienia i utrzymania. Najpierw argumentowałbym za wersją OO (ponieważ konserwacja / czytelność powinna być początkowo ważniejsza) i rozważam wersję przełącznika / skrzynki tylko wtedy, gdy wersja OO ma poważne problemy z wydajnością i można wykazać, że przełącznik / skrzynka spowoduje znaczna poprawa.

FrustratedWithFormsDesigner
źródło
1
Wraz z testami czasowymi zrzut kodu może pomóc pokazać, jak działa wywoływanie metody C ++ (i C #).
S.Lott,
2

Jedną z zalet konserwacji polimorfizmu, o której nikt nie wspomniał, jest to, że będziesz w stanie ładniej ustrukturyzować swój kod za pomocą dziedziczenia, jeśli zawsze włączasz tę samą listę spraw, ale czasami kilka spraw jest obsługiwanych w ten sam sposób, a czasami nie są

Na przykład. jeśli przełączasz się między Dog, Cata Elephantczasami Dogi Catmasz ten sam przypadek, możesz sprawić, by oba dziedziczyły po klasie abstrakcyjnej DomesticAnimali umieściły te funkcje w klasie abstrakcyjnej.

Byłem też zaskoczony, że kilka osób użyło parsera jako przykładu, w którym nie użyłbyś polimorfizmu. W przypadku parsera przypominającego drzewo jest to zdecydowanie niewłaściwe podejście, ale jeśli masz coś takiego jak asembler, gdzie każda linia jest nieco niezależna, i zaczynasz od kodu, który wskazuje, jak należy interpretować resztę linii, całkowicie użyłbym polimorfizmu i fabrykę. Każda klasa może implementować funkcje takie jak ExtractConstantslub ExtractSymbols. Zastosowałem to podejście w przypadku zabawkowego tłumacza BASIC.

jwg
źródło
Przełącznik może również dziedziczyć zachowania po domyślnym przypadku. „... rozszerza BaseOperationVisitor” staje się „domyślne: BaseOperation (węzeł)”
Samuel Danielson
0

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

Donald Knuth

Thorsten Müller
źródło
0

Nawet jeśli nie było to złe z punktu widzenia łatwości konserwacji, nie sądzę, że będzie to lepsze z punktu widzenia wydajności. Wirtualne wywołanie funkcji jest po prostu jedną dodatkową pośrednią (tak samo jak w najlepszym przypadku dla instrukcji switch), więc nawet w C ++ wydajność powinna być w przybliżeniu równa. W języku C #, gdzie wszystkie wywołania funkcji są wirtualne, instrukcja przełączania powinna być gorsza, ponieważ w obu wersjach występuje taki sam narzut wirtualny wywołania funkcji.

Dirk Holsopple
źródło
1
Brakuje „nie”? W języku C # nie wszystkie wywołania funkcji są wirtualne. C # to nie Java.
Ben Voigt,
0

Twój kolega nie mówi z tyłu, jeśli chodzi o komentarz dotyczący skoków. Jednak użycie tego do usprawiedliwienia pisania złego kodu jest błędem.

Kompilator C # konwertuje instrukcje przełączające z zaledwie kilkoma przypadkami na serię instrukcji if / else, więc nie jest szybszy niż użycie instrukcji if / else. Kompilator konwertuje większe instrukcje przełączników na Słownik (tabelę skoków, o której mówi twój kolega). Więcej informacji można znaleźć w tej odpowiedzi na pytanie przepełnienia stosu na ten temat .

Instrukcja dużego przełącznika jest trudna do odczytania i utrzymania. Słownik „przypadków” i funkcji jest znacznie łatwiejszy do odczytania. Ponieważ właśnie w ten sposób zamienia się przełącznik, ty i twój kolega powinniście korzystać bezpośrednio ze słowników.

David Arno
źródło
0

Niekoniecznie mówi ze swojego tyłka. Przynajmniej w switchinstrukcjach C i C ++ można zoptymalizować, aby przeskakiwać tabele, podczas gdy nigdy nie widziałem, aby stało się to z dynamiczną wysyłką w funkcji, która ma dostęp tylko do wskaźnika podstawowego. Przynajmniej ten ostatni wymaga znacznie inteligentniejszego optymalizatora, który patrzy na znacznie więcej otaczającego kodu, aby dowiedzieć się dokładnie, jaki podtyp jest używany z wirtualnego wywołania funkcji za pośrednictwem podstawowego wskaźnika / odwołania.

Ponadto dynamiczna wysyłka często służy jako „bariera optymalizacyjna”, co oznacza, że ​​kompilator często nie będzie w stanie wstawić kodu i optymalnie przydzielić rejestrów, aby zminimalizować rozlewanie się stosu i wszystkie inne wymyślne rzeczy, ponieważ nie może ustalić, co funkcja wirtualna zostanie wywołana przez wskaźnik podstawowy, aby wstawić ją i wykonać całą magię optymalizacji. Nie jestem pewien, czy chcesz, aby optymalizator był tak inteligentny i próbował zoptymalizować pośrednie wywołania funkcji, ponieważ może to potencjalnie prowadzić do generowania wielu gałęzi kodu osobno w dół na stosie wywołań (funkcja, która foo->f()wywołałaby aby wygenerować zupełnie inny kod maszynowy niż ten, który wywołujebar->f() przez wskaźnik bazowy, a funkcja wywołująca tę funkcję musiałaby następnie wygenerować dwie lub więcej wersji kodu, i tak dalej - ilość generowanego kodu maszynowego byłaby wybuchowa - być może nie tak źle ze śladowym JIT, który generuje kod „w locie” podczas śledzenia ścieżek wykonywania na gorąco).

Jednakże, ponieważ wiele odpowiedzi powtórzyło się, to zły powód, aby faworyzować mnóstwo switchinstrukcji, nawet jeśli są one przekazywane szybciej niż jakikolwiek margines. Poza tym, jeśli chodzi o mikro-wydajności, rzeczy takie jak rozgałęzianie i wstawianie mają zwykle dość niski priorytet w porównaniu do rzeczy takich jak wzorce dostępu do pamięci.

To powiedziawszy, wskoczyłem tutaj z nietypową odpowiedzią. Chcę uzasadnić możliwość zachowania switchinstrukcji nad rozwiązaniem polimorficznym wtedy i tylko wtedy, gdy wiadomo na pewno, że będzie tylko jedno miejsce, które musi wykonać switch.

Doskonałym przykładem jest centralny moduł obsługi zdarzeń. W takim przypadku na ogół nie ma wielu miejsc obsługujących zdarzenia, tylko jeden (dlaczego jest to „centralny”). W takich przypadkach nie korzysta się z rozszerzalności zapewnianej przez rozwiązanie polimorficzne. Rozwiązanie polimorficzne jest korzystne, gdy istnieje wiele miejsc, które wykonałyby analogiczne switchstwierdzenie. Jeśli wiesz na pewno, że będzie tylko jeden, switchinstrukcja z 15 przypadkami może być o wiele prostsza niż zaprojektowanie klasy bazowej odziedziczonej przez 15 podtypów z zastąpionymi funkcjami i fabryki do ich tworzenia, tylko wtedy, gdy zostaną użyte w jednej funkcji w całym systemie. W takich przypadkach dodanie nowego podtypu jest o wiele bardziej nużące niż dodanie caseinstrukcji do jednej funkcji. Jeśli cokolwiek, argumentowałbym za łatwością utrzymania, a nie za wydajnością,switch oświadczenia w tym szczególnym przypadku, w którym nie korzysta się z rozszerzalności.


źródło