Il moderno C ++ può offrirti prestazioni gratuite?


205

A volte si afferma che C ++ 11/14 può offrirti un aumento delle prestazioni anche quando si compila semplicemente il codice C ++ 98. La giustificazione è di solito lungo le linee della semantica dello spostamento, poiché in alcuni casi i costruttori di valori vengono generati automaticamente o ora fanno parte dell'STL. Ora mi chiedo se questi casi in precedenza erano già stati effettivamente gestiti da RVO o ottimizzazioni del compilatore simili.

La mia domanda è quindi se potessi darmi un esempio reale di un pezzo di codice C ++ 98 che, senza modifiche, viene eseguito più velocemente utilizzando un compilatore che supporta le nuove funzionalità del linguaggio. Capisco che non è necessario un compilatore conforme allo standard per eseguire l'elissione della copia e solo per questo motivo spostare la semantica potrebbe portare a velocità, ma mi piacerebbe vedere un caso meno patologico, se vuoi.

EDIT: Giusto per essere chiari, non sto chiedendo se i nuovi compilatori siano più veloci dei vecchi compilatori, ma piuttosto se esiste un codice per cui l'aggiunta di -std = c ++ 14 ai miei flag di compilatore funzionerebbe più velocemente (evita le copie, ma se tu posso inventare qualsiasi altra cosa oltre a spostare la semantica, anche io sarei interessato)


3
Ricorda che l'ottimizzazione della copia e l'ottimizzazione del valore restituito vengono eseguite quando si costruisce un nuovo oggetto usando un costruttore di copie. Tuttavia, in un operatore di assegnazione di copie, non vi è alcuna elisione della copia (come può essere, dal momento che il compilatore non sa cosa fare con un oggetto già costruito che non è temporaneo). Pertanto, in tal caso, C ++ 11/14 vince alla grande, dandoti la possibilità di utilizzare un operatore di assegnazione di spostamenti. Per quanto riguarda la tua domanda, non penso che il codice C ++ 98 dovrebbe essere più veloce se compilato da un compilatore C ++ 11/14, forse è più veloce perché il compilatore è più recente.
vsoftco,

27
Anche il codice che utilizza la libreria standard è potenzialmente più veloce, anche se lo rendi pienamente compatibile con C ++ 98, perché in C ++ 11/14 la libreria sottostante utilizza la semantica di spostamento interna quando possibile. Quindi il codice che sembra identico in C ++ 98 e C ++ 11/14 sarà (possibilmente) più veloce in quest'ultimo caso, ogni volta che usi oggetti di libreria standard come vettori, elenchi ecc. E sposta la semantica fa la differenza.
vsoftco,

1
@vsoftco, Questo è il tipo di situazione a cui alludevo, ma non ho potuto fare un esempio: da quello che ricordo se devo definire il costruttore di copie, il costruttore di spostamenti non verrà generato automaticamente, il che ci lascia con classi molto semplici in cui RVO, credo, funziona sempre. Un'eccezione potrebbe essere qualcosa in congiunzione con i contenitori STL, in cui i costruttori di valori sono generati dall'implementatore della libreria (il che significa che non dovrei cambiare nulla nel codice per usare gli spostamenti).
Allargato il

le classi non devono essere semplici per non avere un costruttore di copie. Il C ++ prospera sulla semantica del valore e il costruttore di copie, l'operatore di assegnazione, il distruttore ecc. Dovrebbero essere l'eccezione.
sp2danny,

1
@Eric Grazie per il link, è stato interessante. Tuttavia, dopo averlo esaminato rapidamente, i vantaggi in termini di velocità sembrano derivare principalmente dall'aggiunta std::movee dallo spostamento di costruttori (che richiederebbero modifiche al codice esistente). L'unica cosa realmente correlata alla mia domanda era la frase "Ottieni vantaggi immediati in termini di velocità semplicemente ricompilando", che non è supportato da alcun esempio (menziona STL sulla stessa diapositiva, come ho fatto nella mia domanda, ma niente di specifico ). Stavo chiedendo alcuni esempi. Se sto leggendo le diapositive in modo errato, fammi sapere.
alarge

Risposte:


221

Sono a conoscenza di 5 categorie generali in cui la ricompilazione di un compilatore C ++ 03 come C ++ 11 può causare aumenti delle prestazioni illimitati che sono praticamente estranei alla qualità dell'implementazione. Queste sono tutte variazioni della semantica del movimento.

std::vector riallocare

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

ogni volta che il foobuffer viene riallocato in C ++ 03 viene copiato ogni vectorin bar.

In C ++ 11 sposta invece la bar::datas, che è sostanzialmente gratuita.

In questo caso, questo si basa su ottimizzazioni all'interno del stdcontenitore vector. In ogni caso di seguito, l'uso dei stdcontainer è dovuto al fatto che sono oggetti C ++ che hanno una movesemantica efficiente in C ++ 11 "automaticamente" quando si aggiorna il compilatore. Gli oggetti che non lo bloccano che contengono un stdcontenitore ereditano anche i movecostruttori automatici migliorati .

Fallimento NRVO

Quando NRVO (denominato ottimizzazione del valore restituito) fallisce, in C ++ 03 ricade sulla copia, su C ++ 11 ricade sulla mossa. I guasti di NRVO sono facili:

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

o anche:

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

Abbiamo tre valori: il valore restituito e due diversi valori all'interno della funzione. Elision consente di "unire" i valori all'interno della funzione con il valore restituito, ma non tra loro. Entrambi non possono essere uniti al valore di ritorno senza unirsi tra loro.

Il problema di base è che l'elissione della NRVO è fragile e che il codice con modifiche non vicine al returnsito può improvvisamente avere massicce riduzioni delle prestazioni in quel punto senza emettere diagnosi. Nella maggior parte dei casi di fallimento NRVO, C ++ 11 finisce con a move, mentre C ++ 03 finisce con una copia.

Restituzione di un argomento di funzione

Anche qui Elision è impossibile:

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

in C ++ 11 è economico: in C ++ 03 non c'è modo di evitare la copia. Gli argomenti alle funzioni non possono essere elisi con il valore restituito, poiché la durata e la posizione del parametro e del valore restituito sono gestite dal codice chiamante.

Tuttavia, C ++ 11 può spostarsi dall'uno all'altro. (In un esempio meno giocattolo, si potrebbe fare qualcosa per set).

push_back o insert

Infine, l'elisione nei container non avviene: ma C ++ 11 sovraccarica il rvalue move inserendo operatori, che salva le copie.

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 whateverviene creato un temporaneo , quindi viene copiato nel vettore v. std::stringVengono allocati 2 buffer, ognuno con dati identici e uno viene scartato.

In C ++ 11 whateverviene creato un temporaneo . Il whatever&& push_backsovraccarico è quindi movetemporaneo nel vettore v. Un std::stringbuffer viene allocato e spostato nel vettore. Un vuoto std::stringviene scartato.

Incarico

Rubato dalla risposta di @ Jarod42 qui sotto.

L'elisione non può avvenire con l'assegnazione, ma può spostarsi da.

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();

qui some_functionrestituisce un candidato da cui fuggire, ma poiché non viene utilizzato per costruire direttamente un oggetto, non può essere eluso. In C ++ 03, quanto sopra si traduce nella copia del contenuto del temporaneo some_value. In C ++ 11, viene spostato in some_value, che in pratica è gratuito.


Per l'effetto completo di quanto sopra, è necessario un compilatore che sintetizzi i costruttori di movimenti e l'assegnazione per te.

MSVC 2013 implementa i costruttori di spostamento in stdcontenitori, ma non sintetizza i costruttori di spostamento sui tipi.

Quindi i tipi che contengono se std::vectorsimili non ottengono tali miglioramenti in MSVC2013, ma inizieranno a ottenerli in MSVC2015.

clang e gcc hanno da tempo implementato costruttori di mosse implicite. Il compilatore Intel 2013 supporterà la generazione implicita di costruttori di spostamenti se si passa -Qoption,cpp,--gen_move_operations(non lo fanno per impostazione predefinita nel tentativo di essere cross-compatibili con MSVC2013).


1
@allarga si. Ma affinché un costruttore di spostamenti sia molte volte più efficiente di un costruttore di copie, di solito deve spostare le risorse invece di copiarle. Senza scrivere i propri costruttori di spostamento (e semplicemente ricompilare un programma C ++ 03), i stdcontenitori della libreria verranno tutti aggiornati con i movecostruttori "gratuitamente" e (se non lo si è bloccato) costrutti che utilizzano detti oggetti ( e detti oggetti) inizieranno a ottenere la costruzione di movimenti liberi in diverse situazioni. Molte di queste situazioni sono coperte dall'elisione in C ++ 03: non tutte.
Yakk - Adam Nevraumont il

5
Questa è una cattiva implementazione dell'ottimizzatore, quindi, poiché gli oggetti con nomi diversi restituiti non hanno una vita sovrapposta, RVO è teoricamente ancora possibile.
Ben Voigt,

2
@alarge Ci sono luoghi in cui l'elisione fallisce, come quando due oggetti con vite sovrapposte possono essere elisi in un terzo, ma non l'uno con l'altro. Quindi è necessario spostare in C ++ 11 e copiarlo in C ++ 03 (ignorando come se). Elision è spesso fragile in pratica. L'uso dei stdcontenitori di cui sopra è principalmente perché sono economici da spostare in modo costoso per copiare il tipo che si ottiene 'gratuitamente' in C ++ 11 quando si ricompila C ++ 03. È vector::resizeun'eccezione: utilizza movein C ++ 11.
Yakk - Adam Nevraumont,

27
Vedo solo 1 categoria generale che è la semantica di movimento e 5 casi speciali.
Johannes Schaub - litb

3
@sebro Capisco, non consideri "fa sì che i programmi non allocino molti 1000 di molte allocazioni di kilobyte, e invece sposta i puntatori" per essere sufficienti. Volete risultati a tempo. I microbench non sono più una prova dei miglioramenti delle prestazioni rispetto a una prova che fondamentalmente stai facendo di meno. A meno di alcune 100 applicazioni del mondo reale in una vasta gamma di settori che vengono profilati con attività del mondo reale, la profilazione non è davvero una prova. Ho preso vaghe affermazioni sulla "prestazione gratuita" e ho fatto loro fatti specifici sulle differenze nel comportamento del programma in C ++ 03 e C ++ 11.
Yakk - Adam Nevraumont,

46

se hai qualcosa come:

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

// some code

v = foo();

Ne hai una copia in C ++ 03, mentre in C ++ 11 hai un'assegnazione di traslochi. quindi hai l'ottimizzazione gratuita in quel caso.


4
@Yakk: come si verifica la copia elisione nell'assegnazione?
Jarod42,

2
@ Jarod42 Credo anche che in un compito non sia possibile copiare l'elissione, dal momento che il lato sinistro è già costruito e non c'è modo ragionevole per un compilatore di sapere cosa fare con i "vecchi" dati dopo aver rubato le risorse da destra lato della mano. Ma forse mi sbaglio, mi piacerebbe scoprire una volta per sempre la risposta. Copia elisione ha senso quando si copia il costrutto, poiché l'oggetto è "fresco" e non vi è alcun problema a decidere cosa fare con i vecchi dati. Per quanto ne so, l'unica eccezione è questa: "Le assegnazioni possono essere eluse solo in base alla regola as-if"
vsoftco,

4
Il buon codice C ++ 03 ha già fatto una mossa in questo caso, tramitefoo().swap(v);
Ben Voigt il

@BenVoigt certo, ma non tutto il codice è ottimizzato e non tutti i punti in cui ciò accade sono facili da raggiungere.
Yakk - Adam Nevraumont il

La copia ellisse può funzionare in un compito, come dice @BenVoigt. Il termine migliore è RVO (ottimizzazione del valore di ritorno) e funziona solo se foo () è stato implementato in questo modo.
DrumM,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.