Czy orientacja obiektowa naprawdę wpływa na wydajność algorytmu?

14

Orientacja obiektowa bardzo mi pomogła w implementacji wielu algorytmów. Jednak języki zorientowane obiektowo czasami prowadzą cię w „bezpośrednim” podejściu i wątpię, czy takie podejście jest zawsze dobre.

OO jest naprawdę pomocne w szybkim i łatwym kodowaniu algorytmów. Ale czy ten OOP może być niekorzystny dla oprogramowania opartego na wydajności, tj. Jak szybko program działa?

Na przykład przechowywanie węzłów grafowych w strukturze danych wydaje się „proste”, ale jeśli obiekty Węzłów zawierają wiele atrybutów i metod, czy może to prowadzić do powolnego algorytmu?

Innymi słowy, czy wiele odniesień między wieloma różnymi obiektami lub przy użyciu wielu metod z wielu klas może spowodować „ciężką” implementację?

Florents Tselai
źródło
1
Dziwne pytanie. Rozumiem, w jaki sposób OOP pomaga na poziomie architektury. Ale poziom implementacji algorytmów jest zwykle zbudowany na abstrakcjach, które są bardzo obce wszystkim, co oznacza OOP. Tak więc są szanse, że wydajność nie jest największym problemem dla implementacji algorytmów OOP. Jeśli chodzi o wydajność, w przypadku OOP największe wąskie gardło jest zwykle związane z połączeniami wirtualnymi.
SK-logic,
@ SK-logic> orientacja obiektu ma tendencję do manipulowania everithing za pomocą wskaźnika, co implikuje ważniejsze obciążenie po stronie alokacji pamięci, a nie zlokalizowane dane zwykle nie znajdują się w pamięci podręcznej procesora, a na koniec sugerują wiele pośrednich rozgałęzienie (funkcje wirtualne), które jest zabójcze dla potoku procesora. OO jest dobrą rzeczą, ale z pewnością może kosztować wydajność w niektórych przypadkach.
deadalnix,
Jeśli węzły na wykresie mają sto atrybutów, będziesz potrzebować miejsca do ich przechowywania, niezależnie od paradygmatu zastosowanego do faktycznej implementacji, i nie widzę, w jaki sposób jakikolwiek pojedynczy paradygmat ma w tym przewagę. @deadalnix: Może stałe faktory mogą być gorsze z powodu utrudnienia niektórych optymalizacji. Zauważ jednak, że mówię mocniej , nie niemożliwie - na przykład PyPy może rozpakowywać obiekty w ciasnych pętlach, a maszyny JVM od zawsze wprowadzają wywołania funkcji wirtualnych.
Python jest dobry do tworzenia algorytmów prototypowych, a jednak często nie potrzebujesz klasy, gdy implementujesz w nim typowy algorytm.
Job
1
+1 Dla dotycząca orientacji obiektu z algorytmów, coś, co jest pomijane w tych dniach, zarówno w branży oprogramowania, a akademia ...
umlcat

Odpowiedzi:

16

Orientacja obiektowa może uniemożliwić pewne optymalizacje algorytmiczne z powodu enkapsulacji. Dwa algorytmy mogą ze sobą szczególnie dobrze współpracować, ale jeśli są ukryte za interfejsami OO, utracono możliwość wykorzystania ich synergii.

Spójrz na biblioteki numeryczne. Wiele z nich (nie tylko te napisane w latach 60. i 70.) nie są OOP. Jest po temu powód - algorytmy numeryczne działają lepiej jako zbiór odsprzężonych modulesniż jako hierarchie OO z interfejsami i enkapsulacją.

quant_dev
źródło
2
Głównym tego powodem jest to, że tylko C ++ wymyślił użycie szablonów wyrażeń, aby wersja OO była równie wydajna.
DeadMG,
4
Spójrz na nowoczesne biblioteki C ++ (STL, Boost) - one wcale nie są OOP. I to nie tylko ze względu na wydajność. Algorytmy zwykle nie mogą być dobrze reprezentowane w stylu OOP. Rzeczy takie jak programowanie ogólne są znacznie lepiej dostosowane do algorytmów niskiego poziomu.
SK-logic,
3
Co-co-co? Chyba pochodzę z innej planety niż quant_dev i SK-logic. Nie, inny wszechświat. Z różnymi prawami fizyki i wszystkim.
Mike Nakis,
5
@MikeNakis: różnica w punkcie widzenia polega na (1), czy pewien fragment kodu obliczeniowego może w ogóle skorzystać na OOP z czytelności dla człowieka (które przepisy numeryczne nie są); (2) czy projekt klasy OOP jest zgodny z optymalną strukturą danych i algorytmem (patrz moja odpowiedź); oraz (3) to, czy każda warstwa pośrednia zapewnia wystarczającą „wartość” (pod względem wykonanej pracy na wywołanie funkcji lub przejrzystość pojęciowa na warstwę) uzasadnia narzut (z powodu pośrednictwa, wywołania funkcji, warstw lub kopiowania danych). (4) Wreszcie, wyrafinowanie kompilatora / JIT / optymalizatora jest czynnikiem ograniczającym.
rwong,
2
@MikeNakis, co masz na myśli? Czy uważasz, że STL jest biblioteką OOP? Programowanie ogólne i tak nie idzie dobrze z OOP. I nie trzeba wspominać, że OOP to zbyt wąskie ramy, odpowiednie tylko do kilku praktycznych zadań, obce dla wszystkiego innego.
SK-logic,
9

Co decyduje o wydajności?

Podstawy: struktury danych, algorytmy, architektura komputera, sprzęt. Plus koszty ogólne.

Program OOP można zaprojektować tak, aby dokładnie dopasowywał się do wyboru struktur danych i algorytmów uznanych za optymalne przez teorię CS. Będzie miał tę samą charakterystykę wydajności, co program optymalny, plus pewne koszty ogólne. Narzut można zwykle zminimalizować.

Jednak program, który początkowo został zaprojektowany tylko z myślą o OOP, bez względu na podstawy, może początkowo nie być optymalny. Podoptymalność jest czasami usuwana przez refaktoryzację; czasem tak nie jest - wymaga pełnego przepisania.

Zastrzeżenie: czy wydajność ma znaczenie w oprogramowaniu biznesowym?

Tak, ale czas na wprowadzenie na rynek (TTM) jest ważniejszy, o rząd wielkości. Oprogramowanie biznesowe kładzie nacisk na dostosowanie kodu do złożonych reguł biznesowych. Pomiary wydajności należy wykonywać w całym cyklu rozwojowym. (Patrz rozdział: co oznacza optymalna wydajność? ) Należy wprowadzać tylko ulepszenia dostępne na rynku i stopniowo wprowadzać je w późniejszych wersjach.

Co oznacza optymalna wydajność?

Ogólnie rzecz biorąc, problem z wydajnością oprogramowania polega na tym, że: aby udowodnić, że „istnieje szybsza wersja”, ta szybsza wersja musi najpierw powstać (tj. Nie może istnieć żaden dowód inny niż ona sama).

Czasami ta szybsza wersja jest najpierw widziana w innym języku lub paradygmacie. Powinno to być traktowane jako wskazówka do poprawy, a nie ocena niższości niektórych innych języków lub paradygmatów.

Dlaczego robimy OOP, jeśli może to utrudnić nasze poszukiwania optymalnej wydajności?

OOP wprowadza koszty ogólne (w przestrzeni i wykonywaniu), w zamian za poprawę „wykonalności”, a tym samym wartości biznesowej kodu. Zmniejsza to koszty dalszego rozwoju i optymalizacji. Zobacz @MikeNakis .

Które części OOP mogą zachęcać do początkowo nieoptymalnego projektu?

Części OOP, które (i) zachęcają do prostoty / intuicyjności, (ii) stosowanie kolokwialnych metod projektowania zamiast podstaw, (iii) zniechęcają do wielu dostosowanych implementacji tego samego celu.

  • POCAŁUNEK
  • YAGNI
  • SUCHY
  • Projektowanie obiektów (np. Z kartami CRC) bez równego myślenia o podstawach)

Rygorystyczne stosowanie niektórych wytycznych dotyczących OOP (enkapsulacja, przekazywanie wiadomości, robienie jednej rzeczy dobrze) rzeczywiście na początku spowoduje spowolnienie kodu. Pomiary wydajności pomogą zdiagnozować te problemy. Dopóki struktura danych i algorytm są zgodne z optymalnym projektem przewidywanym przez teorię, narzut można zazwyczaj zminimalizować.

Jakie są najczęstsze ograniczenia kosztów ogólnych OOP?

Jak już wspomniano, przy użyciu struktur danych optymalnych do projektu.

Niektóre języki obsługują wstawianie kodu, które może przywrócić wydajność środowiska wykonawczego.

Jak moglibyśmy przyjąć OOP bez poświęcania wydajności?

Naucz się i stosuj zarówno OOP, jak i podstawy.

Prawdą jest, że ścisłe przestrzeganie OOP może uniemożliwić Ci napisanie szybszej wersji. Czasami szybszą wersję można napisać od zera. Dlatego pomaga napisać wiele wersji kodu przy użyciu różnych algorytmów i paradygmatów (OOP, ogólny, funkcjonalny, matematyczny, spaghetti), a następnie użyć narzędzi optymalizacyjnych, aby każda wersja zbliżyła się do obserwowanej maksymalnej wydajności.

Czy istnieją typy kodu, które nie skorzystają z OOP?

(Rozszerzono z dyskusji między [@quant_dev], [@ SK-logic] i [@MikeNakis])

  1. Przepisy numeryczne, które wywodzą się z matematyki.
    • Same równania matematyczne i przekształcenia można rozumieć jako obiekty.
    • Potrzebne są bardzo zaawansowane techniki transformacji kodu, aby wygenerować wydajny kod wykonywalny. Naiwna („biała tablica”) implementacja będzie miała fatalną wydajność.
    • Jednak dzisiejsze kompilatory głównego nurtu nie są w stanie tego zrobić.
    • Specjalistyczne oprogramowanie (MATLAB i Mathematica itp.) Ma zarówno JIT, jak i symboliczne solwery zdolne do generowania wydajnego kodu dla niektórych podproblemów. Te wyspecjalizowane solwery można postrzegać jako kompilatory specjalnego przeznaczenia (mediatory między kodem czytelnym dla człowieka a kodem wykonywalnym maszynowo), które same skorzystają z projektu OOP.
    • Każdy podproblem wymaga własnego „kompilatora” i „transformacji kodu”. Dlatego jest to bardzo aktywna otwarta przestrzeń badawcza, której nowe wyniki pojawiają się każdego roku.
    • Ponieważ badania trwają długo, twórcy oprogramowania muszą przeprowadzić optymalizację na papierze i przepisać zoptymalizowany kod do oprogramowania. Przepisany kod może rzeczywiście być niezrozumiały.
  2. Kod bardzo niskiego poziomu.
      *
rwong
źródło
8

Tak naprawdę nie chodzi o orientację obiektu, jak o pojemniki. Jeśli użyjesz podwójnie połączonej listy do przechowywania pikseli w odtwarzaczu wideo, to ucierpi.

Jeśli jednak użyjesz odpowiedniego kontenera, nie ma powodu, aby std :: vector działał wolniej niż tablica, a ponieważ masz już wszystkie wspólne algorytmy dla niego napisane - przez ekspertów - jest to prawdopodobnie szybsze niż kod z macierzy domowej.

Martin Beckett
źródło
1
Ponieważ kompilatory są nieoptymalne (lub reguły języka programowania zabraniają korzystania z pewnych założeń lub optymalizacji), rzeczywiście istnieje narzut, którego nie można usunąć. Ponadto niektóre optymalizacje, np. Wektoryzacja, mają wymagania dotyczące organizacji danych (np. Struktura tablic zamiast tablic struktur), które OOP może ulepszyć lub utrudnić. (Niedawno pracowałem nad zadaniem optymalizacji std :: vector.)
rwong,
5

OOP jest oczywiście dobrym pomysłem i jak każdy dobry pomysł może być nadużywany. Z mojego doświadczenia wynika, że ​​jest on zbyt często wykorzystywany. Niska wydajność i niski wynik konserwacji.

Nie ma to nic wspólnego z narzutem wywoływania funkcji wirtualnych, a niewiele z tego, co robi optymalizator / jitter.

Ma to wszystko wspólnego ze strukturami danych, które, choć mają najlepszą wydajność big-O, mają bardzo złe stałe czynniki. Odbywa się to przy założeniu, że jeśli w aplikacji występuje jakiś problem ograniczający wydajność, jest to gdzie indziej.

Jednym ze sposobów, w jaki się to manifestuje, jest liczba wykonywanych na sekundę nowych operacji , które, jak się zakłada, mają wydajność O (1), ale mogą wykonywać setki do tysięcy instrukcji (w tym pasujący czas usuwania lub GC). Można to złagodzić, zapisując używane obiekty, ale dzięki temu kod jest mniej „czysty”.

Innym sposobem, w jaki się manifestuje, jest sposób zachęcania ludzi do pisania funkcji właściwości, procedur obsługi powiadomień, wywołań funkcji klasy podstawowej, wszelkiego rodzaju podziemnych wywołań funkcji, które istnieją w celu zachowania spójności. Jeśli chodzi o zachowanie spójności, odnoszą one ograniczone sukcesy, ale odnoszą ogromne sukcesy w marnowaniu cykli. Programiści rozumieją pojęcie znormalizowanych danych, ale zwykle stosują je tylko do projektowania baz danych. Nie stosują go do projektowania struktury danych, przynajmniej częściowo dlatego, że OOP mówi im, że nie muszą. Tak proste, jak ustawienie Zmodyfikowanego bitu w obiekcie, może spowodować tsunami aktualizacji przebiegających przez strukturę danych, ponieważ żadna klasa warta swojego kodu nie przyjmuje wywołania Zmodyfikowanego i tylko je przechowuje .

Być może wydajność danej aplikacji jest zgodna z opisem.

Z drugiej strony, jeśli występuje problem z wydajnością, oto przykład, jak go dostroić. To proces wieloetapowy. Na każdym etapie niektóre szczególne działania stanowią znaczną część czasu i można je zastąpić czymś szybszym. (Nie powiedziałem „wąskiego gardła”. Nie są to rzeczy, które profilerzy są dobrzy w znajdowaniu.) Ten proces często wymaga, w celu uzyskania przyspieszenia, hurtowej wymiany struktury danych. Często ta struktura danych istnieje tylko dlatego, że jest to zalecana praktyka OOP.

Mike Dunlavey
źródło
3

Teoretycznie może to prowadzić do spowolnienia, ale nawet wtedy nie byłby to powolny algorytm, byłby powolną implementacją. W praktyce orientacja obiektowa pozwoli Ci wypróbować różne scenariusze „co, jeśli” (lub ponownie przyjrzeć się algorytmowi w przyszłości), a tym samym zapewnić ulepszenia algorytmu , których nigdy nie miałbyś nadziei osiągnąć, gdybyś napisał go spaghetti w pierwszej kolejności miejsce, ponieważ zadanie byłoby zniechęcające. (Zasadniczo musiałbyś przepisać całość.)

Na przykład, dzieląc różne zadania i byty na czyste obiekty, możesz łatwo wejść później i, powiedzmy, osadzić buforowanie między niektórymi obiektami (dla nich przezroczystymi), co może dać tysiąc- krotnie poprawa.

Zasadniczo rodzaje ulepszeń, które można osiągnąć za pomocą języka niskiego poziomu (lub sprytnych sztuczek z językiem wysokiego poziomu), zapewniają stałe (liniowe) ulepszenia czasu, których nie uwzględnia się w zapisie wielkiej litery. Dzięki ulepszeniom algorytmicznym możesz być w stanie osiągnąć nieliniowe ulepszenia. To bezcenne.

Mike Nakis
źródło
1
+1: różnica między spaghetti a kodem obiektowym (lub kodem napisanym w dobrze zdefiniowanym paradygmacie) jest taka: każda wersja poprawnego przepisania kodu wprowadza nowe zrozumienie problemu. Każda wersja przepisanego spaghetti nigdy nie daje wglądu.
rwong
@rwong nie można lepiej wytłumaczyć ;-)
umlcat,
3

Ale czy ten OOP może być niekorzystny dla oprogramowania opartego na wydajności, tj. Jak szybko program działa?

Często tak !!! ALE...

Innymi słowy, czy wiele odniesień między wieloma różnymi obiektami lub przy użyciu wielu metod z wielu klas może spowodować „ciężką” implementację?

Niekoniecznie. To zależy od języka / kompilatora. Na przykład optymalizujący kompilator C ++, pod warunkiem, że nie używasz funkcji wirtualnych, często obniża narzut obiektu do zera. Możesz wykonywać takie czynności, jak napisanie opakowania nad intnim lub inteligentnego wskaźnika o ograniczonym zasięgu nad zwykłym starym wskaźnikiem, który działa tak samo szybko, jak bezpośrednie używanie tych zwykłych starych typów danych.

W innych językach, takich jak Java, istnieje pewien narzut na obiekt (często dość mały w wielu przypadkach, ale astronomiczny w niektórych rzadkich przypadkach z naprawdę małymi obiektami). Na przykład,Integer jest znacznie mniej wydajny niż int(zajmuje 16 bajtów zamiast 4 w wersji 64-bitowej). Ale to nie są tylko rażące marnotrawstwo czy coś takiego. W zamian Java oferuje takie elementy, jak jednolita refleksja nad każdym typem zdefiniowanym przez użytkownika, a także możliwość zastąpienia dowolnej funkcji nieoznaczonej jako final.

Przyjmijmy jednak najlepszy scenariusz: optymalizujący kompilator C ++, który może zoptymalizować interfejsy obiektów aż do zera . Nawet wtedy OOP często obniża wydajność i uniemożliwia jej osiągnięcie szczytu. To może brzmieć jak kompletny paradoks: jak to możliwe? Problem polega na:

Projektowanie i enkapsulacja interfejsu

Problem polega na tym, że nawet jeśli kompilator może zmiażdżyć strukturę obiektu do zera narzutu (co jest co najmniej bardzo często prawdziwe w przypadku optymalizacji kompilatorów C ++), hermetyzacja i projekt interfejsu (i akumulacja zależności) drobnoziarnistych obiektów często zapobiegają najbardziej optymalne reprezentacje danych dla obiektów, które mają być agregowane przez masy (co często ma miejsce w przypadku oprogramowania o krytycznym znaczeniu).

Weź ten przykład:

class Particle
{
public:
    ...

private:
    double birth;                // 8 bytes
    float x;                     // 4 bytes
    float y;                     // 4 bytes
    float z;                     // 4 bytes
    /*padding*/                  // 4 bytes of padding
};
Particle particles[1000000];     // 1mil particles (~24 megs)

Powiedzmy, że naszym wzorcem dostępu do pamięci jest po prostu sekwencyjne przechodzenie przez te cząsteczki i kilkakrotne przesuwanie ich wokół każdej klatki, odbijanie ich od rogów ekranu, a następnie renderowanie wyniku.

Już teraz widzimy rażące 4 bajtowe wypełnienie nad głową wymagane do birthprawidłowego wyrównania elementu, gdy cząstki są agregowane w sposób ciągły. Już około 16,7% pamięci jest marnowane z martwą przestrzenią używaną do wyrównywania.

Może się to wydawać sporne, ponieważ w dzisiejszych czasach mamy gigabajty pamięci DRAM. Jednak nawet najbardziej bestialskie maszyny, jakie mamy obecnie, często mają zaledwie 8 megabajtów, jeśli chodzi o najwolniejszy i największy obszar pamięci podręcznej procesora (L3). Im mniej możemy się tam zmieścić, tym więcej płacimy za powtarzający się dostęp do pamięci DRAM i tym wolniej. Nagle marnowanie 16,7% pamięci przestało być banalną sprawą.

Możemy łatwo wyeliminować ten narzut bez żadnego wpływu na wyrównanie pola:

class Particle
{
public:
    ...

private:
    float x;                     // 4 bytes
    float y;                     // 4 bytes
    float z;                     // 4 bytes
};
Particle particles[1000000];     // 1mil particles (~12 megs)
double particle_birth[1000000];  // 1mil particle births (~8 bytes)

Teraz zmniejszyliśmy pamięć z 24 MB do 20 MB. Dzięki sekwencyjnemu wzorowi dostępu maszyna będzie teraz zużywać te dane nieco szybciej.

Ale spójrzmy na to birthpole nieco bliżej. Powiedzmy, że rejestruje czas początkowy, w którym cząstka się rodzi (tworzy). Wyobraź sobie, że pole jest dostępne tylko wtedy, gdy cząstka jest tworzona po raz pierwszy, i co 10 sekund, aby zobaczyć, czy cząstka powinna umrzeć i odrodzić się w losowym miejscu na ekranie. W takim przypadku birthjest zimne pole. To nie jest dostępne w naszych pętlach krytycznych dla wydajności.

W rezultacie rzeczywiste dane krytyczne pod względem wydajności nie wynoszą 20 megabajtów, ale w rzeczywistości ciągły blok 12 megabajtów. Rzeczywista gorąca pamięć, do której często uzyskujemy dostęp, skurczyła się do połowy swojej wielkości! Spodziewaj się znacznego przyspieszenia w stosunku do naszego oryginalnego 24-megabajtowego rozwiązania (nie trzeba tego mierzyć - robiłeś już takie rzeczy tysiąc razy, ale w razie wątpliwości możesz się swobodnie).

Zauważ jednak, co tutaj zrobiliśmy. Całkowicie złamaliśmy enkapsulację tego obiektu cząstek. Jego stan jest teraz podzielony między Particleprywatne pola typu i oddzielną, równoległą tablicę. I właśnie tam przeszkadza ziarniste projektowanie obiektowe.

Nie możemy wyrazić optymalnej reprezentacji danych, gdy ograniczamy się do projektu interfejsu pojedynczego, bardzo ziarnistego obiektu, takiego jak pojedyncza cząstka, pojedynczy piksel, nawet pojedynczy 4-komponentowy wektor, być może nawet pojedynczy obiekt „stworzenia” w grze , itp. Prędkość geparda zostanie zmarnowana, jeśli stoi on na malutkiej wyspie, która ma 2 metry kwadratowe, i to właśnie robi bardzo szczegółowa, obiektowa konstrukcja pod względem wydajności. Ogranicza reprezentację danych do natury nieoptymalnej.

Aby pójść dalej, powiedzmy, że ponieważ po prostu poruszamy cząstkami, możemy faktycznie uzyskać dostęp do ich pól x / y / z w trzech oddzielnych pętlach. W takim przypadku możemy skorzystać z funkcji SIMD w stylu SoA dzięki rejestrom AVX, które mogą wektoryzować 8 operacji SPFP równolegle. Ale aby to zrobić, musimy teraz użyć tej reprezentacji:

float particle_x[1000000];       // 1mil particle X positions (~4 megs)
float particle_y[1000000];       // 1mil particle Y positions (~4 megs)
float particle_z[1000000];       // 1mil particle Z positions (~4 megs)
double particle_birth[1000000];  // 1mil particle births (~8 bytes)

Teraz latamy z symulacją cząstek, ale spójrz, co się stało z naszym projektem cząstek. Został całkowicie zburzony, a teraz patrzymy na 4 równoległe tablice i nie ma obiektu, aby je agregować. Nasz obiektowy Particleprojekt przeszedł w sayonara.

Zdarzyło mi się to wiele razy, pracując w obszarach krytycznych pod względem wydajności, w których użytkownicy wymagają szybkości, a tylko poprawność jest tym, czego wymagają więcej. Te małe projekty obiektowe musiały zostać zburzone, a kaskadowe pękanie często wymagało zastosowania strategii powolnej amortyzacji w celu szybszego projektowania.

Rozwiązanie

Powyższy scenariusz przedstawia jedynie problem z granulowanymi projektami obiektowymi. W takich przypadkach często zdarza się, że musimy zburzyć strukturę, aby wyrazić bardziej wydajne reprezentacje w wyniku powtórzeń SoA, podziału pola gorącego / zimnego, zmniejszenia dopełniania dla sekwencyjnych wzorców dostępu (wypełnienie jest czasem pomocne dla wydajności z dostępem losowym) wzorce w przypadkach AoS, ale prawie zawsze przeszkoda dla wzorców sekwencyjnego dostępu) itp.

Możemy jednak przyjąć ostateczną reprezentację, na której się zdecydowaliśmy, i nadal modelować interfejs obiektowy:

// Represents a collection of particles.
class ParticleSystem
{
public:
    ...

private:
    double particle_birth[1000000];  // 1mil particle births (~8 bytes)
    float particle_x[1000000];       // 1mil particle X positions (~4 megs)
    float particle_y[1000000];       // 1mil particle Y positions (~4 megs)
    float particle_z[1000000];       // 1mil particle Z positions (~4 megs)
};

Teraz jesteśmy dobrzy. Możemy uzyskać wszystkie przedmioty, które lubimy. Gepard ma do pokonania cały kraj tak szybko, jak to możliwe. Nasze projekty interfejsów nie uwięziły nas już w wąskim rogu.

ParticleSystempotencjalnie może być abstrakcyjny i korzystać z funkcji wirtualnych. Teraz jest dyskusja, płacimy za koszty ogólne na poziomie gromadzenia cząstek zamiast na poziomie cząstek . Narzut stanowi 1/1 000 000 tego, co byłoby inaczej, gdybyśmy modelowali obiekty na poziomie pojedynczych cząstek.

Jest to rozwiązanie w obszarach krytycznych pod względem wydajności, które radzą sobie z dużym obciążeniem, i dla wszystkich rodzajów języków programowania (ta technika przynosi korzyści w C, C ++, Python, Java, JavaScript, Lua, Swift itp.). Nie można go łatwo nazwać „przedwczesną optymalizacją”, ponieważ dotyczy to projektowania interfejsu i architektury . Nie możemy napisać bazy kodu modelującej pojedynczą cząsteczkę jako obiekt z mnóstwem zależności klienta od plikuParticle'sinterfejs publiczny, a następnie zmień nasze zdanie później. Zrobiłem to bardzo często, gdy jestem powołany do optymalizacji starszych kodowych baz danych, co może zająć miesiące przepisania dziesiątek tysięcy wierszy kodu ostrożnie, aby użyć obszerniejszego projektu. To idealnie wpływa na to, jak projektujemy rzeczy z góry, pod warunkiem, że możemy przewidzieć duże obciążenie.

Powtarzam tę odpowiedź w takiej czy innej formie w wielu pytaniach dotyczących wydajności, a zwłaszcza tych, które dotyczą projektowania obiektowego. Projektowanie obiektowe może być nadal zgodne z najwyższymi wymaganiami dotyczącymi wydajności, ale musimy nieco zmienić sposób myślenia. Musimy dać temu gepardowi trochę miejsca, by biegł tak szybko, jak to możliwe, a to często jest niemożliwe, jeśli projektujemy małe obiekty, które ledwo przechowują jakikolwiek stan.


źródło
Fantastyczny. Właśnie tego szukałem, jeśli chodzi o połączenie OOP z wysokim zapotrzebowaniem na wydajność. Naprawdę nie rozumiem, dlaczego nie jest to więcej głosowane.
pbx
2

Tak, obiektowy sposób myślenia może zdecydowanie być neutralny lub negatywny, jeśli chodzi o programowanie o wysokiej wydajności, zarówno na poziomie algorytmicznym, jak i implementacyjnym. Jeśli OOP zastąpi analizę algorytmiczną, może to doprowadzić do przedwczesnej implementacji, a na najniższym poziomie abstrakcje OOP muszą zostać odłożone na bok.

Problem wynika z nacisku OOP na myślenie o poszczególnych instancjach. Myślę, że można śmiało powiedzieć, że sposób myślenia o algorytmie przez OOP polega na myśleniu o konkretnym zestawie wartości i wdrażaniu go w ten sposób. Jeśli jest to ścieżka najwyższego poziomu, jest mało prawdopodobne, aby zrealizować transformację lub restrukturyzację, które doprowadziłyby do dużych zysków.

Na poziomie algorytmicznym często myśli się o większym obrazie i ograniczeniach lub relacjach między wartościami, które prowadzą do dużych zysków O. Przykładem może być to, że w sposobie myślenia OOP nie ma nic, co prowadziłoby do przekształcenia „sumowania ciągłego zakresu liczb całkowitych” z pętli na(max + min) * n/2

Na poziomie implementacji, chociaż komputery są „wystarczająco szybkie” dla większości algorytmów na poziomie aplikacji, w kodzie o niskim poziomie wydajności krytycznym bardzo martwi się o lokalizację. Ponownie nacisk OOP na myślenie o konkretnej instancji, a wartości jednego przejścia przez pętlę mogą być ujemne. W kodzie o wysokiej wydajności zamiast pisać prostą pętlę, możesz chcieć częściowo rozwinąć pętlę, zgrupować kilka instrukcji ładowania na górze, a następnie przekształcić je w grupę, a następnie zapisać w grupie. Przez cały czas należy zwracać uwagę na obliczenia pośrednie, a także ogromnie pamięć podręczną i dostęp do pamięci; problemy, w których abstrakcje OOP nie są już ważne. A jeśli będzie to przestrzegane, może wprowadzać w błąd: na tym poziomie musisz wiedzieć i myśleć o reprezentacjach na poziomie maszyny.

Kiedy patrzysz na coś w rodzaju Primitive Performance Intela, masz dosłownie tysiące implementacji szybkiej transformacji Fouriera, z których każda dostosowana jest do pracy lepiej dla określonego rozmiaru danych i architektury maszyny. (Fascynujące jest to, że większość tych implementacji jest generowana maszynowo: Markus Püschel Automatic Performance Programming )

Oczywiście, jak mówi większość odpowiedzi, w przypadku większości prac programistycznych i większości algorytmów OOP nie ma znaczenia dla wydajności. Dopóki nie „przedwcześnie pesymizujesz” i dodajesz wiele nielokalnych połączeń, thiswskaźnika nie ma ani tutaj, ani tam.

Larry OBrien
źródło
0

Jest to powiązane i często pomijane.

To nie jest łatwa odpowiedź, zależy od tego, co chcesz zrobić.

Niektóre algorytmy są bardziej wydajne przy użyciu programowania o prostej strukturze, podczas gdy inne są lepsze przy użyciu orientacji obiektowej.

Przed orientacją obiektową wiele szkół uczy (redaguje) projektowanie algorytmów za pomocą programowania strukturalnego. Obecnie wiele szkół uczy programowania obiektowego, ignorując projektowanie i działanie algorytmów.

Oczywiście tam, gdzie szkoły uczą programowania strukturalnego, w ogóle nie dbają o algorytmy.

umlcat
źródło
0

Wydajność sprowadza się w końcu do cykli procesora i pamięci. Ale procentowa różnica między narzutem przesyłania komunikatów OOP i enkapsulacji a bardziej otwartym semantycznym programowaniem może, ale nie musi być wystarczająco znaczącym procentem, aby wprowadzić zauważalną różnicę w wydajności aplikacji. Jeśli aplikacja jest związana z brakiem dysku lub pamięci podręcznej danych, wszelkie koszty OOP mogą zostać całkowicie utracone w hałasie.

Jednak w wewnętrznych pętlach przetwarzania sygnału i obrazu w czasie rzeczywistym oraz w innych aplikacjach związanych z obliczeniami numerycznymi różnica może być znaczącym procentem cykli procesora i pamięci, co może spowodować, że wykonanie dowolnego obciążenia OOP będzie znacznie bardziej kosztowne.

Semantyka konkretnego języka OOP może, ale nie musi, dawać kompilatorowi wystarczającą szansę na optymalizację tych cykli lub obwody predykcyjne gałęzi procesora, aby zawsze poprawnie zgadywały i obejmowały te cykle wstępnym pobieraniem i potokowaniem.

hotpaw2
źródło
0

Dobra konstrukcja obiektowa pomogła mi znacznie przyspieszyć aplikację. Musiałem wygenerować złożoną grafikę w sposób algorytmiczny. Zrobiłem to za pomocą automatyzacji Microsoft Visio. Pracowałem, ale byłem niesamowicie wolny. Na szczęście wprowadziłem dodatkowy poziom abstrakcji między logiką (algorytmem) a programem Visio. Mój komponent Visio ujawnił swoją funkcjonalność poprzez interfejs. To pozwoliło mi łatwo zastąpić wolny komponent innym tworzącym się plikiem SVG, który był co najmniej 50 razy szybszy! Bez czystego podejścia obiektowego kody algorytmu i kontroli wizyjnej byłyby zaplątane w sposób, który zamieniłby zmianę w koszmar.

Olivier Jacot-Descombes
źródło
miałeś na myśli OO Design stosowany w języku proceduralnym, czy język programowania OO Design & OO?
umlcat,
Mówię o aplikacji C #. Zarówno projekt, jak i język są OO. Gdy OO-iness języka wprowadzi kilka niewielkich wyników (wirtualne wywołania metod, tworzenie obiektów, dostęp do elementów przez interfejs), projekt OO pomógł mi w stworzeniu znacznie szybszej aplikacji. Chcę powiedzieć: zapomnij o hitach wydajności z powodu OO (język i design). Jeśli nie wykonasz ciężkich obliczeń z milionami iteracji, OO cię nie skrzywdzi. Tam, gdzie zwykle tracisz dużo czasu, są wejścia / wyjścia.
Olivier Jacot-Descombes,