Kan moderne C++ u gratis prestaties opleveren?

Er wordt wel eens beweerd dat C++11/14 je een prestatieverbetering kan opleveren, zelfs als je alleen maar C++98-code compileert. De rechtvaardiging is meestal in de trant van verplaatsingssemantiek, omdat in sommige gevallen de rvalue-constructors automatisch worden gegenereerd of nu deel uitmaken van de STL. Nu vraag ik me af of deze gevallen eerder al zijn afgehandeld door RVO of vergelijkbare compiler-optimalisaties.

Mijn vraag is dan of je me een echt voorbeeld kunt geven van een stuk C++98-code dat, zonder aanpassingen, sneller werkt met een compiler die de nieuwe taalfuncties ondersteunt. Ik begrijp wel dat een standaard conforme compiler niet vereist is om de kopie-elisie uit te voeren en juist om die reden kan bewegingssemantiek voor snelheid zorgen, maar ik zou graag een minder pathologisch geval zien, als je wilt.

EDIT: voor alle duidelijkheid, ik vraag niet of nieuwe compilers sneller zijn dan oude compilers, maar eerder of er code is waarbij het toevoegen van -std=c++14 aan mijn compilervlaggen sneller zou werken (vermijd kopieën, maar als je iets anders kunt bedenken dan semantiek verplaatsen, ben ik ook geïnteresseerd)


Antwoord 1, autoriteit 100%

Ik ken 5 algemene categorieën waarin het opnieuw compileren van een C++03-compiler als C++11 onbeperkte prestatieverbeteringen kan veroorzaken die praktisch niets te maken hebben met de kwaliteit van de implementatie. Dit zijn allemaal variaties van bewegingssemantiek.

std::vectoropnieuw toewijzen

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

elke keer dat de buffer van de fooopnieuw wordt toegewezen in C++03, kopieerde het elke vectorin bar.

In C++11 verplaatst het in plaats daarvan de bar::datas, wat in principe gratis is.

In dit geval is dit afhankelijk van optimalisaties binnen de stdcontainer vector. In alle onderstaande gevallen is het gebruik van std-containers alleen omdat het C++-objecten zijn die een efficiënte move-semantiek hebben in C++11 “automatisch” wanneer u uw compiler opwaardeert. Objecten die het niet blokkeren en die een std-container bevatten, nemen ook de automatisch verbeterde move-constructors over.

NRVO-fout

Als NRVO (met de naam retourwaarde-optimalisatie) mislukt, valt het in C++03 terug op kopiëren, in C++11 valt het terug op verplaatsen. Storingen van NRVO zijn eenvoudig:

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;
}

of zelfs:

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;
}

We hebben drie waarden — de geretourneerde waarde en twee verschillende waarden binnen de functie. Met Elision kunnen de waarden binnen de functie worden ‘samengevoegd’ met de geretourneerde waarde, maar niet met elkaar. Ze kunnen beide niet worden samengevoegd met de retourwaarde zonder met elkaar te fuseren.

Het basisprobleem is dat NRVO-elisie kwetsbaar is, en code met wijzigingen die niet in de buurt van de return-site liggen, kan plotseling enorme prestatieverminderingen hebben op die plek zonder dat er een diagnose wordt uitgezonden. In de meeste NRVO-foutgevallen eindigt C++11 met een move, terwijl C++03 eindigt met een kopie.

Een functieargument retourneren

Elision is hier ook onmogelijk:

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

in C++11 is dit goedkoop: in C++03 is er geen manier om de kopie te vermijden. Argumenten voor functies kunnen niet worden weggelaten met de retourwaarde, omdat de levensduur en locatie van de parameter en de retourwaarde worden beheerd door de aanroepende code.

C++11 kan echter van de ene naar de andere gaan. (In een minder speelgoedvoorbeeld zou er iets aan de setkunnen worden gedaan).

push_backof insert

Uiteindelijk gebeurt er geen verwijdering in containers: maar C++11 overbelast de rvalue move insert-operators, wat kopieën bespaart.

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) );

in C++03 wordt een tijdelijke whatevergemaakt, dan wordt het gekopieerd naar de vector v. Er worden 2 std::stringbuffers toegewezen, elk met identieke gegevens, en één wordt verwijderd.

In C++11 wordt een tijdelijke whateveraangemaakt. De whatever&&push_backoverbelasting moves die tijdelijk naar de vector v. Er wordt één std::stringbuffer toegewezen en naar de vector verplaatst. Een lege std::stringwordt verwijderd.

Opdracht

Gestolen uit het antwoord van @Jarod42 hieronder.

Elision kan niet optreden bij toewijzing, maar verplaatsen van kan wel.

std::set<int> some_function();
std::set<int> some_value;
// code
some_value = some_function();

hier some_functionretourneert een kandidaat om uit te verwijderen, maar omdat het niet wordt gebruikt om direct een object te construeren, kan het niet worden weggelaten. In C++03 resulteert het bovenstaande erin dat de inhoud van het tijdelijke wordt gekopieerd naar some_value. In C++11 wordt het verplaatst naar some_value, wat in principe gratis is.


Voor het volledige effect van het bovenstaande heb je een compiler nodig die move-constructors en toewijzingen voor je synthetiseert.

MSVC 2013 implementeert move-constructors in stdcontainers, maar synthetiseert geen move-constructors op uw typen.

Dus typen die std::vectors en dergelijke bevatten, krijgen dergelijke verbeteringen niet in MSVC2013, maar krijgen ze wel in MSVC2015.

clang en gcc hebben al lang impliciete move-constructors geïmplementeerd. Intel’s compiler voor 2013 ondersteunt impliciete generatie van move-constructors als je -Qoption,cpp,--gen_move_operationsdoorgeeft (ze doen dit niet standaard in een poging om cross-compatibel te zijn met MSVC2013).


Antwoord 2, autoriteit 20%

als je iets hebt als:

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

Je hebt een kopie in C++03, terwijl je een verplaatsingsopdracht hebt in C++11.
dus je hebt in dat geval gratis optimalisatie.

Other episodes