Czy pisanie kodu opartego na optymalizacji kompilatora jest złą praktyką?

99

Uczyłem się trochę C ++ i często muszę zwracać duże obiekty z funkcji tworzonych w ramach funkcji. Wiem, że istnieje przejście przez referencję, zwrócenie wskaźnika i zwrócenie rozwiązań typu referencyjnego, ale przeczytałem również, że kompilatory C ++ (i standard C ++) pozwalają na optymalizację zwracanych wartości, co pozwala uniknąć kopiowania dużych obiektów przez pamięć, a tym samym oszczędzając czas i pamięć tego wszystkiego.

Teraz wydaje mi się, że składnia jest o wiele jaśniejsza, gdy obiekt jest jawnie zwracany przez wartość, a kompilator zazwyczaj wykorzystuje RVO i czyni proces bardziej wydajnym. Czy poleganie na tej optymalizacji jest złą praktyką? To sprawia, że ​​kod jest bardziej przejrzysty i czytelny dla użytkownika, co jest niezwykle ważne, ale czy powinienem być ostrożny, zakładając, że kompilator złapie okazję RVO?

Czy to mikrooptymalizacja, czy coś, o czym powinienem pamiętać podczas projektowania mojego kodu?

Matt
źródło
7
Aby odpowiedzieć na twoją edycję, jest to mikrooptymalizacja, ponieważ nawet jeśli spróbujesz porównać wyniki, które zarabiasz w nanosekundach, ledwo to zobaczysz. Co do reszty, jestem zbyt zepsuty w C ++, aby dać ci ścisłą odpowiedź na pytanie, dlaczego to nie zadziała. Jeden z nich, jeśli jest prawdopodobne, że zdarzają się przypadki, gdy potrzebujesz dynamicznej alokacji, a zatem używasz nowego / wskaźnika / referencji.
Walfrat,
4
@Walfrat, nawet jeśli obiekty są dość duże, rzędu megabajtów? Moje tablice mogą stać się ogromne ze względu na naturę problemów, które rozwiązuję.
Matt
6
@Matt Nie zrobiłbym tego. Referencje / wskaźniki istnieją właśnie w tym celu. Optymalizacje kompilatora powinny wykraczać poza to, co programiści powinni wziąć pod uwagę przy tworzeniu programu, nawet jeśli tak, często zdarza się, że oba światy się nakładają.
Neil,
5
@Matt Jeśli nie robisz czegoś wyjątkowo specyficznego, co powinno wymagać od programistów z ponad 10-letnim doświadczeniem w jądrach C /, niskie interakcje ze sprzętem nie powinny być potrzebne. Jeśli uważasz, że należysz do czegoś bardzo szczególnego, edytuj swój post i dodaj dokładny opis tego, co powinna zrobić Twoja aplikacja (ciężkie obliczenia matematyczne w czasie rzeczywistym? ...)
Walfrat,
37
W konkretnym przypadku RVO C ++ (N) tak, poleganie na tej optymalizacji jest całkowicie poprawne. Jest tak, ponieważ standard C ++ 17 wyraźnie nakazuje, aby tak się stało w sytuacjach, w których nowoczesne kompilatory już to robiły.
Caleth,

Odpowiedzi:

130

Stosuj zasadę najmniejszego zdziwienia .

Czy to ty i tylko ty będziesz używał tego kodu i czy jesteś pewien, że za 3 lata nie będziesz zaskoczony tym, co robisz?

Wtedy idź przed siebie.

We wszystkich innych przypadkach użyj standardowego sposobu; w przeciwnym razie ty i twoi koledzy będziecie mieli trudności ze znalezieniem błędów.

Na przykład mój kolega skarżył się na mój kod powodujący błędy. Okazuje się, że w ustawieniach kompilatora wyłączył ocenę zwarcia Boolean. Prawie go spoliczkowałem.

Pieter B.
źródło
88
@Neil o to mi chodzi, wszyscy polegają na ocenie zwarć. I nie powinieneś się nad tym zastanawiać, należy go włączyć. To standard defacto. Tak, możesz to zmienić, ale nie powinieneś.
Pieter B
49
„Zmieniłem sposób działania języka, a twój brudny zgniły kod zepsuł się! Arghh!” Łał. Uderzenie byłoby odpowiednie, wyślij swojego kolegę na trening Zen, jest go dużo.
109
@PieterB Jestem pewien, że specyfikacje języka C i C ++ gwarantują ocenę zwarcia. Więc to nie tylko de facto standardem, to standardem. Bez niego nawet nie używasz już C / C ++, ale coś, co jest podejrzanie podobne: P
marcelm
47
Dla porównania, standardowym sposobem jest tutaj zwracanie wartości.
DeadMG,
28
@ dan04 tak, to było w Delphi. Chłopaki, nie dajcie się wciągnąć w ten przykład, o którym mówiłem. Nie rób zaskakujących rzeczy, których nikt inny nie robi.
Pieter B
81

W tym konkretnym przypadku zdecydowanie warto wrócić po wartości.

  • RVO i NRVO są dobrze znanymi i solidnymi optymalizacjami, które naprawdę powinny być wykonane przez dowolny porządny kompilator, nawet w trybie C ++ 03.

  • Przestaw semantykę, aby obiekty zostały przeniesione z funkcji, jeśli (N) RVO nie miało miejsca. Przydaje się to tylko wtedy, gdy Twój obiekt korzysta z dynamicznych danych wewnętrznie (jak std::vectorrobi to), ale tak powinno być, jeśli jest tak duży - przepełnienie stosu jest ryzykowne w przypadku dużych automatycznych obiektów.

  • C ++ 17 wymusza RVO. Nie martw się, nie zniknie ono na tobie i zakończy się całkowicie dopiero wtedy, gdy kompilatory będą aktualne.

I na koniec, wymuszanie dodatkowej alokacji dynamicznej w celu zwrócenia wskaźnika lub wymuszanie domyślnej konstrukcji typu wyniku, aby można go było przekazać jako parametr wyjściowy, zarówno brzydkie, jak i nie idiomatyczne rozwiązania problemu, którego prawdopodobnie nigdy nie będziesz mieć, posiadać.

Wystarczy napisać kod, który ma sens, i podziękować autorom kompilatora za poprawną optymalizację kodu, który ma sens.

Quentin
źródło
9
Dla zabawy zobacz, jak Borland Turbo C ++ 3.0 z 1990 roku obsługuje RVO . Spoiler: Zasadniczo działa dobrze.
nwp 12.10.17
9
Kluczem jest to, że nie jest to żadna przypadkowa optymalizacja specyficzna dla kompilatora lub „nieudokumentowana funkcja”, ale coś, co, chociaż technicznie opcjonalne w kilku wersjach standardu C ++, zostało mocno poparte przez przemysł i prawie każdy główny kompilator zrobił to dla bardzo długi czas.
7
Ta optymalizacja nie jest tak solidna, jak by się mogło wydawać. Tak, jest raczej niezawodny w najbardziej oczywistych przypadkach, ale patrząc na Bugzillę gcc, istnieje wiele ledwo mniej oczywistych przypadków, w których jest ona pomijana.
Marc Glisse,
62

Teraz wydaje mi się, że składnia jest o wiele jaśniejsza, gdy obiekt jest jawnie zwracany przez wartość, a kompilator zazwyczaj wykorzystuje RVO i czyni proces bardziej wydajnym. Czy poleganie na tej optymalizacji jest złą praktyką? To sprawia, że ​​kod jest bardziej przejrzysty i czytelny dla użytkownika, co jest niezwykle ważne, ale czy powinienem być ostrożny, zakładając, że kompilator złapie okazję RVO?

Nie jest to żadna mało znana, urocza, mikrooptymalizacja, o której czytasz na jakimś małym blogu o małym natężeniu ruchu, a potem możesz czuć się sprytnie i lepiej korzystać z niej.

Po C ++ 11 RVO jest standardowym sposobem pisania tego kodu. Jest to powszechne, oczekiwane, nauczane, wspomniane w rozmowach, wspomniane w blogach, wspomniane w standardzie, będą zgłaszane jako błędy kompilatora, jeśli nie zostaną zaimplementowane. W C ++ 17 język idzie o krok dalej i nakazuje kopiowanie elision w niektórych scenariuszach.

Powinieneś absolutnie polegać na tej optymalizacji.

Co więcej, wartość zwracana przez wartość prowadzi do znacznie łatwiejszego do odczytania i zarządzania kodem niż kod zwracany przez referencję. Semantyka wartości jest potężną rzeczą, która sama w sobie może prowadzić do większych możliwości optymalizacji.

Barry
źródło
3
Dzięki temu ma to sens i jest zgodne z „zasadą najmniejszego zdziwienia” wspomnianą powyżej. Sprawiłoby to, że kod byłby bardzo przejrzysty i zrozumiały, i utrudniłoby bałagan ze wskazówkami.
Matt
3
@Matt Jednym z powodów, dla których głosowałem za tą odpowiedzią, jest to, że wspomina o „semantyce wartości”. W miarę zdobywania doświadczenia w C ++ (i ogólnie w programowaniu), od czasu do czasu możesz znaleźć semantykę wartości dla niektórych obiektów, ponieważ są one zmienne, a ich zmiany muszą być widoczne dla innego kodu używającego tego samego obiektu (an przykład „wspólnej zmienności”). Kiedy takie sytuacje się zdarzą, dotknięte obiekty będą musiały zostać udostępnione za pośrednictwem (inteligentnych) wskaźników.
rwong
16

Poprawność pisanego kodu nigdy nie powinna zależeć od optymalizacji. Powinien wyświetlać poprawny wynik po uruchomieniu na „maszynie wirtualnej” C ++, której używają w specyfikacji.

Jednak to, o czym mówisz, jest raczej pytaniem o wydajność. Twój kod działa lepiej, jeśli jest zoptymalizowany za pomocą kompilatora optymalizującego RVO. W porządku, z wszystkich powodów wskazanych w innych odpowiedziach.

Jeśli jednak potrzebujesz tej optymalizacji (na przykład, jeśli konstruktor kopiowania spowodowałby awarię kodu), teraz masz kaprysy kompilatora.

Myślę, że najlepszym przykładem tego w mojej własnej praktyce jest optymalizacja wezwania ogona:

   int sillyAdd(int a, int b)
   {
      if (b == 0)
          return a;
      return sillyAdd(a + 1, b - 1);
   }

To głupi przykład, ale pokazuje wywołanie ogonowe, w którym funkcja jest wywoływana rekurencyjnie na końcu funkcji. Maszyna wirtualna C ++ pokaże, że ten kod działa poprawnie, chociaż mogę sprawić trochę zamieszania, dlaczego w ogóle pisałem taką procedurę dodawania. Jednak w praktycznych implementacjach C ++ mamy stos i ma on ograniczoną przestrzeń. Jeśli zostanie to zrobione pedantycznie, funkcja ta musiałaby wcisnąć przynajmniej b + 1stos ramek na stos, podobnie jak jego dodawanie. Jeśli chcę obliczyć sillyAdd(5, 7), to nie jest wielka sprawa. Jeśli chcę obliczyć sillyAdd(0, 1000000000), mogę mieć poważne kłopoty z spowodowaniem przepływu StackOverflow (a nie tego dobrego ).

Widzimy jednak, że kiedy osiągniemy ostatnią linię powrotną, naprawdę skończyliśmy ze wszystkim w bieżącej ramce stosu. Tak naprawdę nie musimy tego robić. Optymalizacja wywołania ogona pozwala „ponownie wykorzystać” istniejącą ramkę stosu do następnej funkcji. W ten sposób potrzebujemy tylko 1 ramki stosu b+1. (Nadal musimy robić te wszystkie głupie dodawania i odejmowania, ale nie zajmują więcej miejsca.) W efekcie optymalizacja zmienia kod w:

   int sillyAdd(int a, int b)
   {
      begin:
      if (b == 0)
          return a;
      // return sillyAdd(a + 1, b - 1);
      a = a + 1;
      b = b - 1;
      goto begin;  
   }

W niektórych językach specyfikacja wyraźnie wymaga optymalizacji ogona. C ++ nie jest jednym z nich. Nie mogę polegać na kompilatorach C ++ w rozpoznawaniu możliwości optymalizacji wywołania ogona, chyba że pójdę indywidualnie. W mojej wersji programu Visual Studio wersja wydania wykonuje optymalizację wywołania ogona, ale wersja debugowania nie (zgodnie z projektem).

Dlatego źle byłoby dla mnie polegać na umiejętności obliczania sillyAdd(0, 1000000000).

Cort Ammon
źródło
2
Jest to interesujący przypadek narożny, ale nie sądzę, aby można go uogólnić na zasadę z pierwszego akapitu. Załóżmy, że mam program na małe urządzenie, które ładuje się tylko wtedy, gdy korzystam z optymalizacji zmniejszających rozmiar kompilatora - czy to źle? wydaje się raczej pedantyczne powiedzieć, że moim jedynym słusznym wyborem jest przepisanie go w asemblerze, szczególnie jeśli przepisanie to robi to samo, co optymalizator w celu rozwiązania problemu.
sdenham
5
@ sdenham Przypuszczam, że w sporze jest trochę miejsca. Jeśli nie piszesz już dla „C ++”, a raczej dla „kompilatora WindRiver C ++ w wersji 3.4.1”, to widzę logikę. Jednak z reguły jeśli piszesz coś, co nie działa poprawnie zgodnie ze specyfikacją, znajdujesz się w zupełnie innym scenariuszu. Wiem, że biblioteka Boost ma taki kod, ale zawsze umieszczają go w #ifdefblokach i mają dostępne obejście zgodne ze standardami.
Cort Ammon
4
czy to literówka w drugim bloku kodu, w którym jest napisane b = b + 1?
stib
2
Możesz wyjaśnić, co rozumiesz przez „maszynę wirtualną C ++”, ponieważ nie jest to termin używany w żadnym standardowym dokumencie. Myślę , że mówisz o modelu wykonania C ++, ale nie do końca pewny - a twój termin jest zwodniczo podobny do „maszyny wirtualnej z kodem bajtowym”, która odnosi się do czegoś zupełnie innego.
Toby Speight
1
@supercat Scala ma również jawną składnię rekurencji ogona. C ++ jest własną bestią, ale myślę, że rekurencja ogona jest jednoznaczna dla języków niefunkcjonalnych i obowiązkowa dla języków funkcjonalnych, pozostawiając niewielki zestaw języków, w których rozsądna jest jawna składnia rekurencji. Dosłowne tłumaczenie rekurencji ogona na pętle i jawna mutacja jest po prostu lepszą opcją dla wielu języków.
prosfilaes
8

W praktyce programy C ++ oczekują pewnych optymalizacji kompilatora.

Przyjrzyj się zwłaszcza standardowym nagłówkom standardowych implementacji kontenerów . Dzięki GCC możesz poprosić o wstępnie przetworzony formularz ( g++ -C -E) i wewnętrzną reprezentację g++ -fdump-tree-gimpleGIMPLE ( lub Gimple SSA z -fdump-tree-ssa) większości plików źródłowych (technicznie jednostek tłumaczeniowych) przy użyciu kontenerów. Zaskoczy Cię ilość optymalizacji, która jest wykonana (z g++ -O2). Dlatego implementatorzy kontenerów polegają na optymalizacjach (i przez większość czasu implementator standardowej biblioteki C ++ wie, co by się wydarzyła optymalizacja i zapisuje implementację kontenera z myślą o nich; czasami pisałby również zapis optymalizacyjny w kompilatorze do zajmować się funkcjami wymaganymi przez standardową bibliotekę C ++).

W praktyce optymalizacje kompilatora sprawiają, że C ++ i jego standardowe kontenery są wystarczająco wydajne. Możesz więc na nich polegać.

Podobnie jest w przypadku sprawy RVO wspomnianej w pytaniu.

Standard C ++ został zaprojektowany wspólnie (w szczególności poprzez eksperymentowanie z wystarczająco dobrymi optymalizacjami przy jednoczesnym proponowaniu nowych funkcji), aby działał dobrze z możliwymi optymalizacjami.

Rozważmy na przykład poniższy program:

#include <algorithm>
#include <vector>

extern "C" bool all_positive(const std::vector<int>& v) {
  return std::all_of(v.begin(), v.end(), [](int x){return x >0;});
}

skompiluj to z g++ -O3 -fverbose-asm -S. Dowiesz się, że wygenerowana funkcja nie uruchamia żadnej CALLinstrukcji maszyny. Tak więc większość kroki C ++ (budowa zamknięcia lambda, jego wielokrotnego stosowania, zdobycie begini enditeratory itd ...) zostały zoptymalizowane. Kod maszynowy zawiera tylko pętlę (która nie pojawia się jawnie w kodzie źródłowym). Bez takich optymalizacji C ++ 11 nie odniesie sukcesu.

addenda

(dodany 31 grudnia st 2017)

Zobacz CppCon 2017: Matt Godbolt „Co ostatnio zrobił dla mnie mój kompilator? Unbolting the Compiler's Lid ” .

Basile Starynkevitch
źródło
4

Ilekroć korzystasz z kompilatora, rozumiesz, że wygeneruje on kod maszynowy lub bajtowy. Nie gwarantuje to niczego takiego jak ten wygenerowany kod, poza tym, że zaimplementuje kod źródłowy zgodnie ze specyfikacją języka. Należy pamiętać, że ta gwarancja jest taka sama niezależnie od zastosowanego poziomu optymalizacji, a zatem ogólnie nie ma powodu, aby uważać jedno wyjście za bardziej „właściwe” niż drugie.

Co więcej, w takich przypadkach, jak RVO, gdzie jest to określone w języku, wydaje się, że nie ma sensu starać się go unikać, zwłaszcza jeśli upraszcza to kod źródłowy.

Dużo wysiłku włożono w to, aby kompilatory generowały wydajną moc wyjściową, i oczywiście celem jest wykorzystanie tych możliwości.

Mogą istnieć powody używania niezoptymalizowanego kodu (na przykład do debugowania), ale przypadek wymieniony w tym pytaniu nie wydaje się być jednym (a jeśli Twój kod zawiedzie tylko po zoptymalizowaniu, i nie jest to konsekwencją pewnej osobliwości urządzenie, na którym go uruchomiłeś, to gdzieś jest błąd i jest mało prawdopodobne, że znajdzie się w kompilatorze.)

sdenham
źródło
3

Myślę, że inni dobrze opisywali konkretny kąt dotyczący C ++ i RVO. Oto bardziej ogólna odpowiedź:

Jeśli chodzi o poprawność, nie powinieneś polegać na optymalizacjach kompilatora ani ogólnie na specyficznych zachowaniach kompilatora. Na szczęście nie robisz tego.

Jeśli chodzi o wydajność, to trzeba liczyć na zachowanie kompilatora specyficzne w ogóle, a optymalizacje kompilatora w szczególności. Kompilator zgodny ze standardami może dowolnie kompilować kod w dowolny sposób, o ile skompilowany kod zachowuje się zgodnie ze specyfikacją języka. Nie znam żadnej specyfikacji głównego języka, która określa szybkość każdej operacji.

svick
źródło
1

Optymalizacje kompilatora powinny wpływać tylko na wydajność, a nie na wyniki. Poleganie na optymalizacjach kompilatora w celu spełnienia niefunkcjonalnych wymagań jest nie tylko uzasadnione, ale często jest przyczyną wyboru jednego kompilatora.

Flagi, które określają sposób wykonywania poszczególnych operacji (na przykład warunki indeksowania lub przepełnienia), są często łączone z optymalizacjami kompilatora, ale nie powinny. Wyraźnie wpływają na wyniki obliczeń.

Jeśli optymalizacja kompilatora powoduje różne wyniki, jest to błąd - błąd w kompilatorze. Opieranie się na błędzie w kompilatorze jest na dłuższą metę błędem - co się stanie, gdy zostanie naprawione?

Używanie flag kompilatora, które zmieniają sposób wykonywania obliczeń, powinno być dobrze udokumentowane, ale używane w razie potrzeby.

jmoreno
źródło
Niestety, wiele dokumentacji kompilatora nie pozwala na określenie, co jest gwarantowane w różnych trybach. Ponadto twórcy „nowoczesnych” kompilatorów wydają się nieświadomi kombinacji gwarancji, które programiści robią i nie potrzebują. Jeśli program działałby dobrze, gdyby x*y>zarbitralnie dawał 0 lub 1 w przypadku przepełnienia, pod warunkiem, że nie ma innych skutków ubocznych , wymagając, aby programista albo zapobiegał przepełnieniu za wszelką cenę, albo zmusił kompilator do oceny wyrażenia w określony sposób. niepotrzebnie zaburzają optymalizacje vs. mówienie, że ...
supercat
... kompilator może wx*y dowolnym momencie zachowywać się tak, jakby promował swoje operandy do dowolnego dowolnego dłuższego typu (umożliwiając w ten sposób formy podnoszenia i zmniejszania siły, które zmieniłyby zachowanie niektórych przypadków przepełnienia). Jednak wiele kompilatorów wymaga, aby programiści albo zapobiegali przepełnieniu za wszelką cenę, albo zmuszali kompilatory do obcinania wszystkich wartości pośrednich w przypadku przepełnienia.
supercat
1

Nie.

To właśnie robię cały czas. Jeśli potrzebuję uzyskać dostęp do dowolnego 16-bitowego bloku w pamięci, robię to

void *ptr = get_pointer();
uint16_t u16;
memcpy(&u16, ptr, sizeof(u16)); // ntohs omitted for simplicity

... i polegaj na kompilatorze, który robi wszystko, aby zoptymalizować ten fragment kodu. Kod działa na ARM, i386, AMD64 i praktycznie na każdej architekturze. Teoretycznie nieoptymalizowany kompilator mógłby wywołać memcpy, co skutkuje całkowitą złą wydajnością, ale nie stanowi to dla mnie problemu, ponieważ korzystam z optymalizacji kompilatora.

Rozważ alternatywę:

void *ptr = get_pointer();
uint16_t *u16ptr = ptr;
uint16_t u16;
u16 = *u16ptr;  // ntohs omitted for simplicity

Ten alternatywny kod nie działa na komputerach, które wymagają odpowiedniego wyrównania, jeśli get_pointer()zwróci nie wyrównany wskaźnik. Alternatywnie mogą występować problemy z aliasingiem.

Różnica między -O2 i -O0 podczas korzystania z memcpylewy jest ogromna: 3,2 Gb / s wydajności sumy kontrolnej IP w porównaniu z 67 Gb / s wydajności sumy kontrolnej IP. Ponad różnica wielkości rzędu!

Czasami możesz potrzebować pomocy kompilatorowi. Na przykład zamiast polegać na kompilatorze do rozwijania pętli, możesz to zrobić samodzielnie. Albo przez wdrożenie słynnego urządzenia Duffa , albo w bardziej czysty sposób.

Wadą polegającą na optymalizacjach kompilatora jest to, że jeśli uruchomisz gdb w celu debugowania kodu, możesz odkryć, że wiele zostało zoptymalizowanych. Może być więc konieczna ponowna kompilacja z opcją -O0, co oznacza, że ​​wydajność całkowicie wysysa podczas debugowania. Myślę, że jest to wada, którą warto wziąć pod uwagę, biorąc pod uwagę zalety optymalizacji kompilatorów.

Cokolwiek zrobisz, upewnij się, że twoja droga nie jest niezdefiniowanym zachowaniem. Z pewnością dostęp do losowego bloku pamięci jako 16-bitowej liczby całkowitej jest niezdefiniowanym zachowaniem z powodu problemów z aliasingiem i wyrównaniem.

juhist
źródło
0

Wszystkie próby wydajnego kodu napisanego w czymkolwiek poza złożeniem opierają się bardzo, bardzo w dużej mierze na optymalizacjach kompilatora, poczynając od najbardziej podstawowych, takich jak wydajne przydzielanie rejestrów, aby uniknąć zbędnych rozlewów stosów w każdym miejscu i co najmniej rozsądnie, jeśli nie doskonale, wybór instrukcji. W przeciwnym razie wrócilibyśmy do lat 80., gdzie musieliśmy wszędzie registerpodpowiedzieć i użyć minimalnej liczby zmiennych w funkcji, aby pomóc archaicznym kompilatorom języka C lub nawet wcześniej, gdy gotobyła to przydatna optymalizacja rozgałęziania.

Gdybyśmy nie czuli, że moglibyśmy polegać na zdolności naszego optymalizatora do optymalizacji naszego kodu, wszyscy nadal kodowalibyśmy ścieżki wykonywania krytyczne pod względem wydajności w asemblerze.

To naprawdę zależy od tego, jak niezawodnie czujesz, że można przeprowadzić optymalizację. Najlepszym rozwiązaniem jest profilowanie i analizowanie możliwości posiadanych kompilatorów, a nawet dezasemblacja, jeśli istnieje punkt dostępu, którego nie można ustalić, gdzie wydaje się kompilator. nie udało się dokonać oczywistej optymalizacji.

RVO jest czymś, co istnieje od wieków, a przynajmniej wykluczając bardzo złożone przypadki, jest to coś, co kompilatory niezawodnie stosują się od wieków. Zdecydowanie nie warto obejść problemu, który nie istnieje.

Błąd po stronie polegania na optymalizatorze, bez obaw

Wręcz przeciwnie, powiedziałbym, że błąd polega na tym, że zbyt wiele polega na optymalizacjach kompilatora, a za mało, a ta sugestia pochodzi od faceta, który pracuje w obszarach krytycznych pod względem wydajności, w których wydajność, łatwość konserwacji i postrzegana jakość wśród klientów jest wszystkie olbrzymie rozmycie. Wolałbym, abyś zbyt pewnie polegał na swoim optymalizatorze i znalazł jakieś niejasne przypadki, w których polegałeś zbyt wiele, zamiast polegać zbyt mało i po prostu wyrzucać z siebie przesądne lęki przez resztę życia. To przynajmniej pozwoli ci sięgnąć po profilera i właściwie zbadać, czy rzeczy nie działają tak szybko, jak powinny, i zdobyć po drodze cenną wiedzę, a nie przesądy.

Dobrze się opierasz na optymalizatorze. Tak trzymaj. Nie bądź taki, jak ten facet, który zaczyna jawnie prosić o wstawienie każdej funkcji wywoływanej w pętli, a nawet profilować z powodu błędnego strachu przed niedociągnięciami optymalizatora.

Profilowy

Profilowanie to tak naprawdę rondo, ale ostateczna odpowiedź na twoje pytanie. Problemem dla początkujących, którzy chcą pisać efektywny kod, z którym często się zmagają, jest nie to, co należy zoptymalizować, ale czego nie należy optymalizować, ponieważ rozwijają oni wszelkiego rodzaju błędne przeczucia dotyczące nieefektywności, które - choć ludzko intuicyjne - są błędne obliczeniowo. Rozwijanie doświadczenia z profilerem zacznie naprawdę zapewniać odpowiednią ocenę nie tylko możliwości optymalizacji kompilatorów, na których można śmiało polegać, ale także możliwości (i ograniczeń) sprzętu. Prawdopodobnie jeszcze więcej wartości ma profilowanie w uczeniu się, co nie było warte optymalizacji, niż uczenie się, co było.


źródło
-1

Oprogramowanie może być napisane w C ++ na bardzo różnych platformach i do wielu różnych celów.

Zależy to całkowicie od celu oprogramowania. Czy powinien być łatwy w utrzymaniu, rozszerzaniu, łataniu, refaktoryzacji itp. lub inne rzeczy są ważniejsze, takie jak wydajność, koszt lub kompatybilność z określonym sprzętem lub czas potrzebny na opracowanie.

matematyk
źródło
-2

Myślę, że nudna odpowiedź brzmi: „to zależy”.

Czy pisanie kodu opartego na optymalizacji kompilatora, który prawdopodobnie zostanie wyłączony i gdzie luka nie jest udokumentowana oraz gdzie dany kod nie jest testowany jednostkowo, nie jest testowane, aby wiedzieć, że gdyby się zepsuło, jest złą praktyką ? Prawdopodobnie.

Czy pisanie kodu opartego na optymalizacji kompilatora, którego prawdopodobnie nie da się wyłączyć , jest udokumentowane i testowane jednostkowo, jest złą praktyką ? Może nie.

Dave Cousineau
źródło
-6

Jeśli nie powiesz nam więcej, jest to zła praktyka, ale nie z powodu, który sugerujesz.

Możliwe, że w przeciwieństwie do innych języków, których używałeś wcześniej, zwrócenie wartości obiektu w C ++ daje kopię obiektu. Jeśli następnie zmodyfikujesz obiekt, modyfikujesz inny obiekt . To znaczy, jeśli mam, Obj a; a.x=1;a Obj b = a;potem mam b.x += 2; b.f();, to a.xwciąż równa się 1, a nie 3.

Tak więc nie, używanie obiektu jako wartości zamiast odniesienia lub wskaźnika nie zapewnia takiej samej funkcjonalności i może dojść do błędów w oprogramowaniu.

Być może wiesz o tym i nie wpływa to negatywnie na konkretny przypadek użycia. Jednak na podstawie sformułowania zawartego w pytaniu wydaje się, że możesz nie być świadomy tego rozróżnienia; sformułowanie takie jak „utwórz obiekt w funkcji”.

„stwórz obiekt w funkcji” brzmi tak, jak brzmi new Obj;„zwróć obiekt według wartości”Obj a; return a;

Obj a;i Obj* a = new Obj;są bardzo, bardzo różne rzeczy; te pierwsze mogą spowodować uszkodzenie pamięci, jeśli nie zostaną właściwie wykorzystane i zrozumiane, a drugie mogą spowodować wycieki pamięci, jeśli nie zostaną właściwie wykorzystane i zrozumiane.

Aaron
źródło
8
Optymalizacja wartości zwracanej (RVO) to dobrze zdefiniowany semantyczny, w którym kompilator konstruuje zwracany obiekt o jeden poziom wyżej w ramce stosu, unikając w ten sposób niepotrzebnych kopii obiektu. Jest to dobrze zdefiniowane zachowanie, które było obsługiwane na długo przed wprowadzeniem go w C ++ 17. Nawet 10-15 lat temu wszystkie główne kompilatory wspierały tę funkcję i robiły to konsekwentnie.
@ Snowman Nie mówię o fizycznym zarządzaniu pamięcią niskiego poziomu i nie rozmawiałem o wzdęciach ani szybkości pamięci. Jak konkretnie pokazałem w mojej odpowiedzi, mówię o logicznych danych. Logicznie , podanie wartości obiektu tworzy jego kopię, niezależnie od tego, w jaki sposób kompilator jest zaimplementowany lub jakiego zestawu używa się za sceną. Za kulisami rzeczy niskiego poziomu to jedno, a logiczna struktura i zachowanie języka to drugie; są ze sobą powiązane, ale to nie to samo - oba należy zrozumieć.
Aaron,
6
twoja odpowiedź mówi: „zwrócenie wartości obiektu w C ++ daje kopię obiektu”, co jest całkowicie fałszywe w kontekście RVO - obiekt jest konstruowany bezpośrednio w lokalizacji wywołującej i nigdy nie jest wykonywana żadna kopia. Możesz to przetestować, usuwając konstruktor kopii i zwracając obiekt zbudowany w returninstrukcji, która jest wymagana dla RVO. Co więcej, następnie mówisz o słowach kluczowych newi wskaźnikach, a nie o to chodzi w RVO. Wierzę, że albo nie rozumiesz pytania, albo RVO, albo być może jedno i drugie.
-7

Pieter B ma absolutną rację zalecając najmniejsze zdziwienie.

Aby odpowiedzieć na konkretne pytanie, co (najprawdopodobniej) oznacza to w C ++, należy zwrócić a std::unique_ptrdo skonstruowanego obiektu.

Powodem jest to, że dla programisty C ++ jest to bardziej zrozumiałe, co się dzieje.

Chociaż twoje podejście najprawdopodobniej zadziałałoby, skutecznie sygnalizujesz, że obiekt ma niewielki typ wartości, podczas gdy w rzeczywistości tak nie jest. Ponadto odrzucasz wszelkie możliwości abstrakcji interfejsu. Może to być odpowiednie dla twoich bieżących celów, ale często jest bardzo przydatne w przypadku macierzy.

Rozumiem, że jeśli pochodzisz z innych języków, początkowo wszystkie znaki mogą być mylące. Uważaj jednak, aby nie zakładać, że nieużywanie ich spowoduje, że kod będzie wyraźniejszy. W praktyce może być odwrotnie.

Alex
źródło
Jeśli wejdziesz między wrony, musisz krakać jak i one.
14
To nie jest dobra odpowiedź dla typów, które same nie wykonują alokacji dynamicznych. To, że OP uważa za naturalne w swoim przypadku użycia, że ​​zwracanie według wartości wskazuje, że jego obiekty mają automatyczny czas przechowywania po stronie wywołującej. W przypadku prostych, niezbyt dużych obiektów nawet naiwna implementacja wartości zwrotu z kopii będzie o rząd wielkości szybsza niż alokacja dynamiczna. (Jeśli z drugiej strony funkcja zwraca kontener, wówczas zwrócenie wskaźnika unikatowego może być nawet korzystne w porównaniu do naiwnego kompilatora zwracanego przez wartość.)
Peter A. Schneider,
9
@Matt W przypadku, gdy nie zdajesz sobie sprawy, że nie jest to najlepsza praktyka. Niepotrzebne jest przydziały pamięci i wymuszanie semantyki wskaźnika na użytkownikach.
nwp 12.10.17
5
Przede wszystkim, używając inteligentnych wskaźników, należy powrócić std::make_unique, a nie std::unique_ptrbezpośrednio. Po drugie, RVO nie jest jakąś ezoteryczną, specyficzną dla dostawcy optymalizacją: wpisuje się w standard. Nawet wtedy, gdy nie było, było szeroko wspierane i oczekiwane zachowanie. Nie ma sensu zwracanie a, std::unique_ptrgdy wskaźnik nie jest potrzebny.
4
@Snowman: Nie ma „kiedy nie było”. Chociaż dopiero niedawno stał się obowiązkowy , każdy standard C ++ rozpoznał [N] RVO i wprowadził ułatwienia, aby to umożliwić (np. Kompilator zawsze miał wyraźne pozwolenie na pominięcie użycia konstruktora kopiowania na wartości zwracanej, nawet jeśli ma widoczne skutki uboczne).
Jerry Coffin