J'ai joué avec CompilerExplorer de Godbolt. Je voulais voir à quel point certaines optimisations sont bonnes. Mon exemple de travail minimum est:
#include <vector>
int foo() {
std::vector<int> v {1, 2, 3, 4, 5};
return v[4];
}
L'assembleur généré (par clang 5.0.0, -O2 -std = c ++ 14):
foo(): # @foo()
Push rax
mov edi, 20
call operator new(unsigned long)
mov rdi, rax
call operator delete(void*)
mov eax, 5
pop rcx
ret
Comme on peut le voir, Clang connaît la réponse, mais fait beaucoup de choses avant de revenir. Il me semble que même le vecteur est créé, à cause de "l'opérateur new/delete".
Quelqu'un peut-il m'expliquer ce qui se passe ici et pourquoi cela ne revient pas?
Le code généré par GCC (non copié ici) semble construire explicitement le vecteur. Est-ce que quelqu'un sait que GCC n'est pas capable de déduire le résultat?
std::vector<T>
est une classe assez compliquée qui implique une allocation dynamique. Tandis que clang++
est parfois capable d'éluder les allocations de tas , c'est une optimisation assez délicate et vous ne devriez pas vous y fier. Exemple:
int foo() {
int* p = new int{5};
return *p;
}
foo(): # @foo() mov eax, 5 ret
Par exemple, en utilisant std::array<T>
(qui n'alloue pas dynamiquement) produit un code entièrement en ligne :
#include <array>
int foo() {
std::array v{1, 2, 3, 4, 5};
return v[4];
}
foo(): # @foo() mov eax, 5 ret
Comme Marc Glisse noté dans les commentaires de l'autre réponse, c'est ce que dit la norme dans [expr.new] # 1 :
Une implémentation est autorisée à omettre un appel à une fonction d'allocation globale remplaçable ([new.delete.single], [new.delete.array]). Dans ce cas, le stockage est plutôt fourni par l'implémentation ou fourni en étendant l'allocation d'une autre nouvelle expression. L'implémentation peut étendre l'allocation d'une nouvelle expression e1 pour fournir un stockage pour une nouvelle expression e2 si les conditions suivantes étaient vraies si l'allocation n'était pas étendue: [...]
Comme le notent les commentaires, operator new
Peut être remplacé. Cela peut se produire dans n'importe quelle unité de traduction. L'optimisation d'un programme pour le cas où il n'est pas remplacé nécessite donc une analyse globale du programme. Et si elle est remplacée, vous devez l'appeler bien sûr.
Si la valeur par défaut operator new
est une bibliothèque I/O l'appel n'est pas spécifié. Cela importe, car les appels d'E/S de la bibliothèque sont observables et ne peuvent donc pas être optimisés non plus.
N3664 le changement de [expr.new], cité dans une réponse et un commentaire, permet new-expression s to pas appeler une fonction d'allocation globale remplaçable. Mais vector
alloue de la mémoire à l'aide de std::allocator<T>::allocate
, Qui appelle directement ::operator new
, Et non via une nouvelle expression . Donc, cette autorisation spéciale ne s'applique pas, et généralement les compilateurs ne peuvent pas ignorer ces appels directs à ::operator new
.
Cependant, tout espoir n'est pas perdu car la spécification de std::allocator<T>::allocate
A this pour dire:
Remarques: le stockage est obtenu en appelant
::operator new
, Mais il n'est pas précisé quand ni à quelle fréquence cette fonction est appelée.
Tirant parti de cette autorisation, le std::allocator
De libc ++ tilise des fonctions intégrées spéciales de clang pour indiquer au compilateur que l'élision est autorisée. Avec -stdlib=libc++
, clang compile votre code jusqu'à
foo(): # @foo()
mov eax, 5
ret