Acerca de [[trivial_abi]] en Clang

隆Finalmente, escrib铆 una publicaci贸n sobre [[trivial_abi]]!

Esta es una nueva caracter铆stica patentada en el tronco de Clang, nueva a partir de febrero de 2018. Esta es una extensi贸n del proveedor del lenguaje C ++, no es C ++ est谩ndar, no es compatible con el tronco GCC, y no hay propuestas activas de WG21 para incluirlo en el est谩ndar C ++, que yo sepa.



No particip茅 en la implementaci贸n de esta funci贸n. Solo mir茅 los parches en la lista de correo de cfe-commits y me aplaud铆 en silencio. Pero esta es una caracter铆stica tan genial que creo que todos deber铆an saberlo.

Entonces, lo primero con lo que comenzaremos: este no es un atributo est谩ndar, y el tronco de Clang no admite la ortograf铆a est谩ndar del atributo [[trivial_abi]] para 茅l. En cambio, debe escribirlo en el estilo antiguo, como se muestra a continuaci贸n:

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

Y, dado que este es un atributo, el compilador es muy exigente con respecto a d贸nde lo pega y silenciosamente agresivo si lo pega en el lugar equivocado (ya que los atributos no reconocidos simplemente se ignoran sin mensajes). Esto no es un error, esta es una caracter铆stica. La sintaxis correcta es esta:

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


驴Qu茅 problema resuelve esto?



驴Recuerdas mi publicaci贸n el 17/04/2018 donde mostr茅 dos versiones de la clase?

Nota perev: Dado que la publicaci贸n del 17/04/2018 tiene un peque帽o volumen, no lo publiqu茅 por separado, sino que lo insert茅 aqu铆 debajo del spoiler.
publicar desde 17/04/2018

Desventajas de la Llamada de Destructor Trivial Perdida


Consulte la Lista de correo de propuestas est谩ndar de C ++. 驴Cu谩l de las dos funciones, foo o bar, tendr谩 el mejor c贸digo generado por el compilador?

 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(); } 


Compilaci贸n con GCC y libstdc ++. Adivina verdad?

 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 


Esto es lo que sucede aqu铆: GCC es lo suficientemente inteligente como para comprender que cuando se inicia un destructor para una regi贸n de memoria, su vida 煤til finaliza y todas las entradas anteriores a esta regi贸n de memoria est谩n "muertas". Pero GCC tambi茅n es lo suficientemente inteligente como para comprender que un destructor trivial (como el pseudo destructor ~ int ()) no hace nada y no produce efectos.

Entonces, la funci贸n de barra llama a pop_back, que ejecuta ~ Integer (), lo que hace que vec.back () est茅 muerto, y GCC elimina completamente la multiplicaci贸n por 0xDEADBEEF.

Por otro lado, foo llama a pop_back, que lanza el pseudo destructor ~ int () (puede omitir por completo la llamada, pero no lo hace), GCC ve que est谩 vac铆o y se olvida de ello. Por lo tanto, GCC no ve que vec.back () est谩 muerto y no elimina la multiplicaci贸n por 0xDEADBEEF.

Esto sucede para un destructor trivial, pero no para un pseudo destructor como ~ int (). Reemplace nuestro ~ Integer () {} con ~ Integer () = default; 隆y mira c贸mo la instrucci贸n completa apareci贸 nuevamente!

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

En esa publicaci贸n, se proporciona el c贸digo en el que el compilador gener贸 c贸digo para Foo peor que para Bar. Vale la pena discutir por qu茅 esto fue inesperado. Los programadores intuitivamente esperan que el c贸digo "trivial" sea mejor que el c贸digo "no trivial". Este es el caso en la mayor铆a de las situaciones. En particular, este es el caso cuando realizamos una llamada a una funci贸n o la devolvemos:

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

incr compila el siguiente c贸digo:

 leal 1(%rdi), %eax retq 

(leal es el comando x86 que significa "agregar"). Vemos que nuestro obj de 4 bytes se pasa a incr en el registro% edi, y agregamos 1 a su valor y lo devolvemos a% eax. Cuatro bytes en la entrada, cuatro bytes en la salida, f谩cil y simple.

Ahora veamos incr (el caso con un destructor no trivial).

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

Aqu铆, obj no se pasa en el registro, a pesar del hecho de que aqu铆 los mismos 4 bytes con la misma sem谩ntica. Aqu铆 se pasa obj y se devuelve a la direcci贸n. Aqu铆 la persona que llama reserva un espacio para el valor de retorno y nos pasa un puntero a este espacio en rdi, y la persona que llama nos da un puntero para el valor de retorno obj en el siguiente registro de argumentos% rsi. Extraemos el valor de (% rsi), agregamos 1, lo guardamos de nuevo en (% rsi) para actualizar el valor de obj, y luego (trivialmente) copiamos 4 bytes de obj en la ranura para el valor de retorno se帽alado por% rdi. Finalmente, copiamos el puntero original pasado por la persona que llama de% rdi a% rax, ya que el documento x86-64 ABI (p. 22) nos dice que hagamos esto.

La raz贸n por la que Bar es tan diferente de Foo es porque Bar tiene un destructor no trivial, y el x86-64 ABI (p. 19) establece espec铆ficamente:

Si un objeto C ++ tiene un constructor de copia no trivial o un destructor no trivial, se pasa a trav茅s de un enlace invisible (el objeto se reemplaza con un puntero [...] en la lista de par谩metros)

Un documento posterior de Itanium C ++ ABI define lo siguiente:
Si el tipo de par谩metro no es trivial para el prop贸sito de la llamada, la persona que llama debe asignar un lugar temporal y pasar un enlace a este lugar temporal:
[...]
Un tipo se considera no trivial para el prop贸sito de la llamada si:

Tiene un constructor de copia no trivial, un constructor m贸vil, un destructor, o todos sus constructores m贸viles y de copia se eliminan.

Esto explica todo: Bar tiene una generaci贸n de c贸digo m谩s pobre porque se pasa a trav茅s de un enlace invisible. Se transmite a trav茅s de un enlace invisible ya que se ha producido una combinaci贸n desafortunada de dos circunstancias independientes:
  • El documento ABI dice que los objetos con destructor no trivial se pasan a trav茅s de enlaces invisibles
  • Bar tiene un destructor no trivial.

Este es un silogismo cl谩sico: el primer punto es la premisa principal, el segundo es privado. Como resultado, Bar se transmite a trav茅s de un enlace invisible.

Que alguien nos d茅 un silogismo:
  • Todas las personas son mortales
  • S贸crates es un hombre.
  • En consecuencia, S贸crates es mortal.


Si queremos refutar la conclusi贸n "S贸crates es mortal", debemos refutar una de las premisas: refutar lo principal (quiz谩s algunas personas no son mortales) o refutar lo privado (quiz谩s S贸crates no es una persona).

Para que Bar se apruebe en un registro (como Foo), debemos refutar una de las dos premisas. La ruta est谩ndar de C ++ es darle a Bar un destructor trivial, destruyendo la premisa privada. Pero hay otra manera!

C贸mo [[trivial_abi]] resuelve el problema


El nuevo atributo Clang destruye la premisa principal. Clang extiende el documento ABI de la siguiente manera:
Si el tipo de par谩metro no es trivial para el prop贸sito de la llamada, la persona que llama debe asignar un lugar temporal y pasar un enlace a este lugar temporal:
[...]
Un tipo se considera no trivial a los efectos de la llamada si est谩 marcado como [[trivial_abi]] y:
Tiene un constructor de copia no trivial, un constructor m贸vil, un destructor, o todos sus constructores m贸viles y de copia se eliminan.

Incluso si una clase con un constructor o destructor en movimiento no trivial puede considerarse trivial para el prop贸sito de la llamada, si est谩 marcada como [[trivial_abi]].

As铆 que ahora, usando Clang, podemos escribir as铆:

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

compile incr <Baz> y obtenga el mismo c贸digo que incr <Foo>!

Advertencia # 1: [[trivial_abi]] a veces no hace nada


Espero que podamos hacer envoltorios "triviales para llamadas" sobre tipos de biblioteca est谩ndar, como este:

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

Por desgracia, esto no funciona. Si su clase tiene alguna clase base o campos no est谩ticos que son "no triviales para el prop贸sito de la llamada", entonces la extensi贸n Clang en la forma en que est谩 escrita ahora hace que su clase sea "irreversiblemente no trivial", y el atributo no tendr谩 efecto. (No se emiten mensajes de diagn贸stico. Esto significa que puede usar [[trivial_abi]] en la plantilla de clase como un atributo opcional, y la clase ser谩 "condicionalmente trivial", lo que a veces es 煤til. La desventaja, por supuesto, es que puede marque la clase como trivial y luego descubra que el compilador la arregl贸 en silencio).

El atributo se ignora sin mensajes si su clase tiene una clase base virtual o funciones virtuales. En estos casos, es posible que no quepa en los registros, y no s茅 qu茅 quiere obtener al pasarlo por valor, pero probablemente lo sepa.

Entonces, que yo sepa, la 煤nica forma de usar TRIVIAL_ABI para "tipos utilitarios est谩ndar" como opcional <T>, unique_ptr <T> y shared_ptr <T> es
  • implem茅ntelos usted mismo desde cero y aplique el atributo, o
  • irrumpir en su copia local de libc ++ e insertar el atributo all铆 con las manos

(en el mundo de c贸digo abierto, ambos m茅todos son esencialmente iguales)

Advertencia # 2: responsabilidad del destructor


En el ejemplo con Foo / Bar, la clase tiene un destructor vac铆o. Deje que nuestra clase tenga un destructor no trivial.

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

Esto deber铆a serle familiar, esto es unique_ptr <int>, simplificado al l铆mite, con el mensaje impreso cuando se elimina.

Sin TRIVIAL_ABI, incr <Up1> solo se parece a incr <Bar>:

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


隆Con TRIVIAL_ABI, incr parece m谩s grande y aterrador !

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


En la convenci贸n de llamada tradicional, los tipos con un destructor no trivial siempre pasan por un enlace invisible, lo que significa que el lado receptor (incr en este caso) siempre acepta un puntero a un objeto de par谩metro sin poseer este objeto. El objeto es propiedad de la persona que llama. 隆Esto hace que el trabajo de elisi贸n funcione!

Cuando se pasa un tipo con [[trivial_abi]] en los registros, esencialmente hacemos una copia del objeto de par谩metro.

Como x86-64 solo tiene un registro para devolver (aplausos), la funci贸n llamada no tiene forma de devolver el objeto al final. 隆La funci贸n llamada deber铆a tomar posesi贸n del objeto que le pasamos! Esto significa que la funci贸n llamada debe llamar al destructor del objeto de par谩metro cuando finalice.

En nuestro ejemplo anterior, Foo / Bar / Baz, se llama al destructor, pero estaba vac铆o y no lo notamos. Ahora en incr <Up2> vemos c贸digo adicional generado por el destructor al lado de la funci贸n llamada.

Se puede suponer que este c贸digo adicional se puede generar en algunos casos de usuario. Pero, por el contrario, 隆la llamada del destructor no aparece en ning煤n lado! Se llama en incr porque no se llama en la funci贸n de llamada. Y en general, el precio y los beneficios ser谩n equilibrados.

Advertencia # 3: Orden del Destructor


El destructor para un par谩metro con un ABI trivial ser谩 llamado por la funci贸n llamada, y no por la llamada (advertencia No. 2). Richard Smith se帽ala que esto significa que no se lo llamar谩 en el orden en que se encuentran los destructores de los otros par谩metros.

 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{}); } 

Este c贸digo imprime:

 alpha constructed beta constructed alpha destroyed beta destroyed 

cuando TRIVIAL_ABI se define como [[clang :: trivial_abi]], imprime:

 alpha constructed beta constructed beta destroyed alpha destroyed 

Relaci贸n con un objeto "trivialmente reubicable" / "mover-reubicar"


Sin relaci贸n ... 驴eh?

Como puede ver, no hay requisitos para que la clase [[trivial_abi]] tenga una sem谩ntica espec铆fica para el constructor en movimiento, el destructor o el constructor predeterminado. Cualquier clase particular probablemente ser谩 trivialmente reubicable, simplemente porque la mayor铆a de las clases son trivialmente reubicables.

Simplemente podemos hacer la clase offset_ptr para que no sea reubicable trivialmente:

 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]); } 

Aqu铆 est谩 el c贸digo completo.
Cuando se define TRIVIAL_ABI, la troncal de Clang pasa esta prueba en -O0 y -O1, pero en -O2 (es decir, tan pronto como intenta en l铆nea las llamadas a trivial_offset_ptr :: operator + = y al constructor de la copia), se bloquea en la afirmaci贸n.

Entonces una advertencia m谩s. Si su tipo hace algo tan loco con este puntero, probablemente no quiera pasarlo en los registros.

Bug 37319 , de hecho, una solicitud de documentaci贸n. En este caso, resulta que no hay forma de hacer que el c贸digo funcione de la manera que el programador quiere. Decimos que el valor de value_ debe depender del valor de este puntero, pero en el l铆mite entre las funciones de llamada y llamadas, 隆el objeto est谩 en registros y el puntero no existe! Por lo tanto, la funci贸n de llamada lo escribe en la memoria y pasa el puntero de este nuevamente, y 驴c贸mo deber铆a la funci贸n llamada calcular el valor correcto para escribirlo en value_? Quiz谩s sea mejor preguntar c贸mo funciona incluso en -O0. Este c贸digo no deber铆a funcionar en absoluto.

Por lo tanto, si desea utilizar [[trivial_abi]], debe evitar las funciones de los miembros (no solo especiales, sino cualquiera en general) que dependen en gran medida de la propia direcci贸n del objeto (con un significado indefinido de la palabra "esencial").

Intuitivamente, cuando una clase se marca como [[trivial_abi]], cada vez que espera copiar, puede obtener copia m谩s memcpy. Y de manera similar, cuando esperas un movimiento, puedes obtener el movimiento m谩s memcpy.

Cuando un tipo es "trivialmente reubicable" (como lo defin铆 en C ++ ahora ), cada vez que espere copiar y destruir, puede obtener memcpy. Y de manera similar, cuando esperas desplazamiento y destrucci贸n, puedes obtener memcpy. De hecho, las llamadas a funciones especiales se pierden si hablamos de "reubicaci贸n trivial", pero cuando la clase tiene el atributo [[trivial_abi]] de Clang, las llamadas no se pierden. Simplemente obtienes (por as铆 decirlo) memcpy adem谩s de las llamadas que esperabas. Esta (especie de) memoria es el precio que paga por una convenci贸n de registro de llamadas m谩s r谩pida.

Enlaces para lecturas adicionales:


El hilo cfe-dev de Akira Hatanaka de noviembre de 2017
Documentaci贸n oficial de Clang
La unidad prueba para trivial_abi
Error 37319: trivial_offset_ptr no puede funcionar

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


All Articles