22

Fino ad oggi, avevo sempre pensato che i compilatori decenti convertissero automaticamente la struct pass-by-value in pass-by-reference se la struttura fosse abbastanza grande da rendere quest'ultima più veloce. Per quanto ne so, sembra un'ottima ottimizzazione. Tuttavia, per soddisfare la mia curiosità sul fatto che ciò accada realmente, ho creato un semplice caso di test sia in C++ che in D e ho esaminato l'output di GCC e Digital Mars D. Entrambi hanno insistito sul passaggio di strutture a 32 byte in base al valore quando tutte le la funzione in questione è stata aggiungere i membri e restituire i valori, senza alcuna modifica della struttura passata. La versione C++ è riportata di seguito.Perché non viene passata la struttura per riferimento a un'ottimizzazione comune?

#include "iostream.h" 

struct S { 
    int i, j, k, l, m, n, o, p; 
}; 

int foo(S s) { 
    return s.i + s.j + s.k + s.l + s.m + s.n + s.o + s.p; 
} 

int main() { 
    S s; 
    int bar = foo(s); 
    cout << bar; 
} 

La mia domanda è, perché diavolo non sarebbe qualcosa di simile essere ottimizzato dal compilatore di passare per riferimento in realtà invece di spingere tutti quei int s nello stack?

Nota: interruttori del compilatore utilizzati: GCC -O2 (-O3 inline foo().), DMD -O -line -release.

Modifica: Ovviamente, nel caso generale la semantica del pass-by-value rispetto al pass-by-reference non sarà la stessa, ad esempio se i costruttori di copia sono coinvolti o la struttura originale viene modificata nel callee . Tuttavia, in molti scenari del mondo reale, la semantica sarà identica in termini di comportamento osservabile. Questi sono i casi che sto chiedendo.

risposta

22

Non dimenticate che in C/C++ il compilatore deve essere in grado di compilare una chiamata a una funzione basato solo sulla dichiarazione di funzione.

Dato che i chiamanti potrebbero utilizzare solo tali informazioni, non c'è modo per un compilatore di compilare la funzione per sfruttare l'ottimizzazione di cui si sta parlando. Il chiamante non può sapere che la funzione non modificherà nulla e quindi non può passare da ref. Poiché alcuni chiamanti potrebbero passare per valore a causa della mancanza di informazioni dettagliate, la funzione deve essere compilata assumendo il valore pass-by e tutti devono passare per valore.

Si noti che anche se si contrassegnava il parametro come "const", il compilatore non può ancora eseguire l'ottimizzazione, poiché la funzione potrebbe trovarsi distesa e rigettare la costanza (ciò è consentito e ben definito purché il l'oggetto che viene passato non è in realtà const).

Penso che per le funzioni statiche (o quelle in uno spazio dei nomi anonimo), il compilatore possa eventualmente realizzare l'ottimizzazione di cui si sta parlando, poiché la funzione non ha un collegamento esterno. Finché l'indirizzo della funzione non viene passato a un'altra routine o memorizzato in un puntatore, non dovrebbe essere richiamabile da un altro codice. In questo caso il compilatore potrebbe avere una conoscenza completa di tutti i chiamanti, quindi suppongo che potrebbe fare l'ottimizzazione.

Non sono sicuro che ce ne siano (in realtà, sarei sorpreso se qualcuno lo fa, poiché probabilmente non potrebbe essere applicato molto spesso).

Ovviamente, come programmatore (quando si utilizza C++) è possibile forzare il compilatore a eseguire questa ottimizzazione utilizzando i parametri const& quando possibile. So che stai chiedendo perché il compilatore non può farlo automaticamente, ma suppongo che questa sia la prossima cosa migliore.

+0

Quando si esegue l'ottimizzazione del link-time, la generazione di un time code del collegamento a.k.a. o l'intera compilazione del programma, il compilatore non ha bisogno di compilare la chiamata solo in base alla dichiarazione. Ha una visione completa di ciò che sta succedendo. Per la compilazione di applicazioni embedded sensibili alle dimensioni e alla velocità, la generazione del codice del tempo di collegamento è l'unico modo per andare comunque. –

10

Una risposta è che il compilatore dovrebbe rilevare che il metodo chiamato non modifica il contenuto della struttura in alcun modo. In tal caso, l'effetto del passaggio per riferimento sarebbe diverso da quello del passaggio per valore.

+2

Si potrebbe adottare la semantica copy-on-write per tali strutture. –

+0

Sì, suppongo che potresti avere l'ABI per la tua lingua dichiarare che le strutture sono passate per riferimento, ma trasformate in copie se mai il chiamato tenta di modificarle. Sembra comunque un po 'complicato: è necessario utilizzare tutte le complicate analisi alias del caso peggiore per accertare che siano garantite per essere intoccate. Più facile avere regole esplicite nella lingua - se non vuoi passare una struttura per rallentarti, crea un riferimento o un puntatore. – Edmund

11

Il problema è che stai chiedendo al compilatore di prendere una decisione sull'intenzione del codice utente. Forse voglio che la mia struttura super grande venga passata in base al valore in modo da poter fare qualcosa nel costruttore di copie. Credetemi, qualcuno là fuori ha qualcosa che hanno validamente bisogno di essere chiamato in un costruttore di copie proprio per questo scenario. Passare a a da ref aggirerà il costruttore di copie.

Avere questo essere una decisione generata dal compilatore sarebbe una cattiva idea. La ragione è che rende impossibile ragionare sul flusso del tuo codice. Non puoi guardare una chiamata e sapere che cosa farà esattamente. Devi a) conoscere il codice e b) indovinare l'ottimizzazione del compilatore.

4

È vero che i compilatori in alcune lingue potrebbero eseguire questa operazione se hanno accesso alla funzione chiamata e se possono assumere che la funzione chiamata non cambierà. A volte si parla di ottimizzazione globale e sembra probabile che alcuni compilatori C o C++ possano effettivamente ottimizzare casi come questo - più probabilmente inserendo il codice per una funzione così banale.

3

Ci sono molti motivi per passare in base al valore, e avendo il compilatore ottimizzare la tua intenzione potrebbe violare il codice.

Esempio, se la funzione chiamata modifica la struttura in alcun modo. Se avevi intenzione di restituire i risultati al chiamante, avresti passato un puntatore/riferimento o lo avresti restituito tu stesso.

Quello che stai chiedendo al compilatore di fare è modificare il comportamento del tuo codice, che sarebbe considerato un bug del compilatore.

Se si desidera eseguire l'ottimizzazione e passare per riferimento, modificare in qualsiasi modo le definizioni di funzione/metodo esistenti di qualcuno per accettare riferimenti; non è poi così difficile. Potresti essere sorpreso dalla rottura che causi senza rendertene conto.

2

Passaggio da base al valore per riferimento da cambierà la firma della funzione. Se la funzione non è statica, ciò causerebbe errori di collegamento per altre unità di compilazione che non sono a conoscenza dell'ottimizzazione eseguita.
In effetti, l'unico modo per effettuare tale ottimizzazione è una sorta di fase di ottimizzazione globale post-link. Questi sono notoriamente difficili da fare, ma alcuni compilatori li fanno in una certa misura.

1

Beh, la risposta banale è che la posizione della struct in memoria è diverso, e quindi i dati che sta passando è diverso. La risposta più complessa, penso, è il threading.

il compilatore avrebbe bisogno di rilevare a) che pippo non modifica la struct; b) che foo non esegue alcun calcolo sulla posizione fisica degli elementi della struct; E c) che il chiamante, o un altro thread generato dal chiamante, non modifica la struttura prima dell'esecuzione di foo.

Nel tuo esempio, è concepibile che il compilatore possa fare queste cose - ma la memoria salvata è irrilevante e probabilmente non vale la pena di indovinare. Cosa succede se si esegue lo stesso programma con una struttura che contiene due milioni di elementi?

2

Penso che questa sia sicuramente un'ottimizzazione che potresti implementare (sotto alcune ipotesi, vedi l'ultimo paragrafo), ma non mi è chiaro che sarebbe proficuo. Invece di spingere gli argomenti nello stack (o passarli attraverso i registri, a seconda della convenzione di chiamata), si dovrebbe premere un puntatore attraverso il quale leggere i valori. Questo extraindirizzamento sarebbe costato cicli. Richiederebbe inoltre che l'argomento passato sia in memoria (quindi è possibile puntarlo) anziché nei registri. Sarebbe utile solo se i record passati avessero molti campi e la funzione che riceve il record ne leggesse solo alcuni. I cicli extra sprecati per via indiretta dovrebbero compensare i cicli non sprecati spingendo campi non necessari.

Potreste essere sorpresi dal fatto che l'ottimizzazione inversa, argument promotion, sia effettivamente implementata in LLVM. Converte un argomento di riferimento in un argomento di valore (o un aggregato in scalari) per le funzioni interne con un numero ridotto di campi da cui viene solo letto. Questo è particolarmente utile per le lingue che passano quasi tutto per riferimento. Se segui questo con dead argument elimination, non devi passare anche i campi che non vengono toccati.

Sostiene che le ottimizzazioni che cambiano il modo in cui viene chiamata una funzione possono funzionare solo quando la funzione ottimizzata è interna al modulo che si sta compilando (si ottiene dichiarando una funzione static in C e con i modelli in C++). L'ottimizzatore deve correggere non solo la funzione ma anche tutti i punti di chiamata. Ciò rende tali ottimizzazioni abbastanza limitate nell'ambito, a meno che non le facciate al momento del collegamento. Inoltre, l'ottimizzazione non verrebbe mai chiamata quando è coinvolto un costruttore di copia (come hanno menzionato altri poster) perché potrebbe potenzialmente cambiare la semantica del programma, cosa che un buon ottimizzatore non dovrebbe mai fare.

2

Il pass-by-reference è solo zucchero sintattico per il passaggio all'indice/puntatore. Quindi la funzione deve implicitamente rinviare un puntatore per leggere il valore del parametro. Dereferenziare il puntatore potrebbe essere più costoso (se in un ciclo), quindi la copia della struttura per copia per valore.

Ancora più importante, come altri hanno menzionato, il pass-by-reference ha una semantica diversa rispetto al pass-by-value. const riferimenti non significa che il valore di riferimento non cambia. altre chiamate di funzione potrebbero modificare il valore di riferimento.

+0

IIRC il commento const non è valido in D2.0. Il compilatore/è/libero di non assumere modifiche tramite riferimenti const. – BCS

1

il compilatore avrebbe bisogno di essere sicuri che lo struct che viene passato (come indicato nel codice chiamante) in non viene modificato

double x; // using non structs, oh-well 

void Foo(double d) 
{ 
     x += d; // ok 
     x += d; // Oops 
} 

void main() 
{ 
    x = 1; 
    Foo(x); 
} 
Problemi correlati