Questo articolo è la traduzione di un mio post del 2016 intitolato The power of devirtualization.
La devirtualizzazione accade quando il compilatore può decidere a tempo di compilazione quale funzione chiamare, e quindi produrre una chiamata diretta (al posto di una indiretta), o addirittura mettere il codice in linea ed evitare la chiamata. Questo accade anche in C++98/03, ma spesso richiede l’abilitazione di ottimizzazioni o generazione del codice a tempo di linking per permettere al compilatore di capire se è possibile evitare la chiamata indiretta, e anche in questo caso è piuttosto difficile che la rimozione dell’indirezione accada.
Consideriamo questo codice pre-C++11:
class A {
public:
virtual int value() { return 1; }
};
class B : public A {
public:
int value() { return 2; }
};
int test(B* b) {
return b->value() + 11;
}
Durante la compilazione della chiamata al metodo value() nella funzione test(B* b) il compilatore in generale non può assumere se il tipo reale di b sia B oppure una sottoclasse di B, quindi il codice verrà generato con una chiamata indiretta (una chiamata virtuale al metodo value()). In C++11 possiamo aggiungere la parola chiave override, ma questa non influenza la generazione del codice.
Ma quando aggiungiamo la parola chiave a final al metodo (1)
class B : public A {
public:
int value() final { return 2; }
};
o all’intera classe (2)
class B final : public A {
public:
int value() override { return 2; }
};
abbiamo la garanzia che nessuno potrà sovrascrivere il metodo (1), o che nessuna sottoclasse può esistere (2), e finirà per devirtualizzare la chiamata, potenzialmente ottimizzando e inserendo la funzione in linea, e trasformare l’intera chiamata in un semplice return 13;.
Senza devirtualizzazione (ovvero senza la keyword final, Visual Studio 2015 (Update 3) produce questo codice assembly:
sub rsp, 40
mov rax, qword ptr [rcx]
call qword ptr [rax]
add eax, 11
add rsp, 40
ret
GCC 6.2 produce questo
mov rax, qword ptr [rdi]
mov rdx, qword ptr [rax]
cmp rdx, offset flat:B::value()
jne .L12
mov eax, 13 (*)
ret
.L12:
sub rsp, 8
call rdx
add rsp, 8
add eax, 11
ret
e Clang 3.9.0 produce questo
push rax
mov rax, qword ptr [rdi]
call qword ptr [rax]
add eax, 11
pop rcx
ret
Tutti e tre i compilatori generano lo stesso codice assembly quando la devirtualizzazione è possibile:
mov eax, 13
ret
Una nota riguardo GCC: il codice generato è piuttosto complesso, ma in pratica è una devirtualizzazione parziale: il codice compilato ha un caso speciale per chiamare B::value(), per il quale produce il risultato 13 immediatamente (*). Questo credo si basi sul fatto che il metodo è piuttosto semplice, e il compilatore assume che la chiamata venga spesso rediretta verso B::value() (probabilmente questa deduzione è corroborata dalla mancanza di altre sottoclassi nell’unità di compilazione).
Potete visionare il codice generato in differenti versioni di GCC e Clang su Godbolt Compiler Explorer. Scoprirete che quest’ottimizzazione funziona in tutte le versioni che supportano C++11 (GCC 4.7+ e Clang 3.0+), anche su architetture non x86/x64. La devirtualizzazione viene applicata anche in Visual Studio 2013 (almeno sulla versione testata da me, Update 5), ma non in Visual Studio 2012 (dove la parola chiave final è accettata e produce errori in caso di overload, ma non attiva alcuna ottimizzazione).
Un ultimo chiarimento richiesto via Reddit: l’uso della keyword final permette la devirtualizzazione, ma i compilatori C++ non sono obbligati ad applicare la devirtualizzazione in such cases.
Questo articolo è la traduzione di un mio post del 2016 intitolato The power of devirtualization.
#
Great article! And interesting the reference to the website http://gcc.godbolt.org/ . I use to show assembly in the debugger, but that could be very helpful too!
Have a nice day Marco,
Ste
#