„Przedwczesna optymalizacja jest źródłem wszelkiego zła”
Myślę, że wszyscy możemy się zgodzić. I bardzo się staram tego uniknąć.
Ale ostatnio zastanawiałem się nad praktyką przekazywania parametrów przez const Reference zamiast Value . Zostałem nauczony / nauczony, że nietrywialne argumenty funkcji (tj. Większość typów nieprymitywnych) powinny być przekazywane przez odniesienie do stałej - wiele książek, które przeczytałem, poleca to jako „najlepszą praktykę”.
Nadal nie mogę przestać się zastanawiać: nowoczesne kompilatory i nowe funkcje językowe potrafią zdziałać cuda, więc wiedza, której się nauczyłem, może być bardzo przestarzała i nigdy tak naprawdę nie zadałam sobie trudu, aby profilować, jeśli istnieją jakiekolwiek różnice w wydajności między
void fooByValue(SomeDataStruct data);
i
void fooByReference(const SomeDataStruct& data);
Czy praktyka, której się nauczyłem - przekazywanie stałych odwołań (domyślnie dla typów niebanalnych) - przedwczesna optymalizacja?
źródło
Odpowiedzi:
„Optymalizacja przedwczesna” nie polega na wczesnym korzystaniu z optymalizacji . Chodzi o optymalizację przed zrozumieniem problemu, przed zrozumieniem środowiska wykonawczego, a często sprawienie, że kod będzie mniej czytelny i mniej konserwowalny dla wątpliwych wyników.
Używanie „const &” zamiast przekazywania obiektu przez wartość jest dobrze rozumianą optymalizacją, z dobrze zrozumiałym wpływem na środowisko wykonawcze, praktycznie bez wysiłku i bez żadnego złego wpływu na czytelność i łatwość konserwacji. W rzeczywistości poprawia oba, ponieważ mówi mi, że wywołanie nie zmodyfikuje przekazanego obiektu. Zatem dodanie „const &” zaraz po napisaniu kodu NIE JEST PREMATURE.
źródło
const&
, więc myślę, że pytanie jest całkiem rozsądne.const& foo
mówi, że funkcja nie zmodyfikuje foo, więc osoba dzwoniąca jest bezpieczna. Ale skopiowana wartość mówi, że żaden inny wątek nie może zmienić foo, więc odbiorca jest bezpieczny. Dobrze? Tak więc w aplikacji wielowątkowej odpowiedź zależy od poprawności, a nie od optymalizacji.TL; DR: Pomijając odniesienie do stałej jest nadal dobrym pomysłem w C ++, biorąc pod uwagę wszystko. Nie przedwczesna optymalizacja.
TL; DR2: Większość reklam nie ma sensu, dopóki nie zrobią tego.
Cel
Ta odpowiedź próbuje jedynie trochę rozszerzyć linkowany element w Wytycznych C ++ Core (po raz pierwszy wspomniany w komentarzu amona).
Ta odpowiedź nie próbuje rozwiązać problemu właściwego myślenia i stosowania różnych reklam szeroko rozpowszechnionych w kręgach programistów, szczególnie kwestii pogodzenia sprzecznych wniosków lub dowodów.
Możliwość zastosowania
Ta odpowiedź dotyczy tylko wywołań funkcji (nieusuwalnych zasięgów zagnieżdżonych w tym samym wątku).
(Uwaga dodatkowa). Gdy rzeczy przejezdne mogą wymknąć się z zakresu (tj. Mają okres życia, który potencjalnie przekracza zakres zewnętrzny), ważniejsze staje się zaspokojenie potrzeby aplikacji w zakresie zarządzania czasem życia obiektu, zanim cokolwiek innego. Zwykle wymaga to użycia odniesień, które są również w stanie zarządzać przez całe życie, takich jak inteligentne wskaźniki. Alternatywą może być użycie menedżera. Zauważ, że lambda jest rodzajem odłączalnego lunety; Przechwyty lambda zachowują się tak, jakby miały zasięg obiektu. Dlatego uważaj na zrzuty lambda. Uważaj również na to, jak przekazywana jest sama lambda - przez kopię lub przez odniesienie.
Kiedy przekazać wartość
W przypadku wartości skalarnych (standardowe operacje podstawowe, które mieszczą się w rejestrze maszyny i mają wartość semantyczną), dla których nie ma potrzeby komunikacji po mutacji (wspólne odwołanie), należy przekazać wartość.
W sytuacjach, w których odbiorca wymaga klonowania obiektu lub agregatu, należy przekazać wartość, w której kopia odbiorcy spełnia potrzebę sklonowanego obiektu.
Kiedy przekazywać przez odniesienie itp.
we wszystkich innych sytuacjach, mijaj wskaźniki, referencje, inteligentne wskaźniki, uchwyty (patrz: idiom uchwyt-ciało) itp. Ilekroć ta rada jest przestrzegana, stosuj zasadę stałej poprawności jak zwykle.
Rzeczy (agregaty, obiekty, tablice, struktury danych), które mają wystarczająco dużo miejsca w pamięci, zawsze powinny być zaprojektowane w celu ułatwienia przekazywania przez odniesienie, ze względu na wydajność. Ta rada zdecydowanie obowiązuje, gdy ma ona setki bajtów lub więcej. Ta rada ma granicę, gdy ma kilkadziesiąt bajtów.
Niezwykłe paradygmaty
Istnieją specjalne paradygmaty programowania, które z założenia są obficie kopiowane. Na przykład przetwarzanie ciągów, serializacja, komunikacja sieciowa, izolacja, zawijanie bibliotek stron trzecich, komunikacja między procesami w pamięci współużytkowanej itp. W tych obszarach aplikacji lub paradygmatach programowania dane są kopiowane ze struktur do struktur, a czasem ponownie pakowane w tablice bajtów.
Jak specyfikacja języka wpływa na tę odpowiedź przed rozważeniem optymalizacji.
Sub-TL; DR Propagowanie referencji nie powinno wywoływać żadnego kodu; przejście przez stałą referencyjną spełnia to kryterium. Jednak wszystkie pozostałe języki spełniają to kryterium bez wysiłku.
(Początkującym programistom C ++ zaleca się całkowite pominięcie tej sekcji).
(Początek tego rozdziału jest częściowo inspirowany odpowiedzią gnasher729. Jednak dochodzi się do innego wniosku.)
C ++ pozwala konstruktorom kopii i operatorom przypisania zdefiniowanym przez użytkownika.
(To był (był) odważny wybór, który był (był) zarówno niesamowity, jak i godny ubolewania. Jest to zdecydowanie odchylenie od dzisiejszej akceptowalnej normy w projektowaniu języka.)
Nawet jeśli programista C ++ tego nie zdefiniuje, kompilator C ++ musi wygenerować takie metody w oparciu o zasady językowe, a następnie ustalić, czy należy wykonać dodatkowy kod inny niż
memcpy
. Na przykład, aclass
/struct
zawierający elementstd::vector
członkowski musi mieć konstruktor kopii i operator przypisania, który nie jest trywialny.W innych językach odradzane są konstruktory kopiowania i klonowanie obiektów (z wyjątkiem przypadków, gdy jest to absolutnie konieczne i / lub znaczące dla semantyki aplikacji), ponieważ obiekty mają semantykę odniesienia, według projektu językowego. Te języki będą zazwyczaj miały mechanizm usuwania śmieci, który jest oparty na osiągalności zamiast własności opartej na zakresie lub liczenia referencji.
Gdy w C ++ (lub C) przekazywana jest referencja lub wskaźnik (w tym const referen), programista ma pewność, że nie zostanie wykonany żaden specjalny kod (funkcje zdefiniowane przez użytkownika lub wygenerowane przez kompilator), poza propagacją wartości adresu (odniesienie lub wskaźnik). Jest to przejrzystość zachowania, z której programiści C ++ czują się swobodnie.
Jednak tło jest takie, że język C ++ jest niepotrzebnie skomplikowany, tak że ta przejrzystość zachowania jest jak oaza (siedlisko, które można przeżyć) gdzieś w pobliżu strefy opadu jądrowego.
Aby dodać więcej błogosławieństw (lub obelg), C ++ wprowadza uniwersalne odwołania (wartości r) w celu ułatwienia operatorom ruchów zdefiniowanym przez użytkownika (konstruktory ruchów i operatory przypisania ruchów) dobrej wydajności. Jest to korzystne dla bardzo istotnego przypadku użycia (przenoszenia (przenoszenia) obiektów z jednej instancji do drugiej), poprzez zmniejszenie potrzeby kopiowania i głębokiego klonowania. Jednak w innych językach nielogiczne jest mówienie o takim ruchu obiektów.
(Sekcja nie na temat) Sekcja poświęcona artykułowi „Chcesz prędkości? Przekaż wartość!” napisany około 2009 roku.
Ten artykuł został napisany w 2009 roku i wyjaśnia uzasadnienie projektu dla wartości rw C ++. Artykuł ten stanowi ważny kontrargument do mojego wniosku w poprzedniej sekcji. Jednak przykład kodu tego artykułu i oświadczenie o wydajności od dawna zostało obalone.
Sub-TL; DR Projektowanie semantyki wartości r w C ++ pozwala na przykład na zaskakująco elegancką semantykę po stronie użytkownika dla
Sort
funkcji. Tego eleganckiego nie można modelować (naśladować) w innych językach.Funkcja sortowania jest stosowana do całej struktury danych. Jak wspomniano powyżej, powielanie byłoby dużo. Jako optymalizacja wydajności (która jest praktycznie istotna), funkcja sortowania ma być destrukcyjna w wielu językach innych niż C ++. Niszczycielski oznacza, że docelowa struktura danych jest modyfikowana, aby osiągnąć cel sortowania.
W C ++ użytkownik może wybrać jedną z dwóch implementacji: destrukcyjną z lepszą wydajnością lub normalną, która nie modyfikuje danych wejściowych. (Szablon jest pominięty ze względu na zwięzłość).
Oprócz sortowania, ta elegancja jest również przydatna w implementacji destrukcyjnego algorytmu znajdowania mediany w tablicy (początkowo nieposortowanej), poprzez partycjonowanie rekurencyjne.
Zauważ jednak, że większość języków zastosowałaby zrównoważone podejście do sortowania binarnego do sortowania zamiast destrukcyjnego algorytmu sortowania do tablic. Dlatego praktyczne znaczenie tej techniki nie jest tak wysokie, jak się wydaje.
Jak optymalizacja kompilatora wpływa na tę odpowiedź
Kiedy inliniowanie (a także optymalizacja całego programu / optymalizacja czasu łącza) jest stosowane na kilku poziomach wywołań funkcji, kompilator jest w stanie zobaczyć (czasem wyczerpująco) przepływ danych. Gdy tak się dzieje, kompilator może zastosować wiele optymalizacji, z których niektóre mogą wyeliminować tworzenie całych obiektów w pamięci. Zazwyczaj, gdy taka sytuacja ma zastosowanie, nie ma znaczenia, czy parametry są przekazywane przez wartość, czy przez stałą odniesienia, ponieważ kompilator może analizować wyczerpująco.
Jeśli jednak funkcja niższego poziomu wywołuje coś, co jest poza analizą (np. Coś w innej bibliotece poza kompilacją lub wykres wywołania, który jest po prostu zbyt skomplikowany), to kompilator musi zoptymalizować defensywnie.
Obiekty większe niż wartość rejestru maszyny mogą być kopiowane przez jawne instrukcje ładowania / przechowywania pamięci lub przez wywołanie czcigodnej
memcpy
funkcji. Na niektórych platformach kompilator generuje instrukcje SIMD w celu przemieszczania się między dwiema lokalizacjami pamięci, a każda instrukcja przenosi dziesiątki bajtów (16 lub 32).Dyskusja na temat gadatliwości lub bałaganu wizualnego
Programiści C ++ są do tego przyzwyczajeni, tzn. Dopóki programista nie nienawidzi C ++, narzut związany z pisaniem lub odczytywaniem stałych odniesień w kodzie źródłowym nie jest straszny.
Analizy kosztów i korzyści mogły być wykonywane wiele razy wcześniej. Nie wiem, czy są jakieś naukowe, które powinny być cytowane. Sądzę, że większość analiz byłaby nienaukowa lub odtwarzalna.
Oto, co sobie wyobrażam (bez dowodów i wiarygodnych referencji) ...
źródło
Nie zaleca się programistom korzystania z najwolniejszych dostępnych technik. Chodzi o skupienie się na przejrzystości podczas pisania programów. Często klarowność i wydajność stanowią kompromis: jeśli musisz wybrać tylko jeden, wybierz klarowność. Ale jeśli uda ci się osiągnąć oba z łatwością, nie ma potrzeby osłabiania jasności (np. Sygnalizowania, że coś jest stałe), aby uniknąć wydajności.
źródło
Przekazywanie przez ([const] [rvalue] reference) | (wartość) powinno dotyczyć intencji i obietnic złożonych przez interfejs. Nie ma to nic wspólnego z wydajnością.
Zasada kciuka Richya:
źródło
Teoretycznie odpowiedź powinna brzmieć „tak”. I w rzeczywistości tak jest przez pewien czas - w rzeczywistości przekazywanie przez stałą referencyjną zamiast tylko przekazywania wartości może być pesymizacją, nawet w przypadkach, gdy przekazywana wartość jest zbyt duża, aby zmieścić się w jednym zarejestrować (lub większość innych heurystyk, których ludzie próbują użyć, aby określić, kiedy przekazać wartość, czy nie). Wiele lat temu David Abrahams napisał artykuł zatytułowany „Chcesz prędkości? Przekazać wartość!” obejmujący niektóre z tych przypadków. Nie jest już łatwo go znaleźć, ale jeśli możesz wykopać kopię, warto ją przeczytać (IMO).
Jednak w konkretnym przypadku przekazywania przez odniesienie do stałej powiedziałbym, że ten idiom jest tak dobrze ustalony, że sytuacja jest mniej lub bardziej odwrócona: chyba że wiesz, że typ będzie
char
/short
/int
/long
, ludzie oczekują, że obejmie go const referencja domyślnie, więc prawdopodobnie najlepiej się z tym pogodzić, chyba że masz dość konkretny powód, aby zrobić inaczej.źródło