Inicializar uma variável de membro e não referenciá-la / usá-la consome mais RAM durante o tempo de execução ou o compilador simplesmente ignora essa variável?
struct Foo {
int var1;
int var2;
Foo() { var1 = 5; std::cout << var1; }
};
No exemplo acima, o membro 'var1' obtém um valor que é então exibido no console. 'Var2', entretanto, não é usado. Portanto, gravá-lo na memória durante o tempo de execução seria um desperdício de recursos. O compilador leva esses tipos de situações em consideração e simplesmente ignora as variáveis não utilizadas, ou o objeto Foo é sempre do mesmo tamanho, independentemente de seus membros serem usados?
var2
não.sizeof(Foo)
não pode diminuir por definição - se você imprimirsizeof(Foo)
, deve ceder8
(em plataformas comuns). Os compiladores podem otimizar o espaço usado porvar2
(não importa se atravésnew
ou na pilha ou em chamadas de função ...) em qualquer contexto que acharem razoável, mesmo sem LTO ou otimização de todo o programa. Onde isso não for possível, eles não o farão, como acontece com qualquer outra otimização. Acredito que a edição da resposta aceita torna significativamente menos provável que seja enganado por ela.Respostas:
A regra 1 de ouro do C ++ "como se" afirma que, se o comportamento observável de um programa não depender de uma existência de membro de dados não utilizado, o compilador tem permissão para otimizá-lo .
Não (se "realmente" não for usado).
Agora vêm duas questões em mente:
Vamos começar com um exemplo.
Exemplo
Se pedirmos ao gcc para compilar esta unidade de tradução , ele produzirá:
f2
é o mesmo quef1
, e nenhuma memória é usada para conter um realFoo2::var2
. ( Clang faz algo semelhante ).Discussão
Alguns podem dizer que isso é diferente por dois motivos:
Bem, um bom programa é uma montagem inteligente e complexa de coisas simples, em vez de uma simples justaposição de coisas complexas. Na vida real, você escreve toneladas de funções simples usando estruturas simples do que o compilador otimiza. Por exemplo:
Este é um exemplo genuíno de um membro de dados (aqui,
std::pair<std::set<int>::iterator, bool>::first
) não sendo usado. Adivinha? Ele é otimizado ( exemplo mais simples com um conjunto fictício se essa montagem te faz chorar).Agora seria o momento perfeito para ler a excelente resposta de Max Langhof (voto positivo para mim, por favor). Isso explica por que, no final, o conceito de estrutura não faz sentido no nível do assembly que o compilador produz.
"Mas, se eu fizer X, o fato de o membro não utilizado ser otimizado é um problema!"
Houve vários comentários argumentando que essa resposta deve estar errada porque alguma operação (como
assert(sizeof(Foo2) == 2*sizeof(int))
) quebraria algo.Se X faz parte do comportamento observável do programa 2 , o compilador não tem permissão para otimizar as coisas. Existem várias operações em um objeto que contém um membro de dados "não utilizado" que teria um efeito observável no programa. Se tal operação for executada ou se o compilador não puder provar que nenhuma foi executada, esse membro de dados "não utilizado" é parte do comportamento observável do programa e não pode ser otimizado .
As operações que afetam o comportamento observável incluem, mas não estão limitadas a:
sizeof(Foo)
),memcpy
,memcmp
),1)
2) Como uma afirmação de aprovação ou reprovação.
fonte
assert(sizeof(…)…)
não restringe o compilador - ele tem que fornecer umsizeof
que permita que o códigomemcpy
funcione, mas isso não significa que o compilador é de alguma forma obrigado a usar tantos bytes, a menos que eles possam ser expostos a um talmemcpy
que possa não reescrever para produzir o valor correto de qualquer maneira.É importante perceber que o código que o compilador produz não tem nenhum conhecimento real de suas estruturas de dados (porque tal coisa não existe no nível de montagem), e nem o otimizador. O compilador produz apenas código para cada função , não estruturas de dados .
Ok, ele também grava seções de dados constantes e tal.
Com base nisso, já podemos dizer que o otimizador não irá "remover" ou "eliminar" membros, pois não produz estruturas de dados. Ele produz código , que pode ou não usar os membros, e entre seus objetivos está a economia de memória ou ciclos, eliminando usos inúteis (ou seja, gravações / leituras) dos membros.
A essência disso é que "se o compilador pode provar dentro do escopo de uma função (incluindo funções que foram embutidas nela) que o membro não utilizado não faz diferença em como a função opera (e o que ela retorna), então as chances são boas de que a presença do membro não causa sobrecarga ”.
Conforme você torna as interações de uma função com o mundo externo mais complicadas / confusas para o compilador (pegar / retornar estruturas de dados mais complexas, por exemplo
std::vector<Foo>
, a , ocultar a definição de uma função em uma unidade de compilação diferente, proibir / desincentivar inlining etc.) , torna-se cada vez mais provável que o compilador não consiga provar que o membro não utilizado não tem efeito.Não há regras rígidas aqui porque tudo depende das otimizações que o compilador faz, mas contanto que você faça coisas triviais (como mostrado na resposta de YSC) é muito provável que nenhuma sobrecarga esteja presente, enquanto fazer coisas complicadas (por exemplo, retornar a
std::vector<Foo>
de uma função muito grande para inlining) provavelmente incorrerá na sobrecarga.Para ilustrar o ponto, considere este exemplo :
Nós fazemos coisas não triviais aqui (pegar endereços, inspecionar e adicionar bytes da representação de bytes ) e ainda assim o otimizador pode descobrir que o resultado é sempre o mesmo nesta plataforma:
Não só os membros de
Foo
não ocuparam nenhuma memória,Foo
como nem mesmo passaram a existir! Se houver outros usos que não podem ser otimizados, por exemplo,sizeof(Foo)
pode importar - mas apenas para aquele segmento de código! Se todos os usos pudessem ser otimizados dessa forma, a existência de egvar3
não influencia o código gerado. Mas mesmo se for usado em outro lugar,test()
permanecerá otimizado!Resumindo: cada uso de
Foo
é otimizado de forma independente. Alguns podem usar mais memória por causa de um membro desnecessário, outros não. Consulte o manual do compilador para obter mais detalhes.fonte
O compilador só otimizará uma variável de membro não utilizada (especialmente uma pública) se puder provar que a remoção da variável não tem efeitos colaterais e que nenhuma parte do programa depende do tamanho de
Foo
ser o mesmo.Não acho que nenhum compilador atual execute tais otimizações, a menos que a estrutura não esteja realmente sendo usada. Alguns compiladores podem, pelo menos, alertar sobre variáveis privadas não utilizadas, mas geralmente não para as públicas.
fonte
Em geral, você deve assumir que obteve o que solicitou, por exemplo, as variáveis de membro "não utilizadas" estão lá.
Como em seu exemplo ambos os membros são
public
, o compilador não pode saber se algum código (particularmente de outras unidades de tradução = outros arquivos * .cpp, que são compilados separadamente e depois vinculados) acessaria o membro "não utilizado".A resposta do YSC dá um exemplo muito simples, onde o tipo de classe é usado apenas como uma variável de duração de armazenamento automática e onde nenhum ponteiro para essa variável é obtido. Lá, o compilador pode embutir todo o código e, então, eliminar todo o código morto.
Se você tiver interfaces entre funções definidas em unidades de tradução diferentes, normalmente o compilador não sabe de nada. As interfaces seguem normalmente alguma ABI predefinida (como essa ), de forma que diferentes arquivos de objeto podem ser vinculados sem problemas. Normalmente, ABIs não fazem diferença se um membro é usado ou não. Portanto, nesses casos, o segundo membro deve estar fisicamente na memória (a menos que seja eliminado posteriormente pelo vinculador).
E enquanto você estiver dentro dos limites da linguagem, você não pode observar que qualquer eliminação aconteça. Se você ligar
sizeof(Foo)
, você receberá2*sizeof(int)
. Se você criar um array deFoo
s, a distância entre o início de dois objetos consecutivos deFoo
é sempresizeof(Foo)
bytes.Seu tipo é um tipo de layout padrão , o que significa que você também pode acessar membros com base em deslocamentos computados em tempo de compilação (cf. a
offsetof
macro). Além disso, você pode inspecionar a representação byte a byte do objeto copiando em um array dechar
usingstd::memcpy
. Em todos esses casos, pode-se observar que o segundo membro está presente.fonte
gcc -fwhole-program -O3 *.c
poderia em teoria fazer isso, mas na prática provavelmente não. (por exemplo, no caso de o programa fazer algumas suposições sobre o valor exatosizeof()
deste alvo, e porque é uma otimização realmente complicada que os programadores devem fazer manualmente se quiserem.)Os exemplos fornecidos por outras respostas a esta pergunta que elide
var2
são baseados em uma única técnica de otimização: propagação constante e elisão subsequente de toda a estrutura (não a elisão de apenasvar2
). Este é o caso simples, e os compiladores de otimização o implementam.Para códigos C / C ++ não gerenciados, a resposta é que o compilador em geral não elide
var2
. Pelo que eu sei, não há suporte para essa transformação de struct C / C ++ nas informações de depuração e, se a estrutura estiver acessível como uma variável em um depurador,var2
não poderá ser eliminada. Até onde eu sei, nenhum compilador C / C ++ atual pode especializar funções de acordo com a elisão devar2
, portanto, se a estrutura for passada ou retornada de uma funçãovar2
não embutida, então não pode ser elidida.Para linguagens gerenciadas, como C # / Java com um compilador JIT, o compilador pode ser capaz de eliminar com segurança
var2
porque pode rastrear com precisão se está sendo usado e se escapa para código não gerenciado. O tamanho físico da estrutura em linguagens gerenciadas pode ser diferente de seu tamanho informado ao programador.Os compiladores C / C ++ do ano 2019 não podem excluir
var2
a estrutura, a menos que toda a variável de estrutura seja excluída. Para casos interessantes de elisão davar2
estrutura, a resposta é: Não.Alguns compiladores C / C ++ futuros serão capazes de eliminar
var2
a estrutura, e o ecossistema construído em torno dos compiladores precisará se adaptar às informações de eliminação do processo geradas pelos compiladores.fonte
Depende do seu compilador e de seu nível de otimização.
No gcc, se você especificar
-O
, ele ativará os seguintes sinalizadores de otimização :-fdce
significa Dead Code Elimination .Você pode usar
__attribute__((used))
para evitar que o gcc elimine uma variável não utilizada com armazenamento estático:fonte