2016-04-01 39 views
25

Un compilatore può eseguire la conversione automatica da valore a lvalue-to-rvalue se può dimostrare che il lvalue non verrà più utilizzato? Ecco un esempio per chiarire cosa intendo:Un compilatore ottimizzante può aggiungere std :: move?

void Foo(vector<int> values) { ...} 

void Bar() { 
    vector<int> my_values {1, 2, 3}; 
    Foo(my_values); // may the compiler pretend I used std::move here? 
} 

Se un std::move si aggiunge alla linea commentato, quindi il vettore può essere spostato nel parametro s' Foo, piuttosto che copiare. Tuttavia, come scritto, non ho usato std::move.

È abbastanza semplice dimostrare in modo statico che my_values ​​non verrà utilizzato dopo la riga commentata. Quindi il compilatore ha il permesso di spostare il vettore, o è necessario copiarlo?

risposta

32

Il compilatore deve comportarsi come se la copia fosse avvenuta dallo vector alla chiamata di Foo.

Se il compilatore in grado di dimostrare che ci sono è un comportamento valida astratto macchina senza effetti collaterali osservabili (all'interno del comportamento astratto della macchina, non in un vero e proprio computer!), Che prevede lo spostamento del std::vector in Foo, si può fare questo.

Nel caso precedente, questo (spostamento non ha effetti secondari visibili macchina astratta) è vero; il compilatore potrebbe non essere in grado di dimostrarlo, comunque.

Il comportamento possibilmente osservabile quando si copia un std::vector<T> è:

  • Invocare costruttori di copia sugli elementi. Non è possibile osservare
  • invocando il valore predefinito std::allocator<> in momenti diversi. Questo invoca ::new e (forse) In ogni caso, ::new e ::delete non è stato sostituito nel programma di cui sopra, quindi non è possibile osservare questo sotto lo standard.
  • Chiamare il distruttore di T più volte su oggetti diversi. Non osservabile con int.
  • vector non vuoto dopo la chiamata a . Nessuno lo esamina, quindi è vuoto come se non lo fosse.
  • Riferimenti o puntatori o iteratori agli elementi del vettore esterno diversi da quelli interni. Nessun riferimento, vettori o puntatori vengono portati agli elementi del vettore al di fuori dello Foo.

Mentre si può dire "ma cosa succede se il sistema non ha memoria, e il vettore è grande, non è che osservabile?":

La macchina astratta non dispone di un "out of memory "condizione, ha semplicemente l'allocazione a volte in errore (lancio di std::bad_alloc) per motivi non vincolati. E 'non fallire è un comportamento valido della macchina astratta, e non fallire non allocando memoria (effettiva) (sul computer reale) è anche valido, fintanto che la non esistenza della memoria non ha effetti collaterali osservabili.

Un po 'più caso giocattolo:

int main() { 
    int* x = new int[std::size_t(-1)]; 
    delete[] x; 
} 

mentre questo programma alloca chiaramente modo troppa memoria, il compilatore è libero di non assegnare nulla.

Possiamo andare oltre. Anche:

int main() { 
    int* x = new int[std::size_t(-1)]; 
    x[std::size_t(-2)] = 2; 
    std::cout << x[std::size_t(-2)] << '\n'; 
    delete[] x; 
} 

può essere convertito in std::cout << 2 << '\n';. Quel buffer di grandi dimensioni deve esistere in modo astratto, ma finché il tuo programma "reale" si comporta come se la macchina astratta lo dovesse, in realtà non deve allocarlo.

Sfortunatamente, farlo a qualsiasi livello ragionevole è difficile. Ci sono molti modi in cui le informazioni possono fuoriuscire da un programma C++. Quindi affidarsi a tali ottimizzazioni (anche se accadono) non finirà bene.


C'era un po 'di cose su di coalescenza chiamate a new che potrebbero confondere il problema, sono incerto se sarebbe legale di saltare le chiamate, anche se ci fosse un sostituito ::new.


Un fatto importante è che ci sono situazioni che il compilatore è non tenuto a comportarsi come-se ci fosse una copia, anche se std::move non era chiamato.

Quando si return una variabile locale da una funzione in una linea che assomiglia return X; e X è l'identificatore, e quella variabile locale è della durata di memorizzazione automatica (sullo stack), l'operazione è implicitamente una mossa, e la il compilatore (se possibile) può escludere l'esistenza del valore di ritorno e della variabile locale in un oggetto (e persino omettere lo move).

Lo stesso è vero quando si costruisce un oggetto da un temporaneo - l'operazione è implicitamente una mossa (poiché è vincolante per un rvalue) e può eliminare completamente la mossa.

In entrambi questi casi, il compilatore è tenuto a trattarlo come una mossa (non una copia), e può elidere lo spostamento.

std::vector<int> foo() { 
    std::vector<int> x = {1,2,3,4}; 
    return x; 
} 

che x non ha std::move, eppure viene spostato nel valore di ritorno, e tale operazione può essere tralasciata (x e il valore di ritorno può essere trasformato in un oggetto).

Questo:

std::vector<int> foo() { 
    std::vector<int> x = {1,2,3,4}; 
    return std::move(x); 
} 

blocchi elisione, come fa questo:

std::vector<int> foo(std::vector<int> x) { 
    return x; 
} 

e possiamo anche bloccare il movimento:

std::vector<int> foo() { 
    std::vector<int> x = {1,2,3,4}; 
    return (std::vector<int> const&)x; 
} 

o anche:

std::vector<int> foo() { 
    std::vector<int> x = {1,2,3,4}; 
    return 0,x; 
} 

come le regole per lo spostamento implicito sono intenzionalmente fragili. (0,x è un uso del tanto diffamato operatore ,).

Ora, non è consigliabile fare affidamento sul movimento implicito in casi come questo ultimo basato su ,: il comitato standard ha già modificato un caso implicito di copia in una mossa implicita poiché è stata aggiunta la mossa implicita alla lingua perché lo hanno ritenuto innocuo (dove la funzione restituisce un tipo A con un A(B&&) ctor e l'istruzione di reso è return b; dove b è di tipo B; in C++ 11 versione che ne ha fatto una copia, ora fa una mossa.) Ulteriori non è possibile escludere l'espansione implicita del movimento: il casting esplicito su un const& è probabilmente il modo più affidabile per prevenirlo ora e in futuro.

+1

Avete dei collegamenti dove posso imparare di più su come funziona o come viene definita la macchina astratta? – vu1p3n0x

+2

@ vu1p3n0x Trova una copia dello standard C++? [Ecco una bozza] (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3690.pdf). [Eccone un altro] (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3376.pdf). Il comportamento di C++ è specificato nello standard in termini di una macchina astratta. – Yakk

+0

Risposta perfetta. –

2

In questo caso, il compilatore potrebbe uscire da my_values. Questo perché non causa alcuna differenza nel comportamento osservabile .

Citando definizione ++ del C standard di comportamento osservabile:

dei minimi requisiti su conforme attuazione sono:

  • accesso agli oggetti volatili vengono valutati rigorosamente secondo le regole della macchina astratta.
  • Al termine del programma, tutti i dati scritti nei file devono essere identici a uno dei possibili risultati che l'esecuzione del programma secondo la semantica astratta avrebbe prodotto.
  • Le dinamiche di input e output dei dispositivi interattivi devono essere eseguite in modo tale che l'output di prompt venga effettivamente consegnato prima che un programma attenda l'input. Ciò che costituisce un dispositivo interattivo è definito dall'implementazione.

Interpretare questo un po ': "File" qui include il flusso di output standard, e per le chiamate di funzioni che non sono definiti dalla ++ standard C (ad esempio chiamate del sistema operativo, o chiamate a librerie di terze parti), è si deve presumere che tali funzioni possano scrivere su un file, quindi un corollario di ciò è che anche le chiamate di funzione non standard devono essere considerate come un comportamento osservabile.

Tuttavia, il codice (come mostrato) non ha variabili volatile e nessuna chiamata a funzioni non standard. Quindi le due versioni (move o not-move) devono avere un comportamento osservabile identico e quindi il compilatore potrebbe fare l'uno o l'altro (o anche ottimizzare completamente la funzione, ecc.)

In pratica, naturalmente, non è generalmente così facile per un compilatore dimostrare che non si verificano chiamate di funzione non standard, quindi molte opportunità di ottimizzazione come questa sono perse. Ad esempio, in questo caso il compilatore potrebbe non sapere ancora se il valore predefinito ::operator new è stato sostituito con una funzione che genera output.