L'uso dell'auto di C ++ 11 può migliorare le prestazioni?


230

Posso capire perché il autotipo in C ++ 11 migliora la correttezza e la manutenibilità. Ho letto che può anche migliorare le prestazioni ( Almost Always Auto di Herb Sutter), ma mi manca una buona spiegazione.

  • Come può automigliorare le prestazioni?
  • Qualcuno può fare un esempio?

5
Vedi Herbsutter.com/2013/06/13/… che parla di come evitare conversioni implicite accidentali, ad esempio da gadget a widget. Non è un problema comune.
Jonathan Wakely,

42
Accettate "rende meno probabile la pessimizzazione involontaria" come un miglioramento delle prestazioni?
5gon12eder

1
Prestazioni di pulizia del codice solo in futuro, forse
Croll,

Abbiamo bisogno di una risposta breve: No se sei bravo. Può prevenire errori "noobish". Il C ++ ha una curva di apprendimento che uccide coloro che non ce la fanno dopo tutto.
Alec Teal,

Risposte:


309

autopuò aiutare le prestazioni evitando conversioni implicite silenziose . Un esempio che trovo convincente è il seguente.

std::map<Key, Val> m;
// ...

for (std::pair<Key, Val> const& item : m) {
    // do stuff
}

Vedi il bug? Eccoci qui, pensando che stiamo prendendo elegantemente ogni elemento nella mappa come riferimento costante e usando la nuova gamma-for expression per chiarire il nostro intento, ma in realtà stiamo copiando ogni elemento. Questo perché std::map<Key, Val>::value_typeè std::pair<const Key, Val>, non è std::pair<Key, Val>. Pertanto, quando (implicitamente) abbiamo:

std::pair<Key, Val> const& item = *iter;

Invece di prendere un riferimento a un oggetto esistente e lasciarlo a quello, dobbiamo fare una conversione di tipo. È consentito prendere un riferimento const a un oggetto (o temporaneo) di un tipo diverso purché sia ​​disponibile una conversione implicita, ad esempio:

int const& i = 2.0; // perfectly OK

La conversione del tipo è una conversione implicita consentita per lo stesso motivo per cui è possibile convertire un const Keyin Key, ma per consentirlo dobbiamo costruire un temporaneo del nuovo tipo. Pertanto, in modo efficace il nostro loop fa:

std::pair<Key, Val> __tmp = *iter;       // construct a temporary of the correct type
std::pair<Key, Val> const& item = __tmp; // then, take a reference to it

(Certo, in realtà non c'è un __tmpoggetto, è solo lì per l'illustrazione, in realtà il temporaneo senza nome è solo legato alla itemsua vita).

Sto solo cambiando in:

for (auto const& item : m) {
    // do stuff
}

ci ha appena salvato un sacco di copie - ora il tipo a cui fa riferimento corrisponde al tipo di inizializzatore, quindi non è necessaria alcuna conversione temporanea, possiamo solo fare un riferimento diretto.


19
@Barry Puoi spiegare perché il compilatore avrebbe felicemente fatto delle copie invece di lamentarsi di provare a trattare un std::pair<const Key, Val> const &come std::pair<Key, Val> const &? Nuovo in C ++ 11, non sono sicuro di come range-for e autogioca in questo.
Agop,

@Barry Grazie per la spiegazione. Questo è il pezzo che mi mancava - per qualche motivo, ho pensato che non si potesse avere un riferimento costante a un temporaneo. Ma certo che puoi - cesserà di esistere alla fine del suo campo di applicazione.
Agop,

@barry Ti capisco, ma il problema è che non esiste una risposta che copra tutti i motivi per utilizzare quell'aumento autodelle prestazioni. Quindi lo scriverò con le mie parole qui sotto.
Yakk - Adam Nevraumont,

38
Continuo a non pensare che questa sia la prova che " automigliora le prestazioni". È solo un esempio che " autoaiuta a prevenire errori del programmatore che distruggono le prestazioni". Sottolineo che esiste una distinzione sottile ma importante tra i due. Ancora, +1.
Razze di leggerezza in orbita,

70

Poiché autodeduce il tipo dell'espressione di inizializzazione, non è richiesta alcuna conversione di tipo. Combinato con algoritmi basati su modelli, ciò significa che puoi ottenere un calcolo più diretto rispetto a se stessi per creare un tipo, soprattutto quando hai a che fare con espressioni di cui non puoi nominare il tipo!

Un esempio tipico viene da (ab) utilizzando std::function:

std::function<bool(T, T)> cmp1 = std::bind(f, _2, 10, _1);  // bad
auto cmp2 = std::bind(f, _2, 10, _1);                       // good
auto cmp3 = [](T a, T b){ return f(b, 10, a); };            // also good

std::stable_partition(begin(x), end(x), cmp?);

Con cmp2e cmp3, l'intero algoritmo può incorporare la chiamata di confronto, mentre se costruisci un std::functionoggetto, non solo la chiamata non può essere incorporata, ma devi anche passare attraverso la ricerca polimorfica all'interno cancellato dal tipo del wrapper di funzione.

Un'altra variante di questo tema è che puoi dire:

auto && f = MakeAThing();

Questo è sempre un riferimento, associato al valore dell'espressione della chiamata di funzione e non costruisce mai oggetti aggiuntivi. Se non si conosceva il tipo di valore restituito, si potrebbe essere costretti a costruire un nuovo oggetto (forse come temporaneo) tramite qualcosa di simile T && f = MakeAThing(). (Inoltre, auto &&funziona anche quando il tipo restituito non è mobile e il valore restituito è un valore.)


Quindi questo è il motivo "evitare la cancellazione del tipo" da usare auto. L'altra variante è "evitare copie accidentali", ma necessita di abbellimenti; perché autoti dà la velocità semplicemente digitando il tipo lì? (Penso che la risposta sia "tu sbagli il tipo, e si converte silenziosamente") Il che lo rende un esempio meno ben spiegato della risposta di Barry, no? Vale a dire, ci sono due casi di base: auto per evitare la cancellazione del tipo e auto per evitare errori di tipo silenzioso che si convertono accidentalmente, entrambi i quali hanno un costo di runtime.
Yakk - Adam Nevraumont,

2
"non solo la chiamata non può essere integrata" - perché allora? Vuoi dire che in linea di principio qualcosa impedisce che la chiamata venga devirtualizzata dopo l'analisi del flusso di dati se le relative specializzazioni di std::bind, std::functione std::stable_partitionsono state tutte incorporate? O semplicemente che in pratica nessun compilatore C ++ si inserirà in modo abbastanza aggressivo per risolvere il caos?
Steve Jessop,

@SteveJessop: Soprattutto quest'ultimo - dopo aver attraversato il std::functioncostruttore, sarà molto complesso vedere attraverso la chiamata effettiva, specialmente con ottimizzazioni di piccole funzioni (quindi in realtà non vuoi devirtualizzazione). Naturalmente in linea di principio tutto è come se ...
Kerrek SB,

41

Esistono due categorie.

autopuò evitare la cancellazione del tipo. Esistono tipi innominabili (come lambdas) e tipi quasi innegabili (come il risultato di std::bindo altri modelli di espressioni come cose).

Senza auto, devi finire per cancellare i dati fino a qualcosa di simile std::function. La cancellazione del tipo ha dei costi.

std::function<void()> task1 = []{std::cout << "hello";};
auto task2 = []{std::cout << " world\n";};

task1ha un overhead di cancellazione del tipo - una possibile allocazione dell'heap, difficoltà a incorporarlo e un overhead di invocazione della tabella delle funzioni virtuali. task2non ha nessuno. Le lambda necessitano di deduzioni automatiche o di altro tipo per la memorizzazione senza cancellazione del tipo; altri tipi possono essere così complessi da averne solo bisogno in pratica.

In secondo luogo, puoi sbagliare i tipi. In alcuni casi, il tipo sbagliato funzionerà perfettamente, ma ne causerà una copia.

Foo const& f = expression();

compilerà se expression()restituisce Bar const&o Baro addirittura Bar&, da dove Foopuò essere costruito Bar. FooVerrà creato un temporaneo , quindi associato fe la sua durata verrà estesa fino a quando non fscompare.

Il programmatore potrebbe aver inteso Bar const& fe non inteso fare una copia lì, ma una copia viene fatta indipendentemente.

L'esempio più comune è il tipo di *std::map<A,B>::const_iterator, che std::pair<A const, B> const&non lo è std::pair<A,B> const&, ma l'errore è una categoria di errori che costano silenziosamente le prestazioni. Puoi costruire un std::pair<A, B>da a std::pair<const A, B>. (La chiave su una mappa è const, perché modificarla è una cattiva idea)

Sia @Barry che @KerrekSB hanno prima illustrato questi due principi nelle loro risposte. Questo è semplicemente un tentativo di evidenziare le due questioni in una risposta, con una formulazione che mira al problema piuttosto che essere incentrata sull'esempio.


9

Le tre risposte esistenti forniscono esempi in cui l'utilizzo autoaiuta "rende meno probabile la pessimizzazione involontaria" efficacemente facendolo "migliorare le prestazioni".

C'è un rovescio della medaglia. L'utilizzo autocon oggetti che dispongono di operatori che non restituiscono l'oggetto di base può comportare un codice errato (ancora compilabile e eseguibile). Ad esempio, questa domanda chiede come l'utilizzo abbia autodato risultati diversi (errati) utilizzando la libreria Eigen, ovvero le seguenti righe

const auto    resAuto    = Ha + Vector3(0.,0.,j * 2.567);
const Vector3 resVector3 = Ha + Vector3(0.,0.,j * 2.567);

std::cout << "resAuto = " << resAuto <<std::endl;
std::cout << "resVector3 = " << resVector3 <<std::endl;

ha prodotto un output diverso. Certo, ciò è dovuto principalmente alla valutazione pigra di Eigens, ma quel codice è / dovrebbe essere trasparente per l'utente (libreria).

Sebbene le prestazioni non siano state notevolmente influenzate qui, l'uso autoper evitare la pessimizzazione involontaria potrebbe essere classificato come ottimizzazione prematura, o almeno sbagliato;).


1
Aggiunta la domanda opposta: stackoverflow.com/questions/38415831/…
Leon
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.