Um dos problemas do pimpl é a penalidade de desempenho em usá-lo (alocação de memória adicional, membros de dados não contíguos, indiretos adicionais, etc.). Gostaria de propor uma variação no idioma pimpl que evite essas penalidades de desempenho às custas de não obter todos os benefícios do pimpl. A idéia é deixar todos os membros de dados privados na própria classe e mover apenas os métodos privados para a classe pimpl. O benefício comparado ao pimpl básico é que a memória permanece contígua (sem necessidade de direcionamento adicional). Os benefícios comparados a não usar pimpl são:
- Esconde as funções privadas.
- Você pode estruturá-lo para que todas essas funções tenham ligação interna e permitam ao compilador otimizá-lo mais agressivamente.
Então, minha idéia é fazer com que o pimpl seja herdado da própria classe (parece um pouco louco, eu sei, mas tenha paciência comigo). Seria algo como isto:
No arquivo Ah:
class A
{
A();
void DoSomething();
protected: //All private stuff have to be protected now
int mData1;
int mData2;
//Not even a mention of a PImpl in the header file :)
};
No arquivo A.cpp:
#define PCALL (static_cast<PImpl*>(this))
namespace //anonymous - guarantees internal linkage
{
struct PImpl : public A
{
static_assert(sizeof(PImpl) == sizeof(A),
"Adding data members to PImpl - not allowed!");
void DoSomething1();
void DoSomething2();
//No data members, just functions!
};
void PImpl::DoSomething1()
{
mData1 = bar(mData2); //No Problem: PImpl sees A's members as it's own
DoSomething2();
}
void PImpl::DoSomething2()
{
mData2 = baz();
}
}
A::A(){}
void A::DoSomething()
{
mData2 = foo();
PCALL->DoSomething1(); //No additional indirection, everything can be completely inlined
}
Até onde eu vejo, não há absolutamente nenhuma penalidade de desempenho no uso deste vs pimpl e alguns possíveis ganhos de desempenho e uma interface de arquivo de cabeçalho mais limpa. Uma desvantagem que isso tem em relação ao pimpl padrão é que você não pode ocultar os membros dos dados, portanto as alterações nesses membros ainda acionarão uma recompilação de tudo o que depende do arquivo de cabeçalho. Mas, do meu ponto de vista, é esse benefício ou o desempenho de manter os membros contíguos na memória (ou fazer isso- "Por que a tentativa nº 3 é deplorável"). Outra ressalva é que, se A é uma classe de modelo, a sintaxe fica irritante (você sabe, você não pode usar mData1 diretamente, é necessário fazer isso-> mData1 e precisa começar a usar o tipo de texto e talvez as palavras-chave do modelo para tipos dependentes tipos de modelos, etc.). Outra ressalva é que você não pode mais usar o privado na classe original, apenas membros protegidos; portanto, não pode restringir o acesso de nenhuma classe herdada, não apenas o pimpl. Eu tentei, mas não pude contornar esse problema. Por exemplo, tentei tornar o pimpl uma classe de modelo de amigo na esperança de tornar a declaração de amigo suficientemente ampla para permitir que eu definisse a classe de pimpl real em um espaço de nome anônimo, mas isso simplesmente não funciona. Se alguém tiver alguma idéia de como manter os membros de dados privados e ainda permitir que uma classe pimpl herdada definida em um espaço de nome anônimo acesse esses, eu realmente gostaria de vê-lo! Isso eliminaria minha reserva principal de usar isso.
Porém, sinto que essas advertências são aceitáveis pelos benefícios do que proponho.
Tentei procurar online alguma referência a esse idioma "pimpl somente de função", mas não consegui encontrar nada. Estou realmente interessado no que as pessoas pensam sobre isso. Existem outros problemas com este ou os motivos pelos quais não devo usá-lo?
ATUALIZAR:
Eu encontrei essa proposta que mais ou menos tenta realizar exatamente o que sou, mas fazendo isso alterando o padrão. Eu concordo completamente com essa proposta e espero que ela se enquadre no padrão (não conheço nada desse processo, portanto não tenho idéia da probabilidade de isso acontecer). Eu preferiria que fosse possível fazer isso por meio de um mecanismo de linguagem incorporado. A proposta também explica os benefícios do que estou tentando alcançar muito melhor do que eu. Ele também não tem o problema de quebrar o encapsulamento, como minha sugestão tem (privado -> protegido). Ainda assim, até que a proposta chegue ao padrão (se isso acontecer), acho que minha sugestão possibilita esses benefícios, com as ressalvas que listei.
UPDATE2:
Uma das respostas menciona o LTO como uma possível alternativa para obter alguns dos benefícios (acho que otimizações mais agressivas). Não sei exatamente o que acontece em várias passagens de otimização do compilador, mas tenho um pouco de experiência com o código resultante (uso o gcc). Simplesmente colocar os métodos privados na classe original forçará aqueles a ter um vínculo externo.
Eu posso estar errado aqui, mas a maneira como interpreto isso é que o otimizador em tempo de compilação não pode eliminar a função, mesmo que todas as suas instâncias de chamada estejam completamente embutidas nessa TU. Por alguma razão, até o LTO se recusa a se livrar da definição da função, mesmo que pareça que todas as instâncias de chamada em todo o binário vinculado estejam todas inline. Encontrei algumas referências afirmando que é porque o vinculador não sabe se, de alguma forma, você ainda chamará a função usando ponteiros de função (embora eu não entenda por que o vinculador não consegue descobrir que o endereço desse método nunca é usado )
Este não é o caso se você usar minha sugestão e colocar esses métodos privados em um pimpl dentro de um espaço para nome anônimo. Se eles forem incorporados, as funções NÃO aparecerão (com -O3, que inclui -finline-functions) no arquivo de objeto.
Pelo que entendi, o otimizador, ao decidir se alinha ou não uma função, leva em consideração seu impacto no tamanho do código. Portanto, usando minha sugestão, estou tornando um pouco "mais barato" para o otimizador incorporar esses métodos particulares.
PCALL
é um comportamento indefinido. Você não pode converter umA
para umPImpl
e usá-lo, a menos que o objeto subjacente seja realmente do tipoPImpl
. No entanto, a menos que eu esteja enganado, os usuários apenas criarão objetos do tipoA
.Respostas:
Os pontos de venda do padrão Pimpl são:
Para esse efeito, o Pimpl clássico consiste em três partes:
Uma interface para o objeto de implementação, que deve ser público e usar métodos virtuais para a interface:
Essa interface é necessária para ser estável.
Um objeto de interface que faz proxy com a implementação privada. Não precisa usar métodos virtuais. O único membro permitido é um ponteiro para a implementação:
O arquivo de cabeçalho desta classe deve ser estável.
Pelo menos uma implementação
O Pimpl nos compra uma grande estabilidade para uma classe de biblioteca, ao custo de uma alocação de heap e envio virtual adicional.
Como sua solução se compara?
Portanto, para cada objetivo do padrão Pimpl, você falha em cumprir esse objetivo. Portanto, não é razoável chamar seu padrão de variação do Pimpl; é muito mais uma classe comum. Na verdade, é pior do que uma classe comum porque suas variáveis de membro são privadas. E por causa desse elenco, que é um ponto flagrante de fragilidade.
Observe que o padrão Pimpl nem sempre é ideal - há uma troca entre estabilidade e polimorfismo, por um lado, e compactação de memória, por outro. É semanticamente impossível para um idioma ter os dois (sem compilação JIT). Portanto, se você está otimizando minuciosamente a compactação de memória, claramente o Pimpl não é uma solução adequada para o seu caso de uso. Você provavelmente também deixará de usar metade da biblioteca padrão, pois essas terríveis classes de vetores e seqüências envolvem alocações dinâmicas de memória ;-)
fonte
Para mim, as vantagens não superam as desvantagens.
Vantagens :
Ele pode acelerar a compilação, pois salva uma reconstrução se apenas as assinaturas de método privado forem alteradas. Mas a reconstrução é necessária se as assinaturas de método público ou protegido ou os membros de dados privados foram alterados, e é raro que eu precise alterar as assinaturas de método privado sem tocar em nenhuma dessas outras opções.
Ele pode permitir otimizações de compilador mais agressivas, mas o LTO deve permitir muitas das mesmas otimizações (pelo menos, eu acho que pode - eu não sou um guru de otimização de compiladores), além de outras mais, e pode ser padronizado e automático.
Desvantagens:
Você mencionou algumas desvantagens: a incapacidade de usar privado e as complexidades com modelos. Para mim, porém, a maior desvantagem é que é simplesmente estranho: um estilo de programação não convencional, com saltos no estilo pimpl não muito padrão entre interface e implementação, que não serão familiares a futuros mantenedores ou novos membros da equipe e que podem ser mal suportado por ferramentas (veja, por exemplo, esse bug do GDB ).
As preocupações padrão sobre otimização aplicam-se aqui: Você mediu que as otimizações melhoram significativamente seu desempenho? Seu desempenho seria melhor fazendo isso ou gastando o tempo necessário para mantê-lo e investi-lo em criação de perfis de pontos ativos, aprimorando algoritmos etc.? Pessoalmente, prefiro escolher um estilo de programação claro e direto, supondo que isso libere tempo para fazer otimizações direcionadas. Mas essa é minha perspectiva para os tipos de código nos quais trabalho - para o domínio do seu problema, as vantagens e desvantagens podem ser diferentes.
Nota lateral: Permitindo particular com pimpl somente de método
Você perguntou sobre como permitir membros privados com sua sugestão de pimpl somente de método. Infelizmente, considero o método pimpl apenas de método um tipo de hack, mas se você decidiu que as vantagens superam as desvantagens, é melhor adotar o hack.
Ah:
A.cpp:
fonte
Você pode usar
std::aligned_storage
para declarar armazenamento para seu pimpl na classe de interface.Na implementação, você pode construir sua classe Pimpl no local
_storage
:fonte
Não, você não pode implementá-lo sem uma penalidade de desempenho. O PIMPL é, por sua própria natureza, uma penalidade de desempenho, pois você está aplicando um indireto em tempo de execução.
Obviamente, isso depende exatamente do que você deseja indiretamente. Algumas informações simplesmente não são usadas pelo consumidor - como exatamente o que você pretende colocar em seus 64 bytes alinhados de 4 bytes. Mas outras informações são como o fato de que você deseja 64 bytes alinhados por 4 bytes para o seu objeto.
PIMPLs genéricos sem penalidades de desempenho não existem e nunca existirão. É a mesma informação que você nega ao usuário que ele deseja usar para otimizar. Se você der a eles, seu IMPL não será abstraído; se você negar, eles não podem otimizar. Você não pode ter as duas coisas.
fonte
Com todo o respeito e não com a intenção de matar essa emoção, não vejo nenhum benefício prático que isso sirva de uma perspectiva em tempo de compilação. Muitos dos benefícios
pimpls
surgirão ao ocultar detalhes do tipo definido pelo usuário. Por exemplo:... nesse caso, o maior custo de compilação vem do fato de que, para definir
Foo
, precisamos conhecer os requisitos de tamanho / alinhamentoBar
(o que significa que precisamos recursivamente exigir a definição deBar
).Se você não estiver ocultando os membros dos dados, um dos benefícios mais significativos de uma perspectiva em tempo de compilação será perdido. Também há algum código com aparência potencialmente perigosa, mas o cabeçalho não fica mais claro e o arquivo de origem está ficando mais pesado com mais funções de encaminhamento; portanto, é provável que aumente, em vez de diminuir, o tempo de compilação geral.
Cabeçalhos mais leves é a chave
Para diminuir o tempo de compilação, você deve poder mostrar uma técnica que resulta em um cabeçalho de peso dramaticamente mais leve (normalmente, permitindo que você não recursivamente mais
#include
alguns outros cabeçalhos, porque ocultou detalhes que não exigem mais determinadasstruct/class
definições). É aí que o genuínopimpls
pode ter um efeito significativo, interrompendo cadeias em cascata de inclusões de cabeçalho e produzindo cabeçalhos muito mais independentes, com todos os detalhes privados ocultos.Maneiras mais seguras
Se você quiser fazer algo assim de qualquer maneira, também será muito mais simples usar um
friend
definido em seu arquivo de origem, em vez de um que herda a mesma classe que você não instancia com truques de conversão de ponteiros para chamar métodos um objeto não instanciado ou simplesmente use funções autônomas com ligação interna dentro do arquivo de origem que recebem os parâmetros apropriados para realizar o trabalho necessário (qualquer um deles pode pelo menos permitir que você oculte alguns métodos privados do cabeçalho para uma economia muito trivial em tempos de compilação e um pouco de espaço de manobra para evitar a recompilação em cascata).Alocador fixo
Se você deseja o tipo mais barato de pimpl, o truque principal é usar um alocador fixo. Especialmente ao agregar pimpls a granel, o maior assassino é a perda de localidade espacial e as falhas de página obrigatórias adicionais ao acessar o pimpl pela primeira vez. Ao pré-alocar pools de memória que agrupam a memória para os pimpls que estão sendo alocados e retornam a memória ao pool, em vez de liberá-la na desalocação, o custo de um monte de instâncias do pimpl diminui drasticamente. Ainda não é gratuito, do ponto de vista de desempenho, mas é muito mais barato e muito mais compatível com cache / página.
fonte