Czy nowoczesny C ++ może zapewnić Ci wydajność za darmo?

205

Czasami twierdzi się, że C ++ 11/14 może zwiększyć wydajność, nawet po prostu kompilując kod C ++ 98. Uzasadnienie jest zwykle zgodne z semantyką ruchu, ponieważ w niektórych przypadkach konstruktory wartości są generowane automatycznie lub są teraz częścią STL. Teraz zastanawiam się, czy te przypadki były wcześniej obsługiwane przez RVO lub podobne optymalizacje kompilatora.

Moje pytanie brzmi zatem, czy możesz podać mi prawdziwy przykład fragmentu kodu C ++ 98, który bez modyfikacji działa szybciej przy użyciu kompilatora obsługującego nowe funkcje językowe. Rozumiem, że standardowy kompilator nie jest wymagany do wykonania kopiowania i właśnie z tego powodu semantyka ruchu może przynieść szybkość, ale chciałbym zobaczyć mniej patologiczny przypadek, jeśli wolisz.

EDYCJA: Żeby było jasne, nie pytam, czy nowe kompilatory są szybsze niż stare kompilatory, ale raczej czy istnieje kod, dzięki któremu dodanie -std = c ++ 14 do moich flag kompilatora działałoby szybciej (unikaj kopii, ale jeśli mogę wymyślić coś innego niż semantykę ruchów, byłbym również zainteresowany)

duża
źródło
3
Pamiętaj, że eliminacja kopii i optymalizacja wartości zwracanej są wykonywane podczas konstruowania nowego obiektu za pomocą konstruktora kopii. Jednak w operatorze przypisania kopii nie ma wymazywania kopii (jak to możliwe, ponieważ kompilator nie wie, co zrobić z już zbudowanym obiektem, który nie jest tymczasowy). Dlatego w takim przypadku C ++ 11/14 wygrywa duże, dając ci możliwość korzystania z operatora przypisania ruchu. Jeśli chodzi o twoje pytanie, nie sądzę, że kod C ++ 98 powinien być szybszy, jeśli jest kompilowany przez kompilator C ++ 11/14, może jest szybszy, ponieważ kompilator jest nowszy.
vsoftco
27
Również kod korzystający ze standardowej biblioteki jest potencjalnie szybszy, nawet jeśli sprawisz, że będzie w pełni kompatybilny z C ++ 98, ponieważ w C ++ 11/14 podstawowa biblioteka używa wewnętrznej semantyki, jeśli to możliwe. Tak więc kod wyglądający identycznie w C ++ 98 i C ++ 11/14 będzie (prawdopodobnie) szybszy w tym drugim przypadku, ilekroć użyjesz standardowych obiektów biblioteki, takich jak wektory, listy itp., A semantyka przenoszenia robi różnicę.
vsoftco
1
@vsoftco, Do takiej sytuacji nawiązałem, ale nie mogłem wymyślić przykładu: z tego co pamiętam, jeśli muszę zdefiniować konstruktor kopiowania, konstruktor ruchu nie zostanie wygenerowany automatycznie, co pozostawia nam bardzo proste zajęcia, w których RVO, jak sądzę, zawsze działa. Wyjątkiem może być coś w połączeniu z kontenerami STL, w których konstruktory wartości są generowane przez implementatora biblioteki (co oznacza, że ​​nie musiałbym niczego zmieniać w kodzie, aby używał ruchów).
alarge
klasy nie muszą być proste, aby nie mieć konstruktora kopiowania. C ++ rozwija się w zakresie semantyki wartości, a konstruktor kopiowania, operator przypisania, destruktor itp. Powinny być wyjątkiem.
sp2danny
1
@Eric Dziękuję za link, to było interesujące. Jednak po szybkim przejrzeniu tego, zalety szybkości wydają się wynikać głównie z dodawania std::movei przenoszenia konstruktorów (co wymagałoby modyfikacji istniejącego kodu). Jedyne, co naprawdę związane było z moim pytaniem, to zdanie „Natychmiastowe korzyści prędkości po prostu poprzez rekompilację”, które nie jest poparte żadnymi przykładami (wspomina STL na tym samym slajdzie, jak zrobiłem w moim pytaniu, ale nic konkretnego ). Prosiłem o kilka przykładów. Jeśli źle czytam slajdy, daj mi znać.
alarge

Odpowiedzi:

221

Zdaję sobie sprawę z 5 ogólnych kategorii, w których rekompilacja kompilatora C ++ 03 jako C ++ 11 może powodować nieograniczony wzrost wydajności, który jest praktycznie niezwiązany z jakością implementacji. Są to wszystkie odmiany semantyki ruchu.

std::vector zmienić przydział

struct bar{
  std::vector<int> data;
};
std::vector<bar> foo(1);
foo.back().data.push_back(3);
foo.reserve(10); // two allocations and a delete occur in C++03

każdym razem, gdy foobufor „s jest przesunięte w C ++ 03 jest kopiowany każdy vectorw bar.

W C ++ 11 zamiast tego przenosi bar::datas, który jest w zasadzie darmowy.

W tym przypadku zależy to od optymalizacji wewnątrz stdkontenera vector. W każdym przypadku poniżej użycie stdkontenerów wynika tylko z tego, że są to obiekty C ++, które mają wydajną movesemantykę w C ++ 11 „automatycznie” podczas aktualizacji kompilatora. Obiekty, które go nie blokują i które zawierają stdkontener, również dziedziczą automatycznie ulepszone movekonstruktory.

Awaria NRVO

Kiedy NRVO (optymalizacja nazw zwracanych wartości) zawiedzie, w C ++ 03 wraca do kopii, w C ++ 11 wraca do ruchu. Awarie NRVO są łatwe:

std::vector<int> foo(int count){
  std::vector<int> v; // oops
  if (count<=0) return std::vector<int>();
  v.reserve(count);
  for(int i=0;i<count;++i)
    v.push_back(i);
  return v;
}

lub nawet:

std::vector<int> foo(bool which) {
  std::vector<int> a, b;
  // do work, filling a and b, using the other for calculations
  if (which)
    return a;
  else
    return b;
}

Mamy trzy wartości - wartość zwracaną i dwie różne wartości w funkcji. Elision pozwala „scalić” wartości w funkcji z wartością zwracaną, ale nie ze sobą. Oba nie mogą zostać połączone z wartością zwracaną bez połączenia ze sobą.

Podstawową kwestią jest to, że wybór NRVO jest kruchy, a kod ze zmianami, które nie znajdują się w pobliżu returnwitryny, może nagle spowodować znaczne obniżenie wydajności w tym miejscu bez emitowania diagnostyki. W większości przypadków awarii NRVO C ++ 11 kończy się na a move, podczas gdy C ++ 03 kończy się na kopii.

Zwracanie argumentu funkcji

Wykluczenie jest również tutaj niemożliwe:

std::set<int> func(std::set<int> in){
  return in;
}

w C ++ 11 jest to tanie: w C ++ 03 nie ma możliwości uniknięcia kopiowania. Argumenty funkcji nie mogą być pomijane za pomocą wartości zwracanej, ponieważ czasem życia i lokalizacją parametru i wartości zwracanej zarządza kod wywołujący.

Jednak C ++ 11 może przenosić się z jednego do drugiego. (W mniej zabawkowym przykładzie coś można zrobić z set).

push_back lub insert

Wreszcie nie następuje elucja do kontenerów: ale C ++ 11 przeciąża operatorów wstawiania wartości rvalue, co zapisuje kopie.

struct whatever {
  std::string data;
  int count;
  whatever( std::string d, int c ):data(d), count(c) {}
};
std::vector<whatever> v;
v.push_back( whatever("some long string goes here", 3) );

w C ++ 03 whatevertworzony jest plik tymczasowy , a następnie kopiowany do wektora v. std::stringPrzydzielono 2 bufory, każdy z identycznymi danymi, a jeden odrzucono.

W C ++ 11 whatevertworzony jest tymczasowy . whatever&& push_backPrzeciążenie następnie movey, że tymczasowa do wektora v. Jeden std::stringbufor jest przydzielany i przenoszony do wektora. Pusty std::stringjest odrzucany.

Zadanie

Skradzione z odpowiedzi @ Jarod42 poniżej.

Wykluczenie nie może nastąpić wraz z przypisaniem, ale przejście może.

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();

tutaj some_functionzwraca kandydata do ucieczki, ale ponieważ nie jest on używany do bezpośredniej budowy obiektu, nie można go uciec. W C ++ 03 powyższe powoduje skopiowanie zawartości tymczasowej some_value. W C ++ 11 jest przeniesiony do some_value, który w zasadzie jest darmowy.


Aby uzyskać pełny efekt powyższego, potrzebujesz kompilatora, który syntetyzuje dla ciebie konstruktory ruchów i przypisania.

MSVC 2013 implementuje konstruktory ruchu w stdkontenerach, ale nie syntezuje konstruktorów ruchu na twoich typach.

Tak więc typy zawierające std::vectorsi i podobne nie uzyskują takich ulepszeń w MSVC2013, ale zaczną je otrzymywać w MSVC2015.

clang i gcc już dawno zaimplementowały niejawne konstruktory ruchów. Kompilator Intela 2013 będzie obsługiwał niejawne generowanie konstruktorów ruchów, jeśli zdasz -Qoption,cpp,--gen_move_operations(domyślnie nie robią tego, aby być kompatybilnym z MSVC2013).

Jak - Adam Nevraumont
źródło
1
@alarge tak. Ale aby konstruktor ruchu był wielokrotnie wydajniejszy niż konstruktor kopiowania, zwykle musi przenosić zasoby zamiast je kopiować. Bez pisania własnych konstruktorów ruchów (i po prostu rekompilację programu C ++ 03), stdkontenery biblioteczne zostaną zaktualizowane movekonstruktorami „za darmo” i (jeśli go nie zablokowałeś) konstrukcje wykorzystujące te obiekty ( i wspomniane obiekty) zaczną uzyskiwać konstrukcję swobodnego ruchu w wielu sytuacjach. Wiele z tych sytuacji jest objętych wyborem w C ++ 03: nie wszystkie.
Yakk - Adam Nevraumont
5
Jest to zatem zła implementacja optymalizatora, ponieważ zwracane obiekty o różnych nazwach nie mają nakładającego się okresu istnienia, RVO jest teoretycznie nadal możliwe.
Ben Voigt,
2
@alarge Są miejsca, w których elekcja kończy się niepowodzeniem, na przykład gdy dwa obiekty z nakładającymi się okresami życia można połączyć w jedną trzecią, ale nie w siebie. Następnie wymagane jest przeniesienie w C ++ 11 i skopiowanie w C ++ 03 (ignorowanie as-if). W praktyce elucja jest często krucha. Zastosowanie stdpowyższych kontenerów wynika głównie z tego, że są one tanie w przenoszeniu za dużo, aby skopiować typ, który dostajesz „za darmo” w C ++ 11 podczas ponownej kompilacji C ++ 03. vector::resizeWyjątek: używa moveC ++ 11.
Yakk - Adam Nevraumont
27
Widzę tylko 1 kategorię ogólną, czyli semantykę ruchów, i 5 specjalnych przypadków.
Johannes Schaub - litb
3
@sebro Rozumiem, że nie uważasz, że „powoduje, że programy nie przydzielają wielu tysięcy wielu przydziałów kilobajtów, a zamiast tego przesuwa wskaźniki” jest wystarczające. Chcesz wyniki na czas. Mikrodrobności nie są dowodem na poprawę wydajności, jak dowód na to, że zasadniczo robisz mniej. Brak kilku 100 rzeczywistych aplikacji w wielu różnych branżach profilowanych za pomocą rzeczywistych zadań nie jest tak naprawdę dowodem. Przyjąłem niejasne twierdzenia na temat „darmowej wydajności” i podałem im konkretne fakty dotyczące różnic w zachowaniu programu w C ++ 03 i C ++ 11.
Yakk - Adam Nevraumont
46

jeśli masz coś takiego:

std::vector<int> foo(); // function declaration.
std::vector<int> v;

// some code

v = foo();

Masz kopię w C ++ 03, a zadanie przeniesienia w C ++ 11. więc masz darmową optymalizację w takim przypadku.

Jarod42
źródło
4
@Yakk: Jak następuje usunięcie kopii w zadaniu?
Jarod42
2
@ Jarod42 Uważam również, że wybór kopii nie jest możliwy w zadaniu, ponieważ lewa strona jest już zbudowana i nie ma rozsądnego sposobu, aby kompilator wiedział, co zrobić ze „starymi” danymi po kradzieży zasobów z prawej strony strona dłoni. Ale może się mylę, chciałbym znaleźć odpowiedź na zawsze. Skasowanie kopii ma sens, gdy kopiujesz konstrukt, ponieważ obiekt jest „świeży” i nie ma problemu z decyzją, co zrobić ze starymi danymi. O ile mi wiadomo, jedynym wyjątkiem jest to: „Przydziały mogą być podejmowane tylko w oparciu o zasadę„ jak gdyby ”
vsoftco,
4
Dobry kod C ++ 03 wykonał już ruch w tym przypadku za pośrednictwemfoo().swap(v);
Ben Voigt
@BenVoigt na pewno, ale nie cały kod jest zoptymalizowany i nie wszystkie miejsca, w których to się dzieje, są łatwo dostępne.
Yakk - Adam Nevraumont
Kopiowanie kopii może działać w zadaniu, jak mówi @BenVoigt. Lepszym terminem jest RVO (optymalizacja wartości zwracanej) i działa tylko wtedy, gdy zaimplementowano foo () w ten sposób.
DrumM