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, a class
/ struct
zawierający element std::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ść).
/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
std::vector<T> result(std::move(input)); /* destructive move */
std::sort(result.begin(), result.end()); /* in-place sorting */
return result; /* return-value optimization (RVO) */
}
/*caller specifically passes in read-only argument*/
std::vector<T> my_sort(const std::vector<T>& input)
{
/* reuse destructive implementation by letting it work on a clone. */
/* Several things involved; e.g. expiring temporaries as r-value */
/* return-value optimization, etc. */
return my_sort(std::vector<T>(input));
}
/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/
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) ...
- Tak, wpływa to na wydajność oprogramowania napisanego w tym języku.
- Jeśli kompilatory mogą zrozumieć cel kodu, potencjalnie może być wystarczająco inteligentny, aby to zautomatyzować
- Niestety, w językach, które sprzyjają zmienności (w przeciwieństwie do czystości funkcjonalnej), kompilator klasyfikuje większość rzeczy jako zmutowane, w związku z czym automatyczne odliczanie stałości odrzuca większość rzeczy jako niestałe
- Koszty ogólne zależą od ludzi; ludzie, którzy uznają to za duże obciążenie umysłowe, odrzuciliby C ++ jako realny język programowania.