c++ - make_unique - vector of unique_ptr




Por que um T*pode ser passado no registrador, mas um unique_ptr<T> não pode? (2)

  1. Isso é realmente um requisito da ABI, ou talvez seja apenas uma pessimização em certos cenários?

Um exemplo é o suplemento do processador de arquitetura AMD64 da interface binária do aplicativo System V. Esta ABI é para CPUs compatíveis com x86 de 64 bits (arquitetura Linux x86_64). É seguido no Solaris, Linux, FreeBSD, macOS, Windows Subsystem para Linux:

Se um objeto C ++ tiver um construtor de cópia não trivial ou um destruidor não trivial, ele será passado por referência invisível (o objeto é substituído na lista de parâmetros por um ponteiro que possui a classe INTEGER).

Um objeto com um construtor de cópia não trivial ou um destruidor não trivial não pode ser transmitido por valor porque esses objetos devem ter endereços bem definidos. Problemas semelhantes se aplicam ao retornar um objeto de uma função.

Observe que apenas 2 registradores de uso geral podem ser usados ​​para passar 1 objeto com um construtor de cópia trivial e um destruidor trivial, ou seja, apenas valores de objetos com sizeof não superior a 16 podem ser passados ​​em registradores. Consulte Convenções de chamada de Agner Fog para obter um tratamento detalhado das convenções de chamada, em particular §7.1 Passando e retornando objetos. Existem convenções de chamada separadas para a passagem de tipos SIMD nos registradores.

Existem ABIs diferentes para outras arquiteturas de CPU.

  1. Por que a ABI é assim? Ou seja, se os campos de uma estrutura / classe se encaixam em registros ou mesmo em um único registro - por que não devemos ser capazes de passá-lo nesse registro?

É um detalhe de implementação, mas quando uma exceção é manipulada, durante o desenrolamento da pilha, os objetos com duração automática de armazenamento sendo destruídos devem ser endereçáveis ​​em relação ao quadro da pilha de funções, porque os registros foram derrotados naquele tempo. O código de desenrolamento de pilha precisa dos endereços dos objetos para chamar seus destruidores, mas os objetos nos registradores não têm um endereço.

Pedanticamente, os destruidores operam em objetos :

Um objeto ocupa uma região de armazenamento em seu período de construção ([class.cdtor]), ao longo de sua vida útil e em seu período de destruição.

e um objeto não pode existir no C ++ se nenhum armazenamento endereçável for alocado para ele porque a identidade do objeto é o seu endereço .

Quando um endereço de um objeto com um construtor de cópia trivial mantido em registros é necessário, o compilador pode simplesmente armazenar o objeto na memória e obter o endereço. Se o construtor de cópias não é trivial, por outro lado, o compilador não pode apenas armazená-lo na memória, mas precisa chamar o construtor de cópias que faz uma referência e, portanto, requer o endereço do objeto nos registradores. A convenção de chamada provavelmente não pode depender se o construtor de cópia foi incorporado no chamado ou não.

Outra maneira de pensar sobre isso é que, para tipos trivialmente copiáveis, o compilador transfere o valor de um objeto em registradores, dos quais um objeto pode ser recuperado por armazenamentos de memória simples, se necessário. Por exemplo:

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

no x86_64 com o System V ABI compila em:

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.

Em sua palestra instigante, Chandler Carruth mentions que uma mudança na ABI pode ser necessária (entre outras coisas) para implementar o movimento destrutivo que poderia melhorar as coisas. Na IMO, a alteração da ABI pode ser ininterrupta se as funções que usam a nova ABI optarem explicitamente por ter um novo vínculo diferente, por exemplo, declará-las no bloco extern "C++20" {} (possivelmente, em um novo espaço de nome embutido para migrar APIs existentes). Para que apenas o código compilado com as novas declarações de função com a nova ligação possa usar a nova ABI.

Observe que a ABI não se aplica quando a função chamada foi incorporada. Assim como na geração do código no tempo do link, o compilador pode incorporar funções definidas em outras unidades de tradução ou usar convenções de chamada personalizadas.

Estou assistindo a palestra de Chandler Carruth no CppCon 2019:

Não há abstrações de custo zero

nele, ele dá o exemplo de como ficou surpreso com a quantidade de sobrecarga que você incorre usando um std::unique_ptr<int> sobre um int* ; esse segmento começa aproximadamente no ponto 17:25.

Você pode dar uma olhada nos resultados da compilação de seu exemplo de par de trechos (godbolt.org) - para testemunhar que, de fato, parece que o compilador não está disposto a passar o valor unique_ptr - que, de fato, na linha inferior é apenas um endereço - dentro de um registro, apenas na memória direta.

Um dos pontos que Carruth destaca por volta das 27:00 é que a ABI C ++ exige que parâmetros de valor (alguns, mas não todos; talvez - tipos não primitivos? Tipos não trivialmente construtíveis?) Sejam passados ​​na memória em vez de dentro de um registro.

Minhas perguntas:

  1. Isso é realmente um requisito da ABI em algumas plataformas? (qual?) Ou talvez seja apenas uma pessimização em certos cenários?
  2. Por que a ABI é assim? Ou seja, se os campos de uma estrutura / classe se encaixam em registros ou mesmo em um único registro - por que não devemos ser capazes de passá-lo nesse registro?
  3. O comitê de padrões de C ++ discutiu esse ponto nos últimos anos ou nunca?

PS - Para não deixar essa pergunta sem código:

Ponteiro simples:

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

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

Ponteiro exclusivo:

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

Isso é realmente um requisito da ABI em algumas plataformas? (qual?) Ou talvez seja apenas uma pessimização em certos cenários?

Se algo estiver visível no limite da unidade de compliação, seja definido de forma implícita ou explícita, ele se tornará parte da ABI.

Por que a ABI é assim?

O problema fundamental é que os registros são salvos e restaurados o tempo todo enquanto você move para baixo e para cima na pilha de chamadas. Portanto, não é prático ter uma referência ou ponteiro para eles.

O alinhamento e as otimizações resultantes disso são bons quando isso acontece, mas um designer da ABI não pode confiar nisso. Eles têm que projetar a ABI assumindo o pior caso. Eu não acho que os programadores ficariam muito felizes com um compilador em que a ABI mudou dependendo do nível de otimização.

Um tipo trivialmente copiável pode ser passado em registradores porque a operação de cópia lógica pode ser dividida em duas partes. Os parâmetros são copiados para os registradores usados ​​para transmitir parâmetros pelo chamador e, em seguida, copiados para a variável local pelo receptor. Se a variável local possui ou não um local de memória, isso é apenas uma preocupação do chamado.

Um tipo em que um construtor de copiar ou mover deve ser usado, por outro lado, não pode ter sua operação de cópia dividida dessa maneira, portanto deve ser transmitida na memória.

O comitê de padrões de C ++ discutiu esse ponto nos últimos anos ou nunca?

Não tenho idéia se os órgãos de normas consideraram isso.

A solução óbvia para mim seria adicionar movimentos destrutivos adequados (em vez da casa intermediária atual de um "estado válido mas não especificado") ao idioma, em seguida, introduzir uma maneira de sinalizar um tipo como permitindo "movimentos destrutivos triviais" "mesmo que não permita cópias triviais.

mas essa solução exigiria quebrar a ABI do código existente para implementar nos tipos existentes, o que pode trazer um pouco de resistência (embora a ABI quebre como resultado de novas versões padrão do C ++ não sejam sem precedentes, por exemplo, as alterações std :: string em C ++ 11 resultou em uma quebra ABI ..





abi