web-dev-qa-db-fra.com

Pourquoi un T * peut-il être passé dans le registre, mais un unique_ptr <T> ne peut pas?

Je regarde le discours de Chandler Carruth dans CppCon 2019:

Il n'y a pas d'abstractions à coût nul

il donne l'exemple de la façon dont il a été surpris par la quantité de frais généraux que vous encourez en utilisant un std::unique_ptr<int> sur un int*; ce segment commence au point de temps 17:25.

Vous pouvez jeter un œil aux résultats de la compilation de son exemple de paire d'extraits (godbolt.org) - pour constater qu'en effet, il semble que le compilateur ne veuille pas transmettre la valeur unique_ptr - qui en fait, la ligne de fond n'est qu'une adresse - à l'intérieur d'un registre, uniquement en mémoire directe.

L'un des points soulevés par M. Carruth vers 27h00 est que l'ABI C++ nécessite que des paramètres de sous-valeur (certains mais pas tous; peut-être - des types non primitifs? Des types non trivialement constructibles?) Soient passés en mémoire plutôt que dans un registre.

Mes questions:

  1. Est-ce réellement une exigence ABI sur certaines plates-formes? (lequel?) Ou peut-être que ce n'est qu'une pessimisation dans certains scénarios?
  2. Pourquoi l'ABI est-il comme ça? Autrement dit, si les champs d'une structure/classe s'inscrivent dans des registres, ou même dans un seul registre - pourquoi ne pourrions-nous pas le transmettre dans ce registre?
  3. Le comité des normes C++ a-t-il discuté de ce point ces dernières années, ou jamais?

PS - Pour ne pas laisser cette question sans code:

Pointeur simple:

void bar(int* ptr) noexcept;
void baz(int* ptr) noexcept;

void foo(int* ptr) noexcept {
    if (*ptr > 42) {
        bar(ptr); 
        *ptr = 42; 
    }
    baz(ptr);
}

Pointeur unique:

using std::unique_ptr;
void bar(int* ptr) noexcept;
void baz(unique_ptr<int> ptr) noexcept;

void foo(unique_ptr<int> ptr) noexcept {
    if (*ptr > 42) { 
        bar(ptr.get());
        *ptr = 42; 
    }
    baz(std::move(ptr));
}
84
einpoklum
  1. Est-ce en fait une exigence ABI, ou peut-être que c'est juste une pessimisation dans certains scénarios?

Un exemple est Supplément de processeur d'architecture d'application binaire de l'interface d'application System V . Cette ABI est destinée aux CPU 64 bits compatibles x86 (architecture Linux x86_64). Il est suivi sur Solaris, Linux, FreeBSD, macOS, sous-système Windows pour Linux:

Si un objet C++ possède un constructeur de copie non trivial ou un destructeur non trivial, il est transmis par référence invisible (l'objet est remplacé dans la liste des paramètres par un pointeur de classe INTEGER).

Un objet avec un constructeur de copie non trivial ou un destructeur non trivial ne peut pas être transmis par valeur car ces objets doivent avoir des adresses bien définies. Des problèmes similaires s'appliquent lors du retour d'un objet à partir d'une fonction.

Notez que seuls 2 registres à usage général peuvent être utilisés pour passer 1 objet avec un constructeur de copie trivial et un destructeur trivial, c'est-à-dire que seules les valeurs des objets avec sizeof pas plus de 16 peuvent être passées dans les registres. Voir Conventions d'appel par Agner Fog pour un traitement détaillé des conventions d'appel, en particulier §7.1 Passage et retour d'objets. Il existe des conventions d'appel distinctes pour le passage des types SIMD dans les registres.

Il existe différentes ABI pour d'autres architectures de CPU.


  1. Pourquoi l'ABI est-il comme ça? Autrement dit, si les champs d'une structure/classe s'inscrivent dans des registres, ou même dans un seul registre - pourquoi ne pourrions-nous pas le transmettre dans ce registre?

Il s'agit d'un détail d'implémentation, mais lorsqu'une exception est gérée, lors du déroulement de la pile, les objets dont la durée de stockage automatique est détruite doivent être adressables par rapport à la trame de pile de fonctions car les registres ont été encombrés à ce moment. Le code de déroulement de pile a besoin des adresses des objets pour invoquer leurs destructeurs mais les objets dans les registres n'ont pas d'adresse.

Pédantiquement, les destructeurs opèrent sur des objets :

Un objet occupe une région de stockage pendant sa période de construction ([class.cdtor]), tout au long de sa durée de vie et pendant sa période de destruction.

et un objet ne peut pas exister en C++ si aucun stockage adressable ne lui est alloué car l'identité de l'objet est son adresse .

Lorsqu'une adresse d'un objet avec un constructeur de copie trivial conservé dans des registres est nécessaire, le compilateur peut simplement stocker l'objet en mémoire et obtenir l'adresse. Si le constructeur de copie n'est pas trivial, en revanche, le compilateur ne peut pas simplement le stocker en mémoire, il doit plutôt appeler le constructeur de copie qui prend une référence et nécessite donc l'adresse de l'objet dans les registres. La convention d'appel ne peut probablement pas dépendre du fait que le constructeur de copie a été inséré dans l'appelé ou non.

Une autre façon de penser à cela est que pour les types trivialement copiables, le compilateur transfère la valeur d'un objet dans des registres, à partir desquels un objet peut être récupéré par mémoire ordinaire stocke si nécessaire. Par exemple.:

void f(long*);
void g(long a) { f(&a); }

sur x86_64 avec System V ABI se compile en:

g(long):                             // Argument a is in rdi.
        Push    rax                  // Align stack, faster sub rsp, 8.
        mov     qword ptr [rsp], rdi // Store the value of a in rdi into the stack to create an object.
        mov     rdi, rsp             // Load the address of the object on the stack into rdi.
        call    f(long*)             // Call f with the address in rdi.
        pop     rax                  // Faster add rsp, 8.
        ret                          // The destructor of the stack object is trivial, no code to emit.

Dans son discours incitant à la réflexion, Chandler Carruth mentionne qu'un changement d'ABI cassant peut être nécessaire (entre autres) pour mettre en œuvre le mouvement destructeur qui pourrait améliorer les choses. OMI, le changement ABI pourrait être ininterrompu si les fonctions utilisant le nouvel ABI opt-in explicitement pour avoir une nouvelle liaison différente, par ex. les déclarer dans extern "C++20" {} block (éventuellement, dans un nouvel espace de noms en ligne pour la migration des API existantes). Pour que seul le code compilé avec les nouvelles déclarations de fonction avec la nouvelle liaison puisse utiliser le nouvel ABI.

Notez que l'ABI ne s'applique pas lorsque la fonction appelée a été insérée. En plus de la génération de code au moment de la liaison, le compilateur peut incorporer des fonctions définies dans d'autres unités de traduction ou utiliser des conventions d'appel personnalisées.

47
Maxim Egorushkin

Avec les ABI courants, le destructeur non trivial -> ne peut pas passer dans les registres

(Une illustration d'un point dans la réponse de @ MaximEgorushkin utilisant l'exemple de @ harold dans un commentaire; corrigé selon le commentaire de @ Yakk.)

Si vous compilez:

struct Foo { int bar; };
Foo test(Foo byval) { return byval; }

vous obtenez:

test(Foo):
        mov     eax, edi
        ret

c'est-à-dire que l'objet Foo est passé à test dans un registre (edi) et également retourné dans un registre (eax).

Lorsque le destructeur n'est pas trivial (comme le std::unique_ptr exemple d'OP) - Les ABI courants nécessitent un placement sur la pile. Cela est vrai même si le destructeur n'utilise pas du tout l'adresse de l'objet.

Ainsi, même dans le cas extrême d'un destructeur ne rien faire, si vous compilez:

struct Foo2 {
    int bar;
    ~Foo2() {  }
};

Foo2 test(Foo2 byval) { return byval; }

vous obtenez:

test(Foo2):
        mov     edx, DWORD PTR [rsi]
        mov     rax, rdi
        mov     DWORD PTR [rdi], edx
        ret

avec chargement et stockage inutiles.

8
einpoklum

Est-ce réellement une exigence ABI sur certaines plates-formes? (lequel?) Ou peut-être que ce n'est qu'une pessimisation dans certains scénarios?

Si quelque chose est visible à la frontière de l'unité de complication, qu'il soit défini implicitement ou explicitement, il fait partie de l'ABI.

Pourquoi l'ABI est-il comme ça?

Le problème fondamental est que les registres sont enregistrés et restaurés tout le temps lorsque vous montez et descendez dans la pile des appels. Il n'est donc pas pratique d'avoir une référence ou un pointeur vers eux.

L'intégration et les optimisations qui en découlent sont agréables quand cela se produit, mais un concepteur ABI ne peut pas compter sur cela. Ils doivent concevoir l'ABI en supposant le pire des cas. Je ne pense pas que les programmeurs seraient très satisfaits d'un compilateur où l'ABI a changé en fonction du niveau d'optimisation.

Un type trivialement copiable peut être passé dans les registres car l'opération de copie logique peut être divisée en deux parties. Les paramètres sont copiés dans les registres utilisés pour transmettre les paramètres par l'appelant, puis copiés dans la variable locale par l'appelé. Le fait que la variable locale ait ou non un emplacement mémoire n'est donc que la préoccupation de l'appelé.

Un type où un constructeur de copie ou de déplacement doit être utilisé d'autre part ne peut pas avoir son opération de copie divisée de cette manière, il doit donc être passé en mémoire.

Le comité des normes C++ a-t-il discuté de ce point ces dernières années, ou jamais?

Je ne sais pas si les organismes de normalisation ont envisagé cela.

La solution évidente pour moi serait d'ajouter des mouvements destructeurs appropriés (plutôt que la maison de transition actuelle d'un "état valide mais non spécifié") à la jauge, puis d'introduire un moyen de signaler un type comme permettant des "mouvements destructeurs triviaux "même s'il ne permet pas de copies triviales.

mais une telle solution DEVRAIT nécessiter la rupture de l'ABI du code existant à implémenter pour les types existants, ce qui peut apporter un peu de résistance (bien que les ruptures ABI à la suite de nouvelles versions standard C++ ne soient pas sans précédent, par exemple les changements std :: string en C++ 11 a entraîné une rupture ABI ..

2
plugwash