Por fim, escrevi um post sobre [[trivial_abi]]!
Esse é um novo recurso proprietário no tronco Clang, novo em fevereiro de 2018. Esta é uma extensão de fornecedor da linguagem C ++, não é C ++ padrão, não é suportada pelo tronco GCC e não há propostas ativas do WG21 para incluí-lo no padrão C ++, tanto quanto eu sei.

Eu não participei da implementação desse recurso. Apenas olhei para os remendos na lista de e-mails do commit e aplaudi silenciosamente para mim mesmo. Mas esse é um recurso tão interessante que acho que todo mundo deveria saber.
Portanto, a primeira coisa que começaremos: esse não é um atributo padrão, e o tronco Clang não suporta a ortografia padrão do atributo [[trivial_abi]] para ele. Em vez disso, você deve escrevê-lo no estilo antigo, como mostrado abaixo:
__attribute__((trivial_abi)) __attribute__((__trivial_abi__)) [[clang::trivial_abi]]
E, como esse é um atributo, o compilador é muito exigente sobre onde você o cola e passivamente agressivamente silencioso se você o cola no lugar errado (pois os atributos não reconhecidos são simplesmente ignorados sem mensagens). Isso não é um bug, é um recurso. A sintaxe correta é esta:
#define TRIVIAL_ABI __attribute__((trivial_abi)) class TRIVIAL_ABI Widget {
Que problema isso resolve?
Lembra da minha postagem em 17/04/2018, onde eu mostrei duas versões da classe?
Nota perev: Como o post de 17/04/2018 tem um pequeno volume, não o publiquei separadamente, mas o inseri aqui embaixo do spoiler.
post de 17/04/2018Desvantagens da falta de chamada do trivial destruidor
Consulte a lista de correspondência da proposta padrão do C ++. Qual das duas funções, foo ou bar, terá o melhor código gerado pelo compilador?
struct Integer { int value; ~Integer() {}
Compilando com GCC e libstdc ++. Adivinha certo?
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
Aqui está o que acontece aqui: O GCC é inteligente o suficiente para entender que, quando um destruidor para uma área de memória é iniciado, sua vida útil termina e todas as entradas anteriores nessa área de memória ficam "mortas". Mas o GCC também é inteligente o suficiente para entender que um destruidor trivial (como o pseudo-destruidor ~ int ()) não faz nada e não produz efeitos.
Portanto, a função bar chama pop_back, que executa ~ Integer (), o que torna vec.back () morto, e o GCC remove completamente a multiplicação por 0xDEADBEEF.
Por outro lado, foo chama pop_back, que lança o pseudo-destruidor ~ int () (ele pode ignorar completamente a chamada, mas não o faz), o GCC vê que está vazio e esquece. Portanto, o GCC não vê que vec.back () está morto e não remove a multiplicação por 0xDEADBEEF.
Isso acontece para um destruidor trivial, mas não para um pseudo-destruidor como ~ int (). Substitua nosso ~ Integer () {} por ~ Integer () = padrão; e veja como a instrução original apareceu novamente!
struct Foo { int value; ~Foo() = default;
Nesse post, é fornecido o código no qual o compilador gerou o código para Foo pior que para Bar. Vale a pena discutir por que isso foi inesperado. Os programadores esperam intuitivamente que o código "trivial" seja melhor que o código "não trivial". Este é o caso na maioria das situações. Em particular, este é o caso quando fazemos uma chamada de função ou retornamos:
template<class T> T incr(T obj) { obj.value += 1; return obj; }
incr
compila para o seguinte código:
leal 1(%rdi), %eax retq
(leal é o
comando x86 que significa “adicionar”.) Vemos que nosso objetivo de 4 bytes é passado para incr no registro% edi e adicionamos 1 ao seu valor e o devolvemos a% eax. Quatro bytes na entrada, quatro bytes na saída, fácil e simples.
Agora vejamos incr (o caso de um destruidor não trivial).
movl (%rsi), %eax addl $1, %eax movl %eax, (%rsi) movl %eax, (%rdi) movq %rdi, %rax retq
Aqui, obj não é passado no registro, apesar de aqui os mesmos 4 bytes com a mesma semântica. Aqui obj é passado e retornado ao endereço. Aqui, o chamador reserva algum espaço para o valor de retorno e nos passa um ponteiro para esse espaço em rdi, e o chamador nos fornece um ponteiro para o valor de retorno obj no próximo registro de argumentos% rsi. Extraímos o valor de (% rsi), adicionamos 1, salvamos de volta a (% rsi) para atualizar o valor de obj e depois copiamos (trivialmente) 4 bytes de obj no slot para o valor de retorno apontado por% rdi. Por fim, copiamos o ponteiro original transmitido pelo chamador de% rdi para% rax, pois o documento
ABI x86-64 (p. 22) nos diz para fazer isso.
A razão pela qual Bar é tão diferente de Foo é porque Bar tem um destruidor não trivial e o
ABI x86-64 (p. 19) especifica especificamente:
Se um objeto C ++ tiver um construtor de cópia não trivial ou um destruidor não trivial, ele é passado por um link invisível (o objeto é substituído por um ponteiro na lista de parâmetros)
Um documento posterior do
Itanium C ++ ABI define o seguinte:
Se o tipo de parâmetro não for trivial para a finalidade da chamada, o chamador deve alocar um local temporário e passar um link para esse local temporário:
[...]
Um tipo é considerado não trivial para a finalidade da chamada se:
Ele possui um construtor de cópia não trivial, um construtor em movimento, um destruidor ou todos os seus construtores em movimento e cópia são excluídos.
Então, isso explica tudo: a barra tem uma geração de código pior porque é passada por um link invisível. É transmitido através de um link invisível desde que ocorreu uma combinação infeliz de duas circunstâncias independentes:
- Documento da ABI diz que objetos com destruidor não trivial são passados por links invisíveis
- Bar tem um destruidor não trivial.
Este é um
silogismo clássico: o primeiro ponto é a premissa principal, o segundo é privado. Como resultado, a barra é transmitida através de um link invisível.
Que alguém nos dê um silogismo:
- Todas as pessoas são mortais
- Sócrates é um homem.
- Consequentemente, Sócrates é mortal.
Se queremos refutar a conclusão “Sócrates é mortal”, devemos refutar uma das premissas: refutar a coisa principal (talvez algumas pessoas não sejam mortais) ou refutar o privado (talvez Sócrates não seja uma pessoa).
Para que Bar seja passado em um registro (como Foo), devemos refutar uma das duas premissas. O caminho C ++ padrão é dar a Bar um destruidor trivial, destruindo a premissa particular. Mas existe outro caminho!
Como [[trivial_abi]] resolve o problema
O novo atributo Clang destrói a premissa principal. Clang estende o documento ABI da seguinte maneira:
Se o tipo de parâmetro não for trivial para a finalidade da chamada, o chamador deve alocar um local temporário e passar um link para esse local temporário:
[...]
Um tipo é considerado não trivial para a finalidade da chamada se estiver marcado como [[trivial_abi]] e:
Ele possui um construtor de cópia não trivial, um construtor em movimento, um destruidor ou todos os seus construtores em movimento e cópia são excluídos.
Mesmo que uma classe com um construtor ou destruidor móvel não trivial possa ser considerada trivial para o propósito da chamada, se estiver marcada como [[trivial_abi]].
Então agora, usando Clang, podemos escrever assim:
#define TRIVIAL_ABI __attribute__((trivial_abi)) struct TRIVIAL_ABI Baz { int value; ~Baz() {}
compile incr <Baz> e obtenha o mesmo código que incr <Foo>!
Aviso nº 1: [[trivial_abi]] às vezes não faz nada
Eu espero que possamos criar invólucros "triviais para fins de chamada" sobre tipos de biblioteca padrão, 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; };
Infelizmente, isso não funciona. Se a sua classe tiver alguma classe base ou campos não estáticos "não triviais para a finalidade da chamada", a extensão Clang na forma em que foi gravada agora tornará sua classe "irreversivelmente não trivial" e o atributo não terá efeito. (Nenhuma mensagem de diagnóstico é emitida. Isso significa que você pode usar [[trivial_abi]] no modelo de classe como um atributo opcional, e a classe será "condicionalmente trivial", o que às vezes é útil. A desvantagem, é claro, é que você pode marque a classe como trivial e descubra que o compilador a corrigiu silenciosamente.)
O atributo será ignorado sem mensagens se sua classe tiver uma classe base virtual ou funções virtuais. Nesses casos, pode não caber nos registros e não sei o que você deseja obter, passando por valor, mas você provavelmente sabe.
Então, até onde eu sei, a única maneira de usar o TRIVIAL_ABI para "tipos utilitários padrão", como opcional <T>, unique_ptr <T> e shared_ptr <T>, é
- implementá-los você mesmo do zero e aplicar o atributo ou
- entre na sua cópia local da libc ++ e insira o atributo lá com suas mãos
(no mundo do código aberto, os dois métodos são essencialmente os mesmos)
Aviso # 2: responsabilidade do destruidor
No exemplo com Foo / Bar, a classe possui um destruidor vazio. Deixe nossa classe realmente ter um destruidor não trivial.
struct Up1 { int value; Up1(Up1&& u) : value(u.value) { u.value = 0; } ~Up1() { puts("destroyed"); } };
Isso deve ser familiar para você, isso é unique_ptr <int>, simplificado até o limite, com a impressão de uma mensagem quando excluída.
Sem TRIVIAL_ABI, o incr <Up1> se parece com o incr <Bar>:
movl (%rsi), %eax addl $1, %eax movl %eax, (%rdi) movl $0, (%rsi) movq %rdi, %rax retq
Com TRIVIAL_ABI, incr parece
maior e mais assustador !
pushq %rbx leal 1(%rdi), %ebx movl $.L.str, %edi callq puts movl %ebx, %eax popq %rbx retq
Na convenção de chamada tradicional, tipos com um destruidor não trivial são sempre transmitidos por um link invisível, o que significa que o lado receptor (neste caso, incr) sempre aceita um ponteiro para um objeto de parâmetro sem possuir esse objeto. O objeto pertence ao chamador, o que faz o trabalho de elision funcionar!
Quando um tipo com [[trivial_abi]] é passado em registradores, essencialmente fazemos uma cópia do objeto de parâmetro.
Como o x86-64 possui apenas um registro para retornar (aplausos), a função chamada não tem como retornar o objeto no final. A função chamada deve se apropriar do objeto que passamos para ele! Isso significa que a função chamada deve chamar o destruidor do objeto de parâmetro quando terminar.
No exemplo anterior, Foo / Bar / Baz, o destruidor é chamado, mas estava vazio e não percebemos. Agora, no incr <Up2>, vemos o código adicional gerado pelo destruidor no lado da função chamada.
Pode-se supor que esse código adicional possa ser gerado em alguns casos de usuário. Mas, pelo contrário, a chamada do destruidor não aparece em lugar algum! É chamado incr, porque não é chamado na função de chamada. E, em geral, preço e benefícios serão equilibrados.
Aviso nº 3: ordem do destruidor
O destruidor de um parâmetro com uma ABI trivial será chamado pela função chamada, e não a chamada (aviso nº 2). Richard Smith aponta que isso significa que ele não será chamado na ordem em que os destruidores dos outros parâmetros estão localizados.
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
quando TRIVIAL_ABI é definido como [[clang :: trivial_abi]], ele imprime:
alpha constructed beta constructed beta destroyed alpha destroyed
Relacionamento com um objeto "trivialmente relocável" / "mover-relocalizado"
Sem relação ... hein?
Como você pode ver, não há requisitos para a classe [[trivial_abi]] ter semântica específica para o construtor em movimento, destruidor ou construtor padrão. Qualquer classe em particular provavelmente será trivialmente relocável, simplesmente porque a maioria das classes é trivialmente relocável.
Podemos simplesmente criar a classe offset_ptr para que não seja trivialmente relocável:
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]); }
Aqui está o código completo.Quando TRIVIAL_ABI é definido, o tronco Clang passa neste teste em -O0 e -O1, mas em -O2 (ou seja, assim que tenta alinhar chamadas para trivial_offset_ptr :: operator + = e o construtor de cópia), ele falha na afirmação.
Então, mais um aviso. Se o seu tipo faz algo tão louco com esse ponteiro, você provavelmente não vai querer passá-lo nos registros.
Bug 37319 , de fato, um pedido de documentação. Nesse caso, verifica-se que não há como fazer o código funcionar da maneira que o programador deseja. Dizemos que o valor de value_ deve depender do valor deste ponteiro, mas na fronteira entre a chamada e as funções chamadas, o objeto está nos registradores e o ponteiro para ele não existe! Portanto, a função de chamada grava na memória e passa o ponteiro this novamente, e como a função chamada deve calcular o valor correto para gravá-lo no valor_? Talvez seja melhor perguntar como isso funciona em -O0?
Este código não deve funcionar.Portanto, se você quiser usar [[trivial_abi]], evite funções-membro (não apenas especiais, mas qualquer) que dependem muito do endereço do próprio objeto (com algum significado indefinido da palavra "essencial").
Intuitivamente, quando uma classe é marcada como [[trivial_abi]], sempre que você espera copiar, você pode obter cópia mais memcpy. Da mesma forma, quando você espera uma mudança, pode obter a mudança mais o memcpy.
Quando um tipo é "trivialmente relocável" (como definido por mim no
C ++ agora ), a qualquer momento que você espera copiar e destruir, é possível obter o memcpy. E da mesma forma, quando você espera deslocamento e destruição, pode realmente obter memcpy. De fato, chamadas para funções especiais serão perdidas se falarmos sobre "realocação trivial", mas quando a classe tiver o atributo [[trivial_abi]] de Clang, as chamadas não serão perdidas. Você acabou de receber (por assim dizer) o memcpy, além das chamadas que esperava. Esse (tipo de) memcpy é o preço que você paga por uma convenção de registro de chamadas mais rápida.
Links para leitura adicional:
Tópico cfe-dev de Akira Hatanaka de novembro de 2017Documentação oficial do ClangA unidade testa trivial_abiBug 37319: trivial_offset_ptr não pode funcionar