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
#