Uma variável de membro não utilizada ocupa memória?

91

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?

Chriss555888
fonte
25
Isso depende do compilador, da arquitetura, do sistema operacional e da otimização usada.
Coruja
16
Há uma tonelada de código de driver de baixo nível por aí que especificamente adiciona membros de estrutura sem ação para preenchimento para corresponder aos tamanhos de quadro de dados de hardware e como um hack para obter o alinhamento de memória desejado. Se um compilador começasse a otimizá-los, haveria muitas falhas.
Andy Brown
2
@Andy eles não fazem nada enquanto o endereço dos seguintes membros de dados é avaliado. Isso significa que a existência desses membros de preenchimento tem um comportamento observável no programa. Aqui, var2não.
YSC
4
Eu ficaria surpreso se o compilador pudesse otimizá-lo, visto que qualquer unidade de compilação endereçada a tal estrutura pode ser vinculada a outra unidade de compilação usando a mesma estrutura e o compilador não pode saber se a unidade de compilação separada endereça o membro ou não.
Galik
2
@geza sizeof(Foo)não pode diminuir por definição - se você imprimir sizeof(Foo), deve ceder 8(em plataformas comuns). Os compiladores podem otimizar o espaço usado por var2(não importa se através newou 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.
Max Langhof

Respostas:

106

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 .

Uma variável de membro não utilizada ocupa memória?

Não (se "realmente" não for usado).


Agora vêm duas questões em mente:

  1. Quando o comportamento observável não dependeria da existência de um membro?
  2. Esse tipo de situação ocorre em programas da vida real?

Vamos começar com um exemplo.

Exemplo

#include <iostream>

struct Foo1
{ int var1 = 5;           Foo1() { std::cout << var1; } };

struct Foo2
{ int var1 = 5; int var2; Foo2() { std::cout << var1; } };

void f1() { (void) Foo1{}; }
void f2() { (void) Foo2{}; }

Se pedirmos ao gcc para compilar esta unidade de tradução , ele produzirá:

f1():
        mov     esi, 5
        mov     edi, OFFSET FLAT:_ZSt4cout
        jmp     std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
f2():
        jmp     f1()

f2é o mesmo que f1, e nenhuma memória é usada para conter um real Foo2::var2. ( Clang faz algo semelhante ).

Discussão

Alguns podem dizer que isso é diferente por dois motivos:

  1. este é um exemplo muito trivial,
  2. a estrutura é totalmente otimizada, não conta.

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:

bool insert(std::set<int>& set, int value)
{
    return set.insert(value).second;
}

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:

  • tomando o tamanho de um tipo de objeto ( sizeof(Foo)),
  • pegando o endereço de um membro de dados declarado após o "não utilizado",
  • copiar o objeto com uma função como memcpy,
  • manipular a representação do objeto (como com memcmp),
  • qualificando um objeto como volátil ,
  • etc .

1)

[intro.abstract]/1

As descrições semânticas neste documento definem uma máquina abstrata não determinística parametrizada. Este documento não impõe requisitos à estrutura de implementações em conformidade. Em particular, eles não precisam copiar ou emular a estrutura da máquina abstrata. Em vez disso, implementações em conformidade são necessárias para emular (apenas) o comportamento observável da máquina abstrata, conforme explicado abaixo.

2) Como uma afirmação de aprovação ou reprovação.

YSC
fonte
Comentários sugerindo melhorias na resposta foram arquivados no chat .
Cody Gray
1
Mesmo o assert(sizeof(…)…)não restringe o compilador - ele tem que fornecer um sizeofque permita que o código memcpyfuncione, 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 tal memcpyque possa não reescrever para produzir o valor correto de qualquer maneira.
Davis Herring
@Davis Absolutamente.
YSC
63

É 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 :

struct Foo {
    int var1 = 3;
    int var2 = 4;
    int var3 = 5;
};

int test()
{
    Foo foo;
    std::array<char, sizeof(Foo)> arr;
    std::memcpy(&arr, &foo, sizeof(Foo));
    return arr[0] + arr[4];
}

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:

test(): # @test()
  mov eax, 7
  ret

Não só os membros de Foonão ocuparam nenhuma memória, Foocomo 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 eg var3nã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.

Max Langhof
fonte
6
Mic drop "Consulte o manual do compilador para obter mais detalhes." : D
YSC
22

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 Fooser 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.

Alan Birtles
fonte
1
E ainda assim: godbolt.org/z/UJKguS + nenhum compilador avisaria sobre um membro de dados não utilizado.
YSC
@YSC clang ++ avisa sobre membros e variáveis ​​de dados não utilizados.
Maxim Egorushkin
3
@YSC Acho que é uma situação um pouco diferente, pois otimizou a estrutura completamente e apenas imprime 5 diretamente
Alan Birtles
4
@AlanBirtles Não vejo como é diferente. O compilador otimizou tudo do objeto que não tem efeito no comportamento observável do programa. Portanto, sua primeira frase "é muito improvável que o compilador otimize uma variável de membro não utilizada" está errada.
YSC
2
@YSC em código real, onde a estrutura está realmente sendo usada em vez de apenas construída para seus efeitos colaterais, é provavelmente mais improvável que seja otimizado
Alan Birtles,
7

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 de Foos, a distância entre o início de dois objetos consecutivos de Fooé sempre sizeof(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 offsetofmacro). Além disso, você pode inspecionar a representação byte a byte do objeto copiando em um array de charusing std::memcpy. Em todos esses casos, pode-se observar que o segundo membro está presente.

Handy999
fonte
Os comentários não são para discussão extensa; esta conversa foi movida para o chat .
Cody Gray
2
+1: apenas a otimização agressiva de todo o programa poderia ajustar o layout dos dados (incluindo tamanhos e deslocamentos em tempo de compilação) para casos em que um objeto de struct local não é totalmente otimizado,. gcc -fwhole-program -O3 *.cpoderia 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 exato sizeof()deste alvo, e porque é uma otimização realmente complicada que os programadores devem fazer manualmente se quiserem.)
Peter Cordes
6

Os exemplos fornecidos por outras respostas a esta pergunta que elide var2sã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 apenas var2). 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, var2não poderá ser eliminada. Até onde eu sei, nenhum compilador C / C ++ atual pode especializar funções de acordo com a elisão de var2, portanto, se a estrutura for passada ou retornada de uma função var2nã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 var2porque 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 var2a estrutura, a menos que toda a variável de estrutura seja excluída. Para casos interessantes de elisão da var2estrutura, a resposta é: Não.

Alguns compiladores C / C ++ futuros serão capazes de eliminar var2a estrutura, e o ecossistema construído em torno dos compiladores precisará se adaptar às informações de eliminação do processo geradas pelos compiladores.

símbolo atômico
fonte
1
Seu parágrafo sobre as informações de depuração se resume a "não podemos otimizá-las se isso tornasse a depuração mais difícil", o que é simplesmente errado. Ou estou interpretando mal. Você poderia esclarecer?
Max Langhof
Se o compilador emitir informações de depuração sobre a estrutura, ele não poderá elide var2. As opções são: (1) Não emite as informações de depuração se não corresponderem à representação física da estrutura, (2) Apoie a elisão do membro da estrutura nas informações de depuração e emita as informações de depuração
atomsymbol
Talvez mais geral seja referir-se à substituição escalar de agregados (e então elisão de depósitos mortos, etc. )
Davis Herring
4

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 :

-fauto-inc-dec 
-fbranch-count-reg 
-fcombine-stack-adjustments 
-fcompare-elim 
-fcprop-registers 
-fdce
-fdefer-pop
...

-fdcesignifica Dead Code Elimination .

Você pode usar __attribute__((used))para evitar que o gcc elimine uma variável não utilizada com armazenamento estático:

Este atributo, anexado a uma variável com armazenamento estático, significa que a variável deve ser emitida mesmo se parecer que a variável não está referenciada.

Quando aplicado a um membro de dados estáticos de um modelo de classe C ++, o atributo também significa que o membro é instanciado se a própria classe for instanciada.

Wennter
fonte
Isso é para membros de dados estáticos , não membros não usados ​​por instância (que não são otimizados a menos que todo o objeto seja). Mas sim, acho que isso conta. BTW, eliminar variáveis ​​estáticas não utilizadas não é eliminação de código morto , a menos que o GCC altere o termo.
Peter Cordes