2015-07-09 9 views
11

Recentemente ho bisogno di un lambda che ha catturato più variabili locali di riferimento, così ho fatto un frammento test per indagare la sua efficienza, e compilato con -O3 usando clang 3.6:La forza standard C++ acquisita per riferimento delle variabili locali è inefficiente?

void do_something_with(void*); 

void test() 
{ 
    int a = 0, b = 0, c = 0; 

    auto func = [&]() { 
     a++; 
     b++; 
     c++; 
    }; 

    do_something_with((void*)&func); 
} 

movl $0x0,0x24(%rsp) 
movl $0x0,0x20(%rsp) 
movl $0x0,0x1c(%rsp) 

lea 0x24(%rsp),%rax 
mov %rax,(%rsp) 
lea 0x20(%rsp),%rax 
mov %rax,0x8(%rsp) 
lea 0x1c(%rsp),%rax 
mov %rax,0x10(%rsp) 

lea (%rsp),%rdi 
callq ... 

Chiaramente la lambda ha solo bisogno dell'indirizzo di una delle variabili, da cui tutti gli altri potrebbero essere ottenuti mediante indirizzamento relativo.

Invece, il compilatore ha creato una struttura sullo stack contenente puntatori a ogni variabile locale e quindi ha passato l'indirizzo della struttura al lambda. E 'molto nello stesso modo come se avessi scritto:

int a = 0, b = 0, c = 0; 

struct X 
{ 
    int *pa, *pb, *pc; 
}; 

X x = {&a, &b, &c}; 

auto func = [p = &x]() { 
    (*p->pa)++; 
    (*p->pb)++; 
    (*p->pc)++; 
}; 

Questo è inefficiente per vari motivi, ma la maggior parte preoccupante perché potrebbe portare a heap-allocazione se troppe variabili vengono catturati.

Le mie domande:

  1. il fatto che sia clang e gcc fanno questo a -O3 mi fa sospettare che qualcosa nella norma obbliga in realtà le chiusure da attuare in maniera inefficiente. È questo il caso?

  2. Se sì, allora per quale motivo? Non può essere per la compatibilità binaria di lambda tra i compilatori, poiché ogni codice che conosca il tipo di lambda è garantito che giace nella stessa unità di traduzione.

  3. In caso contrario, perché manca questo ottimizzazione da due importanti compilatori?


EDIT:
Ecco un esempio di codice più efficiente che mi piacerebbe avere visto dal compilatore. Questo codice utilizza meno spazio di stack, lambda ora esegue un solo indicatore indiretto invece di due, e la dimensione del lambda non cresce nel numero di variabili catturati:

struct X 
{ 
    int a = 0, b = 0, c = 0; 
} x; 

auto func = [&x]() { 
    x.a++; 
    x.b++; 
    x.c++; 
}; 

movl $0x0,0x8(%rsp) 
movl $0x0,0xc(%rsp) 
movl $0x0,0x10(%rsp) 

lea 0x8(%rsp),%rax 
mov %rax,(%rsp) 

lea (%rsp),%rdi 
callq ... 
+2

Non riesco a fornire informazioni di supporto dal bancomat standard, ma secondo http://en.cppreference.com/w/cpp/language/lambda 'Per le entità che vengono catturate per riferimento [...] non è specificato se membri di dati aggiuntivi sono dichiarati nel tipo di chiusura. Sembra che non sia proibito ottimizzarlo. –

+1

Per quello che vale, se si scrive C++ idiomatico piuttosto che fare affidamento su cast non specificati tra i puntatori di funzione e oggetto, probabilmente andrà bene. ([Esempio] (https://goo.gl/vLIqde)) –

+0

@KerrekSB: sapevo che qualcuno si sarebbe lamentato del cast 'void *' - Devo usare il lambda per impedire che tutto venga ottimizzato, ma usando std :: function crea molto rumore nell'output dell'assieme. 'Uintptr_t' sarebbe meglio? EDIT: nel tuo esempio, tutto è stato infatti costantemente propagato e in linea. – PBS

risposta

6

Sembra non specificata comportamento. Il seguente paragrafo dalla sezione C++14 draft standard: N39365.1.2 Espressioni lambda [expr.prim.lambda] mi fa pensare questo:

Un'entità viene catturato con riferimento se è implicitamente o esplicitamente catturato ma non catturato da copia. Non è specificato se i membri di dati non statici non nominati sono dichiarati nel tipo di chiusura per le entità acquisite per riferimento. [...]

che diverso per le entità catturate dalla copia:

Ogni id-espressione all'interno del complesso-dichiarazione di un lambda-espressione che è un ODR-uso (3.2) di un'entità catturato da La copia viene trasformata in un accesso ai corrispondenti dati senza nome membro del tipo di chiusura.

Grazie a dyp per aver indicato alcuni documenti rilevanti che in qualche modo ho perso. Sembra defect report 750: Implementation constraints on reference-only closure objects fornisce il razionale per l'attuale formulazione, e dice:

Consideriamo un esempio come:

void f(vector<double> vec) { 
    double x, y, z; 
    fancy_algorithm(vec, [&]() { /* use x, y, and z in various ways */ }); 
} 

5.1.2 [expr.prim.lambda] comma 8 prevede che la chiusura la classe per questo lambda avrà tre membri di riferimento e il paragrafo 12 richiede che sia derivato da std :: reference_closure, che implica due membri di puntatori aggiuntivi . Sebbene 8.3.2 [dcl.ref] paragrafo 4 consenta di implementare un riferimento senza allocazione di memoria, gli attuali ABI richiedono che i riferimenti siano implementati come puntatori. L'effetto pratico di questi requisiti è che l'oggetto di chiusura per espressione lambda conterrà cinque puntatori. Se non fosse per questi requisiti , tuttavia, sarebbe possibile implementare l'oggetto di chiusura come un singolo puntatore al frame dello stack, generando gli accessi nell'operatore di chiamata funzione come offset rispetto al puntatore del frame . Le specifiche attuali sono troppo ristrette.

che echos tuoi punti esatti di permettere potenziali ottimizzazioni ed è stato attuato come parte di N2927 che include quanto segue:

La nuova formulazione non specifica alcun membro di riscrittura o di chiusura per la cattura "per riferimento" . Gli utilizzi di entità acquisite "per riferimento" influiscono sulle entità originali e il meccanismo per raggiungere questo valore è lasciato interamente all'implementazione.

+1

Vedere [CWG 753] (http://www.open-std.org/JTC1/SC22/WG21/docs/cwg_defects.html#753), tuttavia vedere anche [CWG 2011] (http: //www.open- std.org/JTC1/SC22/WG21/docs/cwg_active.html#2011) – dyp

+0

@dyp In qualche modo mi sono perso nelle mie ricerche, non so come. –

+0

@dyp in realtà intendevi dire [dr 750] (http://www.open-std.org/JTC1/SC22/WG21/docs/cwg_defects.html#750)? –

Problemi correlati