À propos de [[trivial_abi]] à Clang

Enfin, j'ai écrit un article sur [[trivial_abi]]!

Il s'agit d'une nouvelle fonctionnalité propriétaire dans le tronc Clang, nouvelle à partir de février 2018. Il s'agit d'une extension du fournisseur du langage C ++, ce n'est pas du C ++ standard, il n'est pas pris en charge par le tronc GCC, et il n'y a aucune proposition active du WG21 pour l'inclure dans le standard C ++, pour autant que je sache.



Je n'ai pas participé à l'implémentation de cette fonctionnalité. J'ai juste regardé les patchs sur la liste de diffusion cfe-commits et j'ai applaudi silencieusement. Mais c'est une fonctionnalité tellement cool que je pense que tout le monde devrait le savoir.

Donc, la première chose que nous allons commencer: ce n'est pas un attribut standard, et le tronc Clang ne prend pas en charge l'orthographe standard de l'attribut [[trivial_abi]] pour lui. Au lieu de cela, vous devez l'écrire dans l'ancien style, comme indiqué ci-dessous:

__attribute__((trivial_abi)) __attribute__((__trivial_abi__)) [[clang::trivial_abi]] 

Et, comme il s'agit d'un attribut, le compilateur est très pointilleux sur l'endroit où vous le collez et passivement agressivement silencieux si vous le collez au mauvais endroit (car les attributs non reconnus sont simplement ignorés sans messages). Ce n'est pas un bug, c'est une fonctionnalité. La syntaxe correcte est la suivante:

 #define TRIVIAL_ABI __attribute__((trivial_abi)) class TRIVIAL_ABI Widget { // ... }; 


Quel problème cela résout-il?



Rappelez-vous mon post du 17/04/2018 où j'ai montré deux versions de la classe?

Remarque perev: Étant donné que l'article du 17/04/2018 a un petit volume, je ne l'ai pas publié séparément, mais je l'ai inséré ici sous le spoiler.
poste à partir du 17/04/2018

Inconvénients de l'appel Trivial Destructor manquant


Voir la liste de diffusion de proposition standard C ++. Laquelle des deux fonctions, foo ou bar, aura le meilleur code généré par le compilateur?

 struct Integer { int value; ~Integer() {} // deliberately non-trivial }; void foo(std::vector<int>& v) { v.back() *= 0xDEADBEEF; v.pop_back(); } void bar(std::vector<Integer>& v) { v.back().value *= 0xDEADBEEF; v.pop_back(); } 


Compilation avec GCC et libstdc ++. Devinez non?

 foo: movq 8(%rdi), %rax imull $-559038737, -4(%rax), %edx subq $4, %rax movl %edx, (%rax) movq %rax, 8(%rdi) ret bar: subq $4, 8(%rdi) ret 


Voici ce qui se passe ici: GCC est suffisamment intelligent pour comprendre que lorsqu'un destructeur pour une zone mémoire est démarré, sa durée de vie se termine et toutes les entrées précédentes dans cette zone mémoire sont «mortes». Mais GCC est également assez intelligent pour comprendre qu'un destructeur trivial (comme le pseudo destructeur ~ int ()) ne fait rien et ne produit aucun effet.

Ainsi, la fonction bar appelle pop_back, qui exécute ~ Integer (), ce qui rend vec.back () mort, et GCC supprime complètement la multiplication par 0xDEADBEEF.

D'autre part, foo appelle pop_back, qui lance le pseudo-destructeur ~ int () (il peut ignorer complètement l'appel, mais ne le fait pas), GCC voit qu'il est vide et l'oublie. Par conséquent, GCC ne voit pas que vec.back () est mort et ne supprime pas la multiplication par 0xDEADBEEF.

Cela se produit pour un destructeur trivial, mais pas pour un pseudo destructeur tel que ~ int (). Remplacez notre ~ Integer () {} par ~ Integer () = default; et voyez à quel point l’instruction est apparue à nouveau!

 struct Foo { int value; ~Foo() = default; // trivial }; struct Bar { int value; ~Bar() {} // deliberately non-trivial }; 

Dans cet article, le code est donné dans lequel le compilateur a généré du code pour Foo pire que pour Bar. Il vaut la peine de discuter de la raison pour laquelle cela était inattendu. Les programmeurs s'attendent intuitivement à ce que le code «trivial» soit meilleur que le code «non trivial». C'est le cas dans la plupart des situations. C'est notamment le cas lorsque nous effectuons un appel de fonction ou un retour:

 template<class T> T incr(T obj) { obj.value += 1; return obj; } 

incr compile le code suivant:

 leal 1(%rdi), %eax retq 

(leal est la commande x86 qui signifie «ajouter».) Nous voyons que notre obj de 4 octets est passé à incr dans le registre% edi, et nous ajoutons 1 à sa valeur et le renvoyons à% eax. Quatre octets en entrée, quatre octets en sortie, facile et simple.

Voyons maintenant incr (le cas d'un destructeur non trivial).

 movl (%rsi), %eax addl $1, %eax movl %eax, (%rsi) movl %eax, (%rdi) movq %rdi, %rax retq 

Ici, obj n'est pas passé dans le registre, malgré le fait qu'ici les mêmes 4 octets avec la même sémantique. Ici obj est passé et renvoyé à l'adresse. Ici, l'appelant réserve de l'espace pour la valeur de retour et nous transmet un pointeur vers cet espace dans rdi, et l'appelant nous donne un pointeur pour la valeur de retour obj dans le prochain registre d'arguments% rsi. Nous extrayons la valeur de (% rsi), ajoutons 1, la sauvegardons dans (% rsi) pour mettre à jour la valeur de obj lui-même, puis copions (trivialement) 4 octets d'obj dans l'emplacement pour la valeur de retour pointée par% rdi. Enfin, nous copions le pointeur d'origine passé par l'appelant de% rdi vers% rax, puisque le document ABI x86-64 (p. 22) nous dit de le faire.

La raison pour laquelle Bar est si différent de Foo est que Bar a un destructeur non trivial, et l' ABI x86-64 (p. 19) indique spécifiquement:

Si un objet C ++ a un constructeur de copie non trivial ou un destructeur non trivial, il est transmis via un lien invisible (l'objet est remplacé par un pointeur [...] dans la liste des paramètres)

Un document Itanium C ++ ABI ultérieur définit les éléments suivants:
Si le type de paramètre n'est pas trivial aux fins de l'appel, l'appelant doit allouer un espace temporaire et transmettre un lien vers cet espace temporaire:
[...]
Un type est considéré comme non trivial aux fins de l'appel si:

Il a un constructeur de copie non trivial, un constructeur en mouvement, un destructeur ou tous ses constructeurs en mouvement et en copie sont supprimés.

Cela explique donc tout: Bar a une génération de code moins bonne car elle est transmise via un lien invisible. Il est transmis via un lien invisible car une combinaison malchanceuse de deux circonstances indépendantes s'est produite:
  • Le document ABI indique que les objets avec destructeur non trivial sont passés par des liens invisibles
  • Bar a un destructeur non trivial.

Il s'agit d'un syllogisme classique: le premier point est la prémisse principale, le second est privé. En conséquence, Bar est transmis via un lien invisible.

Laissez quelqu'un nous donner un syllogisme:
  • Tout le monde est mortel
  • Socrate est un homme.
  • Par conséquent, Socrate est mortel.


Si nous voulons réfuter la conclusion «Socrate est mortel», nous devons réfuter l'une des prémisses: soit pour réfuter l'essentiel (peut-être que certaines personnes ne sont pas mortelles), soit pour réfuter le privé (peut-être que Socrate n'est pas une personne).

Pour que Bar soit passé dans un registre (comme Foo), nous devons réfuter l'un des deux prémisses. Le chemin C ++ standard consiste à donner à Bar un destructeur trivial, détruisant le local privé. Mais il y a une autre façon!

Comment [[trivial_abi]] résout le problème


Le nouvel attribut Clang détruit la prémisse principale. Clang étend le document ABI comme suit:
Si le type de paramètre n'est pas trivial aux fins de l'appel, l'appelant doit allouer un espace temporaire et transmettre un lien vers cet espace temporaire:
[...]
Un type est considéré comme non trivial aux fins de l'appel s'il est marqué comme [[trivial_abi]] et:
Il a un constructeur de copie non trivial, un constructeur en mouvement, un destructeur ou tous ses constructeurs en mouvement et en copie sont supprimés.

Même si une classe avec un constructeur ou destructeur en mouvement non trivial peut être considérée comme triviale aux fins de l'appel, si elle est marquée comme [[trivial_abi]].

Alors maintenant, en utilisant Clang, nous pouvons écrire comme ceci:

 #define TRIVIAL_ABI __attribute__((trivial_abi)) struct TRIVIAL_ABI Baz { int value; ~Baz() {} // deliberately non-trivial }; 

compilez incr <Baz>, et obtenez le même code que incr <Foo>!

Avertissement # 1: [[trivial_abi]] ne fait parfois rien


J'espère que nous pourrions créer des wrappers «triviaux pour les appels» sur les types de bibliothèques standard, comme ceci:

 template<class T, class D> struct TRIVIAL_ABI trivial_unique_ptr : std::unique_ptr<T, D> { using std::unique_ptr<T, D>::unique_ptr; }; 

Hélas, cela ne fonctionne pas. Si votre classe possède une classe de base ou des champs non statiques qui ne sont «pas triviaux aux fins de l'appel», l'extension Clang sous la forme dans laquelle elle est écrite rend votre classe «irréversiblement non triviale» et l'attribut n'aura aucun effet. (Aucun message de diagnostic n'est émis. Cela signifie que vous pouvez utiliser [[trivial_abi]] dans le modèle de classe comme attribut facultatif, et la classe sera «conditionnellement triviale», ce qui est parfois utile. L'inconvénient, bien sûr, est que vous pouvez marquez la classe comme triviale, puis trouvez que le compilateur l'a corrigée tranquillement.)

L'attribut est ignoré sans messages si votre classe a une classe de base virtuelle ou des fonctions virtuelles. Dans ces cas, il peut ne pas entrer dans les registres, et je ne sais pas ce que vous voulez obtenir en le passant par valeur, mais vous le savez probablement.

Donc, pour autant que je sache, la seule façon d'utiliser TRIVIAL_ABI pour les «types d'utilitaires standard» tels que <T> facultatif, unique_ptr <T> et shared_ptr <T> est
  • implémentez-les vous-même à partir de zéro et appliquez l'attribut, ou
  • pénétrer dans votre copie locale de libc ++ et y insérer l'attribut avec vos mains

(dans le monde open source, les deux méthodes sont essentiellement les mêmes)

Avertissement # 2: responsabilité du destructeur


Dans l'exemple avec Foo / Bar, la classe a un destructeur vide. Laissez notre classe avoir un destructeur non trivial.

 struct Up1 { int value; Up1(Up1&& u) : value(u.value) { u.value = 0; } ~Up1() { puts("destroyed"); } }; 

Cela devrait vous être familier, c'est unique_ptr <int>, simplifié à la limite, avec le message imprimé lorsqu'il est supprimé.

Sans TRIVIAL_ABI, incr <Up1> ressemble juste à incr <Bar>:

 movl (%rsi), %eax addl $1, %eax movl %eax, (%rdi) movl $0, (%rsi) movq %rdi, %rax retq 


Avec TRIVIAL_ABI, incr semble plus gros et plus effrayant !

 pushq %rbx leal 1(%rdi), %ebx movl $.L.str, %edi callq puts movl %ebx, %eax popq %rbx retq 


Dans la convention d'appel traditionnelle, les types avec un destructeur non trivial sont toujours passés par un lien invisible, ce qui signifie que le côté récepteur (incr dans ce cas) accepte toujours un pointeur vers un objet paramètre sans posséder cet objet. L'objet appartient à l'appelant, ce qui fait fonctionner l'élision!

Lorsqu'un type avec [[trivial_abi]] est passé dans les registres, nous faisons essentiellement une copie de l'objet paramètre.

Comme x86-64 n'a qu'un seul registre à renvoyer (applaudissements), la fonction appelée n'a aucun moyen de renvoyer l'objet à la fin. La fonction appelée doit s'approprier l'objet que nous lui avons transmis! Cela signifie que la fonction appelée doit appeler le destructeur de l'objet paramètre à la fin.

Dans notre exemple précédent, Foo / Bar / Baz, le destructeur est appelé, mais il était vide et nous ne l'avons pas remarqué. Maintenant, dans incr <Up2>, nous voyons du code supplémentaire généré par le destructeur du côté de la fonction appelée.

On peut supposer que ce code supplémentaire peut être généré dans certains cas d'utilisateur. Mais, au contraire, l'appel du destructeur n'apparaît nulle part! Il est appelé en incr car il n'est pas appelé dans la fonction appelante. Et en général, le prix et les avantages seront équilibrés.

Avertissement n ° 3: Ordonnance du destructeur


Le destructeur d'un paramètre avec un ABI trivial sera appelé par la fonction appelée, et non par la fonction appelante (avertissement n ° 2). Richard Smith souligne que cela signifie que cela signifie qu'il ne sera pas appelé dans l'ordre dans lequel se trouvent les destructeurs des autres paramètres.

 struct TRIVIAL_ABI alpha { alpha() { puts("alpha constructed"); } ~alpha() { puts("alpha destroyed"); } }; struct beta { beta() { puts("beta constructed"); } ~beta() { puts("beta destroyed"); } }; void foo(alpha, beta) {} int main() { foo(alpha{}, beta{}); } 

Ce code imprime:

 alpha constructed beta constructed alpha destroyed beta destroyed 

lorsque TRIVIAL_ABI est défini comme [[clang :: trivial_abi]], il affiche:

 alpha constructed beta constructed beta destroyed alpha destroyed 

Relation avec un objet «trivialement relocalisable» / «move-relocates»


Aucune relation ..., hein?

Comme vous pouvez le voir, il n'y a aucune exigence pour que la classe [[trivial_abi]] ait une sémantique spécifique pour un constructeur en mouvement, un destructeur ou un constructeur par défaut. Toute classe particulière sera probablement relocalisable de manière triviale, simplement parce que la plupart des classes sont relocalisables de manière triviale.

Nous pouvons simplement rendre la classe offset_ptr afin qu'elle ne soit pas relocalisable trivialement:

 template<class T> class TRIVIAL_ABI offset_ptr { intptr_t value_; public: offset_ptr(T *p) : value_((const char*)p - (const char*)this) {} offset_ptr(const offset_ptr& rhs) : value_((const char*)rhs.get() - (const char*)this) {} T *get() const { return (T *)((const char *)this + value_); } offset_ptr& operator=(const offset_ptr& rhs) { value_ = ((const char*)rhs.get() - (const char*)this); return *this; } offset_ptr& operator+=(int diff) { value_ += (diff * sizeof (T)); return *this; } }; int main() { offset_ptr<int> top = &a[4]; top = incr(top); assert(top.get() == &a[5]); } 

Voici le code complet.
Lorsque TRIVIAL_ABI est défini, le tronc de Clang passe ce test à -O0 et -O1, mais à -O2 (c'est-à-dire dès qu'il essaie d'inciter les appels à trivial_offset_ptr :: operator + = et au constructeur de copie), il se bloque lors de l'assertion.

Un dernier avertissement. Si votre type fait quelque chose de si fou avec un pointeur this, vous ne voudrez probablement pas le passer dans les registres.

Bogue 37319 , en fait, une demande de documentation. Dans ce cas, il s'avère qu'il n'y a aucun moyen de faire fonctionner le code comme le souhaite le programmeur. Nous disons que la valeur de value_ devrait dépendre de la valeur du pointeur this, mais à la frontière entre les fonctions appelante et appelée, l'objet est dans les registres et le pointeur vers celui-ci n'existe pas! Par conséquent, la fonction appelante l'écrit dans la mémoire et transmet à nouveau le pointeur this, et comment la fonction appelée doit-elle calculer la valeur correcte afin de l'écrire dans value_? Peut-être vaut-il mieux se demander comment ça marche même à -O0? Ce code ne devrait pas fonctionner du tout.

Donc, si vous souhaitez utiliser [[trivial_abi]], vous devez éviter les fonctions membres (pas seulement spéciales, mais en général) qui dépendent fortement de la propre adresse de l'objet (avec une signification indéfinie du mot «essentiel»).

Intuitivement, quand une classe est marquée comme [[trivial_abi]], chaque fois que vous vous attendez à copier, vous pouvez obtenir copie plus memcpy. Et de même, lorsque vous vous attendez à un déménagement, vous pouvez réellement obtenir le mouvement plus memcpy.

Lorsqu'un type est «trivialement relocalisable» (tel que défini par moi en C ++ Now ), à chaque fois que vous vous attendez à copier et à détruire, vous pouvez réellement obtenir memcpy. Et de la même manière, lorsque vous vous attendez à un déplacement et à une destruction, vous pouvez en fait obtenir de la mémoire. En fait, les appels à des fonctions spéciales sont perdus si nous parlons de «relocalisation triviale», mais lorsque la classe a l'attribut [[trivial_abi]] de Clang, les appels ne sont pas perdus. Vous obtenez juste (pour ainsi dire) memcpy en plus des appels que vous attendiez. Cette (sorte de) memcpy est le prix à payer pour une convention d'enregistrement des appels plus rapide.

Liens pour plus de lecture:


Le fil cfe-dev d'Akira Hatanaka de novembre 2017
Documentation officielle de Clang
Les tests unitaires pour trivial_abi
Bogue 37319: trivial_offset_ptr ne peut pas fonctionner

Source: https://habr.com/ru/post/fr468759/


All Articles