Por que os tipos sempre têm um determinado tamanho, independentemente do seu valor?

149

As implementações podem diferir entre os tamanhos reais dos tipos, mas, na maioria, tipos como unsigned int e float são sempre de 4 bytes. Mas por que um tipo sempre ocupa uma certa quantidade de memória, não importa seu valor? Por exemplo, se eu criei o número inteiro a seguir com o valor 255

int myInt = 255;

Então myIntocuparia 4 bytes com meu compilador. No entanto, o valor real 255pode ser representado com apenas 1 byte. Por que myIntnão ocupar apenas 1 byte de memória? Ou a maneira mais generalizada de perguntar: Por que um tipo tem apenas um tamanho associado a ele, quando o espaço necessário para representar o valor pode ser menor que esse tamanho?

Nichlas Uden
fonte
15
1) " No entanto, o valor real, 256 pode ser representado com apenas 1 byte " Errado, o maior unsingedvalor que pode ser representado com 1 byte é 255. 2) Considere a sobrecarga de calcular o tamanho ideal de armazenamento e encolher / expandir a área de armazenamento de uma variável, conforme o valor muda.
Algirdas Preidžius
99
Bem, quando chega a hora de ler o valor da memória, como você propõe que a máquina determine quantos bytes serão lidos? Como a máquina saberá onde parar de ler o valor? Isso exigirá instalações adicionais. E, em geral, a sobrecarga de memória e desempenho para esses recursos adicionais será muito maior do que o simples uso de 4 bytes fixos como unsigned intvalor.
AnT
74
Eu realmente gosto desta pergunta. Embora possa parecer simples respondê-lo, acho que dar uma explicação precisa requer um bom entendimento de como os computadores e as arquiteturas de computadores realmente funcionam. A maioria das pessoas provavelmente considerará isso um dado adquirido, sem ter uma explicação abrangente para isso.
andreee
37
Considere o que aconteceria se você adicionasse 1 ao valor da variável, tornando-o 256, para que ele fosse expandido. Para onde ele se expande? Você move o restante da memória para ganhar espaço? A variável em si se move? Em caso afirmativo, para onde ele se move e como você encontra os ponteiros que precisa atualizar?
molbdnilo
13
@someidiot não, você está errado. std::vector<X>sempre tem o mesmo tamanho, ou seja, sizeof(std::vector<X>)é uma constante em tempo de compilação.
Sergeya

Respostas:

131

O compilador deve produzir assembler (e, finalmente, código de máquina) para alguma máquina, e geralmente o C ++ tenta ser simpático a essa máquina.

Ser solidário com a máquina subjacente significa basicamente: facilitar a gravação de código C ++, que será mapeado com eficiência nas operações que a máquina pode executar rapidamente. Portanto, queremos fornecer acesso aos tipos e operações de dados que são rápidos e "naturais" em nossa plataforma de hardware.

Concretamente, considere uma arquitetura de máquina específica. Vamos dar a atual família Intel x86.

O manual do desenvolvedor de software das arquiteturas Intel® 64 e IA-32 vol 1 ( link ), seção 3.4.1, diz:

Os registros de uso geral de 32 bits EAX, EBX, ECX, EDX, ESI, EDI, EBP e ESP são fornecidos para armazenar os seguintes itens:

• Operandos para operações lógicas e aritméticas

• Operandos para cálculos de endereços

• ponteiros de memória

Portanto, queremos que o compilador use esses registros EAX, EBX etc. quando compilar aritmética simples em número inteiro C ++. Isso significa que, quando declaro um int, deve ser algo compatível com esses registros, para que eu possa usá-los com eficiência.

Os registradores sempre têm o mesmo tamanho (aqui, 32 bits); portanto, minhas intvariáveis ​​também terão sempre 32 bits. Usarei o mesmo layout (little-endian) para não precisar fazer uma conversão toda vez que carregar um valor de variável em um registro ou armazenar um registro novamente em uma variável.

Usando godbolt , podemos ver exatamente o que o compilador faz por algum código trivial:

int square(int num) {
    return num * num;
}

compila (com o GCC 8.1 e -fomit-frame-pointer -O3por simplicidade) para:

square(int):
  imul edi, edi
  mov eax, edi
  ret

isso significa:

  1. o int numparâmetro foi passado no registro EDI, o que significa exatamente o tamanho e o layout que a Intel espera de um registro nativo. A função não precisa converter nada
  2. a multiplicação é uma única instrução ( imul), que é muito rápida
  3. retornar o resultado é simplesmente uma questão de copiá-lo para outro registro (o chamador espera que o resultado seja colocado no EAX)

Editar: podemos adicionar uma comparação relevante para mostrar a diferença usando um layout não nativo. O caso mais simples é armazenar valores em algo diferente da largura nativa.

Usando godbolt novamente, podemos comparar uma multiplicação nativa simples

unsigned mult (unsigned x, unsigned y)
{
    return x*y;
}

mult(unsigned int, unsigned int):
  mov eax, edi
  imul eax, esi
  ret

com o código equivalente para uma largura fora do padrão

struct pair {
    unsigned x : 31;
    unsigned y : 31;
};

unsigned mult (pair p)
{
    return p.x*p.y;
}

mult(pair):
  mov eax, edi
  shr rdi, 32
  and eax, 2147483647
  and edi, 2147483647
  imul eax, edi
  ret

Todas as instruções extras estão relacionadas à conversão do formato de entrada (dois inteiros não assinados de 31 bits) no formato que o processador pode manipular nativamente. Se quiséssemos armazenar o resultado novamente em um valor de 31 bits, haveria mais uma ou duas instruções para fazer isso.

Essa complexidade extra significa que você só se preocuparia com isso quando a economia de espaço é muito importante. Nesse caso, estamos salvando apenas dois bits em comparação ao uso do nativo unsignedou do uint32_ttipo, o que teria gerado um código muito mais simples.


Uma observação sobre tamanhos dinâmicos:

O exemplo acima ainda é valores de largura fixa em vez de largura variável, mas a largura (e o alinhamento) não correspondem mais aos registros nativos.

A plataforma x86 possui vários tamanhos nativos, incluindo 8 e 16 bits, além dos principais de 32 bits (estou visualizando o modo de 64 bits e várias outras coisas para simplificar).

Esses tipos (char, int8_t, uint8_t, int16_t etc.) também são suportados diretamente pela arquitetura - em parte para compatibilidade com versões anteriores com 8086/286/386 / etc. etc. conjuntos de instruções.

Certamente é o caso de escolher o menor tipo de tamanho fixo natural suficiente, pode ser uma boa prática - eles ainda são rápidos, carregam e armazenam instruções únicas, você ainda recebe aritmética nativa de velocidade total e pode até melhorar o desempenho reduzindo erros de cache.

Isso é muito diferente da codificação de tamanho variável - trabalhei com algumas delas e elas são horríveis. Toda carga se torna um loop em vez de uma única instrução. Toda loja também é um loop. Como toda estrutura é de tamanho variável, não é possível usar matrizes naturalmente.


Uma nota adicional sobre eficiência

Nos comentários subsequentes, você usou a palavra "eficiente", até onde posso dizer em relação ao tamanho do armazenamento. Às vezes, optamos por minimizar o tamanho do armazenamento - pode ser importante quando estamos salvando um número muito grande de valores em arquivos ou enviando-os pela rede. A desvantagem é que precisamos carregar esses valores nos registradores para fazer qualquer coisa com eles, e realizar a conversão não é gratuito.

Quando discutimos a eficiência, precisamos saber o que estamos otimizando e quais são as vantagens e desvantagens. O uso de tipos de armazenamento não nativos é uma maneira de trocar a velocidade de processamento por espaço e, às vezes, faz sentido. Usando armazenamento de comprimento variável (pelo menos para tipos aritméticos), comercializa mais velocidade de processamento (e complexidade do código e tempo do desenvolvedor) para uma economia de espaço muitas vezes mínima.

A penalidade de velocidade que você paga por isso significa que só vale a pena quando você precisa minimizar absolutamente a largura de banda ou o armazenamento a longo prazo e, nesses casos, geralmente é mais fácil usar um formato simples e natural - e depois compactá-lo com um sistema de uso geral (como zip, gzip, bzip2, xy ou o que for).


tl; dr

Cada plataforma possui uma arquitetura, mas você pode criar um número essencialmente ilimitado de maneiras diferentes de representar dados. Não é razoável que qualquer idioma forneça um número ilimitado de tipos de dados internos. Portanto, o C ++ fornece acesso implícito ao conjunto natural e natural de tipos de dados da plataforma e permite codificar qualquer outra representação (não nativa).

Sem utilidade
fonte
Eu estou olhando para todas as respostas legais enquanto tenta entender todas elas. Portanto, com relação à sua resposta, um tamanho dinâmico não diria menos de 32 bits para um número inteiro, não apenas permitiria mais variáveis ​​em um registro ? Se o endianess é o mesmo, por que isso não seria o ideal?
Nichlas Uden
7
@ asd, mas quantos registradores você usará no código que mostra quantas variáveis ​​estão atualmente armazenadas em um registrador?
user253751
1
No FWIW, é comum empacotar vários valores no menor espaço disponível, onde você decide que a economia de espaço é mais importante que o custo da velocidade de empacotá-los e desempacotá-los. Geralmente, não é possível operá-los naturalmente em sua forma compactada, porque o processador não sabe como fazer aritmética corretamente em nada que não seja seus registradores internos. Olhe para cima BCD para uma exceção parcial com suporte ao processador
Useless
3
Se eu realmente não precisa todos os 32 bits para algum valor, eu ainda preciso de um lugar para armazenar o comprimento, então agora eu preciso mais de 32 bits em alguns casos.
11138 Useless
1
+1. Uma observação sobre "formato simples e natural e, em seguida, compactar" é geralmente melhor: isso é definitivamente verdade , mas : para alguns dados, o VLQ-cada-valor-depois-comprimir-o-todo-desempenho é notavelmente melhor do que apenas comprimir-o -whole-thing, e para alguns aplicativos, seus dados não podem ser compactados juntos , porque são díspares (como nos gitmetadados) ou, na verdade, você os mantém na memória; ocasionalmente, é necessário acessar ou modificar aleatoriamente alguns, mas não a maioria dos dados. os valores (como nos mecanismos de renderização HTML + CSS) e, portanto, só podem ser evitados usando algo como VLQ no local.
Mtraceur
139

Como os tipos representam fundamentalmente o armazenamento e são definidos em termos do valor máximo que podem conter, não o valor atual.

A analogia muito simples seria uma casa - uma casa tem um tamanho fixo, independentemente de quantas pessoas moram nela, e também há um código de construção que estipula o número máximo de pessoas que podem morar em uma casa de um determinado tamanho.

No entanto, mesmo se uma única pessoa estiver morando em uma casa que pode acomodar 10 pessoas, o tamanho da casa não será afetado pelo número atual de ocupantes.

SergeyA
fonte
31
Eu gosto da analogia. Se estendermos um pouco, poderíamos imaginar usando uma linguagem de programação que não usa tamanhos fixos de memória para tipos, e isso seria semelhante a derrubar quartos em nossa casa sempre que não estavam sendo usados ​​e reconstruí-los quando necessário (ou seja, toneladas de despesas gerais quando poderíamos construir um monte de casas e deixá-las para quando precisarmos).
ahouse101
5
"Porque tipos fundamentalmente representam armazenamento" isso não é verdade para todos os idiomas (como o original datilografado, por exemplo)
corvus_192
56
@ tags corvus_192 têm significado. Esta questão é marcado com C ++, não 'typescript'
Sergeya
4
@ ahouse101 De fato, existem vários idiomas que possuem números inteiros de precisão ilimitada, que crescem conforme necessário. Essas linguagens não exigem que você aloque memória fixa para variáveis, elas são implementadas internamente como referências a objetos. Exemplos: Lisp, Python.
Barmar
2
@jamesqf Provavelmente não é uma coincidência que a aritmética MP tenha sido adotada no Lisp, que também fazia gerenciamento automático de memória. Os designers consideraram que os impactos no desempenho eram secundários à facilidade de programação. E técnicas de otimização foram desenvolvidas para minimizar o impacto.
Barmar
44

É uma otimização e simplificação.

Você pode ter objetos de tamanho fixo. Armazenando assim o valor.
Ou você pode ter objetos de tamanho variável. Mas armazenando valor e tamanho.

objetos de tamanho fixo

O código que manipula o número não precisa se preocupar com tamanho. Você assume que sempre usa 4 bytes e torna o código muito simples.

Objetos de tamanho dinâmico

O código que o número manipula deve entender ao ler uma variável que ele deve ler o valor e o tamanho. Use o tamanho para garantir que todos os bits altos sejam zerados no registro.

Quando colocar o valor de volta na memória, se o valor não exceder seu tamanho atual, basta colocar o valor de volta na memória. Mas se o valor diminuiu ou cresceu, é necessário mover o local de armazenamento do objeto para outro local na memória para garantir que ele não exceda. Agora você precisa rastrear a posição desse número (pois ele pode se mover se crescer muito para o tamanho). Você também precisa rastrear todos os locais variáveis ​​não utilizados para que possam ser potencialmente reutilizados.

Resumo

O código gerado para objetos de tamanho fixo é muito mais simples.

Nota

A compactação usa o fato de que 255 caberão em um byte. Existem esquemas de compactação para armazenar grandes conjuntos de dados que usarão ativamente valores de tamanho diferentes para números diferentes. Mas como esses dados não são dinâmicos, você não possui as complexidades descritas acima. Você usa menos espaço para armazenar os dados a um custo de compactação / descompactação dos dados para armazenamento.

Martin York
fonte
4
Esta é a melhor resposta para mim: como você controla o tamanho? Com mais memória?
online Thomas
@ThomasMoors Sim, exatamente: com mais memória. Se você, por exemplo, possui uma matriz dinâmica, alguns intarmazenam o número de elementos nessa matriz. Isso por intsi só terá um tamanho fixo novamente.
Alfe
1
@ThomasMoors, existem duas opções comumente usadas, as quais requerem memória extra - você tem um campo (tamanho fixo) informando a quantidade de dados que há (por exemplo, um int para tamanho de matriz ou sequências "estilo pascal" onde a primeira O elemento contém quantos caracteres existem) ou, alternativamente, você pode ter uma cadeia (ou uma estrutura mais complexa) onde cada elemento de alguma forma anota se é o último - por exemplo, cadeias terminadas em zero ou a maioria das formas de listas vinculadas.
Peteris
27

Como em uma linguagem como C ++, um objetivo do projeto é que operações simples sejam compiladas com instruções simples da máquina.

Todos os conjuntos principais de instruções da CPU funcionam com tipos de largura fixa e , se você quiser fazer tipos de largura variável , precisará executar várias instruções da máquina para lidar com eles.

Quanto ao motivo pelo qual o hardware do computador subjacente é assim: é porque é mais simples e mais eficiente para muitos casos (mas não todos).

Imagine o computador como um pedaço de fita:

| xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | ...

Se você simplesmente instrui o computador a observar o primeiro byte na fita, xxcomo ele sabe se o tipo para ou não por aí ou prossegue para o próximo byte? Se você possui um número como 255(hexadecimal FF) ou um número como 65535(hexadecimal FFFF), o primeiro byte é sempre FF.

Então como você sabe? Você precisa adicionar lógica adicional e "sobrecarregar" o significado de pelo menos um valor de bit ou byte para indicar que o valor continua no próximo byte. Essa lógica nunca é "livre", você a emula no software ou adiciona um monte de transistores adicionais à CPU para fazer isso.

Os tipos de linguagens de largura fixa, como C e C ++, refletem isso.

Não precisa ser dessa maneira, e linguagens mais abstratas que se preocupam menos com o mapeamento para códigos com eficiência máxima estão livres para usar codificações de largura variável (também conhecidas como "Quantidades de Comprimento Variável" ou VLQ) para tipos numéricos.

Leitura adicional: Se você procurar por "quantidade variável de comprimento", poderá encontrar alguns exemplos de onde esse tipo de codificação é realmente eficiente e vale a lógica adicional. Geralmente é quando você precisa armazenar uma quantidade enorme de valores que podem estar em qualquer lugar dentro de um grande intervalo, mas a maioria dos valores tende a um pequeno sub-intervalo.


Observe que, se um compilador puder provar que pode armazenar o valor em uma quantidade menor de espaço sem quebrar nenhum código (por exemplo, é uma variável visível apenas internamente em uma única unidade de tradução), e suas heurísticas de otimização sugerem que ele ' Para ser mais eficiente no hardware de destino, é totalmente permitido otimizá-lo de acordo e armazená-lo em uma quantidade menor de espaço, desde que o restante do código funcione "como se" fizesse o padrão.

Porém , quando o código precisa interagir com outro código que pode ser compilado separadamente, os tamanhos precisam permanecer consistentes ou garantir que cada parte do código siga a mesma convenção.

Porque se não for consistente, há esta complicação: e se eu tiver, int x = 255;mas depois no código que faço x = y? Se intpudesse ter largura variável, o compilador precisaria saber com antecedência para pré-alocar a quantidade máxima de espaço necessária. Isso nem sempre é possível, porque e se yum argumento for passado de outro trecho de código compilado separadamente?

mtraceur
fonte
26

Java usa classes chamadas "BigInteger" e "BigDecimal" para fazer exatamente isso, assim como a interface da classe GMP C ++ do C ++ aparentemente (graças ao Digital Trauma). Você pode fazer isso facilmente em praticamente qualquer idioma, se quiser.

As CPUs sempre tiveram a capacidade de usar o BCD (decimal binário codificado), projetado para suportar operações de qualquer tamanho (mas você tende a operar manualmente em um byte de cada vez, o que seria lento pelos padrões atuais da GPU).

O motivo pelo qual não usamos essas ou outras soluções semelhantes? Atuação. Suas linguagens de melhor desempenho não podem se dar ao luxo de expandir uma variável no meio de uma operação de loop apertado - seria muito não determinístico.

Em situações de armazenamento e transporte em massa, os valores compactados costumam ser o ÚNICO tipo de valor que você usaria. Por exemplo, um pacote de música / vídeo transmitido para o seu computador pode gastar um pouco para especificar se o próximo valor é 2 ou 4 bytes como uma otimização de tamanho.

Uma vez no computador, onde pode ser usada, a memória é barata, mas a velocidade e a complicação de variáveis ​​redimensionáveis ​​não são ... esse é realmente o único motivo.

Bill K
fonte
4
Fico feliz em ver alguém mencionar BigInteger. Não é uma idéia tola, é apenas que faz sentido fazê-lo para números extremamente grandes.
precisa
1
Para ser pedante você realmente quer dizer números extremamente precisas :) Bem, pelo menos no caso de BigDecimal ...
Bill K
2
E como esse código está marcado com c ++ , provavelmente vale a pena mencionar a interface da classe GMP C ++ , que é a mesma ideia que o Big * do Java.
Digital Trauma
20

Porque seria muito complicado e a computação pesada teria tipos simples com tamanhos dinâmicos. Não tenho certeza se isso seria possível.
O computador teria que verificar quantos bits o número leva após cada alteração de seu valor. Seriam muitas operações adicionais. E seria muito mais difícil executar cálculos quando você não souber o tamanho das variáveis ​​durante a compilação.

Para suportar tamanhos dinâmicos de variáveis, o computador precisaria lembrar quantos bytes uma variável possui no momento e quais ... exigiriam memória adicional para armazenar essas informações. E essa informação teria que ser analisada antes de cada operação na variável para escolher a instrução correta do processador.

Para entender melhor como o computador funciona e por que as variáveis ​​têm tamanhos constantes, aprenda o básico da linguagem assembler.

Embora, suponha que seria possível alcançar algo assim com valores constexpr. No entanto, isso tornaria o código menos previsível para um programador. Suponho que algumas otimizações do compilador possam fazer algo assim, mas escondem isso de um programador para manter as coisas simples.

Descrevi aqui apenas os problemas que dizem respeito ao desempenho de um programa. Omiti todos os problemas que precisariam ser resolvidos para economizar memória, reduzindo o tamanho das variáveis. Honestamente, não acho que isso seja possível.


Em conclusão, o uso de variáveis ​​menores que as declaradas só faz sentido se seus valores forem conhecidos durante a compilação. É bem provável que os compiladores modernos façam isso. Em outros casos, causaria muitos problemas difíceis ou até insolúveis.

NO_NAME
fonte
Duvido muito que isso seja feito durante o tempo de compilação. Há pouco sentido em conservar a memória do compilador assim, e esse é o único benefício.
Bartek Banachewicz
1
Eu estava pensando em operações como multiplicar a variável constexpr pela variável normal. Por exemplo, temos (teoricamente) a variável constexpr de 8 bytes com valor 56e a multiplicamos por alguma variável de 2 bytes. Em algumas arquiteturas, a operação de 64 bits seria mais pesada em computação, de modo que o compilador poderia otimizar isso para executar apenas a multiplicação de 16 bits.
NO_NAME
Algumas implementações de APL e algumas linguagens da família SNOBOL (SPITBOL, eu acho? Talvez Icon) fizeram exatamente isso (com granularidade): alteram o formato da representação dinamicamente, dependendo dos valores reais. O APL passaria de booleano para inteiro para flutuar e voltar. O SPITBOL passaria da representação em coluna dos booleanos (8 matrizes booleanas separadas armazenadas em uma matriz de bytes) para números inteiros (IIRC).
Davidbak
16

Então myIntocuparia 4 bytes com meu compilador. No entanto, o valor real 255pode ser representado com apenas 1 byte. Por que myIntnão ocupar apenas 1 byte de memória?

Isso é conhecido como codificação de comprimento variável ; existem várias codificações definidas, por exemplo, VLQ . Um dos mais famosos, no entanto, é provavelmente o UTF-8 : o UTF-8 codifica pontos de código em um número variável de bytes, de 1 a 4.

Ou a maneira mais generalizada de perguntar: Por que um tipo tem apenas um tamanho associado a ele, quando o espaço necessário para representar o valor pode ser menor que esse tamanho?

Como sempre na engenharia, trata-se de trade-offs. Não existe uma solução que possua apenas vantagens, portanto, é necessário equilibrar vantagens e compensações ao projetar sua solução.

O design que foi decidido foi usar tipos fundamentais de tamanho fixo, e o hardware / linguagens simplesmente voaram de lá.

Então, qual é a fraqueza fundamental da codificação variável , que fez com que ela fosse rejeitada em favor de esquemas com mais fome de memória? Sem endereçamento aleatório .

Qual é o índice do byte no qual o quarto ponto de código inicia em uma string UTF-8?

Depende dos valores dos pontos de código anteriores, é necessária uma verificação linear.

Certamente existem esquemas de codificação de comprimento variável que são melhores no endereçamento aleatório?

Sim, mas eles também são mais complicados. Se existe um ideal, nunca o vi ainda.

O endereçamento aleatório realmente importa mesmo assim?

Ai sim!

O problema é que qualquer tipo de agregado / matriz depende de tipos de tamanho fixo:

  • Acessando o terceiro campo de a struct? Endereçamento aleatório!
  • Acessando o terceiro elemento de uma matriz? Endereçamento aleatório!

O que significa que você tem essencialmente o seguinte compromisso:

Tipos de tamanho fixo OU Varreduras de memória linear

Matthieu M.
fonte
Isso não é tão problemático quanto você soa. Você sempre pode usar tabelas de vetores. Há uma sobrecarga de memória e uma busca extra, mas as verificações lineares não são necessárias.
Artelius 14/06
2
@ Artelius: Como você codifica a tabela de vetores quando números inteiros têm largura variável? Além disso, qual é a sobrecarga de memória da tabela vetorial ao codificar uma para números inteiros que usam 1 a 4 bytes na memória?
Matthieu M.
Olha, você está certo, no exemplo específico que o OP deu, usando tabelas vetoriais não tem vantagem nenhuma. Em vez de criar uma tabela vetorial, você também pode colocar os dados em uma matriz de elementos de tamanho fixo. No entanto, o PO também solicitou uma resposta mais geral. No Python, uma matriz de números inteiros é uma tabela vetorial de números inteiros de tamanho variável! Isso não ocorre porque resolve esse problema, mas porque o Python não sabe em tempo de compilação se os elementos da lista serão Inteiros, Flutuantes, Dictos, Strings ou Listas, todos com tamanhos diferentes, é claro.
Artelius 10/11/18
@ Artelius: Observe que em Python a matriz contém ponteiros de tamanho fixo para elementos; isso faz com que O (1) chegue a um elemento, ao custo de uma indireção.
precisa
16

A memória do computador é subdividida em pedaços endereçados consecutivamente de um determinado tamanho (geralmente 8 bits e referidos como bytes), e a maioria dos computadores é projetada para acessar com eficiência seqüências de bytes com endereços consecutivos.

Se o endereço de um objeto nunca for alterado durante a vida útil do objeto, o código fornecido poderá acessar rapidamente o objeto em questão. Uma limitação essencial com essa abordagem, no entanto, é que, se um endereço for atribuído ao endereço X, e outro endereço for atribuído ao endereço Y, que está a N bytes de distância, X não poderá crescer mais que N bytes durante a vida útil de Y, a menos que X ou Y seja movido. Para que X se mova, seria necessário que tudo no universo que contém o endereço de X seja atualizado para refletir o novo e da mesma forma que Y se mova. Embora seja possível projetar um sistema para facilitar essas atualizações (Java e .NET o gerenciam muito bem), é muito mais eficiente trabalhar com objetos que permanecerão no mesmo local durante toda a vida útil,

supercat
fonte
"X não poderá crescer mais que N bytes durante a vida útil de Y, a menos que X ou Y sejam movidos. Para que X se mova, seria necessário que tudo no universo que contém o endereço de X fosse atualizado para refletir o novo e da mesma forma que Y se mova. " Este é o ponto mais importante da IMO: objetos que usam apenas o tamanho necessário para o valor atual precisariam adicionar toneladas de sobrecarga para tamanhos / sentinelas, movimentação de memória, gráficos de referência etc. E bastante óbvio quando se pensa em como isso poderia funcionar ... mas ainda assim, vale muito a pena afirmar com tanta clareza, especialmente como poucos fizeram.
Sublinhado_
@underscore_d: idiomas como Javascript, criados desde o início para lidar com objetos de tamanho variável, podem ser incrivelmente eficientes. Por outro lado, embora seja possível simplificar sistemas de objetos de tamanho variável e torná-los rápidos, as implementações simples são lentas e as implementações rápidas são extremamente complexas.
Supercat
13

A resposta curta é: porque o padrão C ++ diz isso.

A resposta longa é: o que você pode fazer em um computador é limitado pelo hardware. É claro que é possível codificar um número inteiro em um número variável de bytes para armazenamento, mas a leitura dele exigiria instruções especiais da CPU para ter desempenho ou você poderia implementá-lo em software, mas seria muito lento. Operações de tamanho fixo estão disponíveis na CPU para carregar valores de larguras predefinidas, não existem para larguras variáveis.

Outro ponto a considerar é como a memória do computador funciona. Digamos que seu tipo inteiro possa ocupar entre 1 a 4 bytes de armazenamento. Suponha que você armazene o valor 42 em seu número inteiro: ele ocupa 1 byte e o coloca no endereço de memória X. Em seguida, você armazena sua próxima variável no local X + 1 (não estou considerando o alinhamento neste momento) e assim por diante . Mais tarde, você decide alterar seu valor para 6424.

Mas isso não se encaixa em um único byte! Então, o que você faz? Onde você coloca o resto? Você já tem algo em X + 1, então não pode colocá-lo lá. Em outro lugar? Como você saberá mais tarde onde? A memória do computador não suporta semântica de inserção: você não pode simplesmente colocar algo em um local e empurrar tudo depois para abrir espaço!

Além: O que você está falando é realmente a área de compactação de dados. Existem algoritmos de compactação para compactar tudo, portanto, pelo menos alguns deles consideram não usar mais espaço para seu número inteiro do que o necessário. No entanto, os dados compactados não são fáceis de modificar (se possível) e acabam sendo recomprimidos toda vez que você faz alterações.

John Doe, o Justo
fonte
11

Existem benefícios de desempenho em tempo de execução bastante substanciais ao fazer isso. Se você operasse com tipos de tamanho variável, teria que decodificar cada número antes de executar a operação (as instruções do código da máquina geralmente têm largura fixa), faça a operação e encontre um espaço na memória grande o suficiente para conter o resultado. Essas são operações muito difíceis. É muito mais fácil simplesmente armazenar todos os dados de maneira ineficiente.

Nem sempre é assim que é feito. Considere o protocolo Protobuf do Google. Os protobufs são projetados para transmitir dados com muita eficiência. Diminuir o número de bytes transmitidos vale o custo de instruções adicionais ao operar com os dados. Consequentemente, os protobufs usam uma codificação que codifica números inteiros em 1, 2, 3, 4 ou 5 bytes, e números inteiros menores ocupam menos bytes. Depois que a mensagem é recebida, ela é descompactada para um formato inteiro de tamanho fixo mais tradicional, mais fácil de operar. É somente durante a transmissão da rede que eles usam um número inteiro de tamanho variável com eficiência de espaço.

Cort Ammon
fonte
11

Eu gosto da analogia da casa de Sergey , mas acho que uma analogia de carro seria melhor.

Imagine tipos variáveis ​​como tipos de carros e pessoas como dados. Quando procuramos um carro novo, escolhemos aquele que melhor se adapta ao nosso objetivo. Queremos um carro pequeno e inteligente que possa acomodar apenas uma ou duas pessoas? Ou uma limusine para transportar mais pessoas? Ambos têm seus benefícios e desvantagens, como velocidade e quilometragem (pense em velocidade e uso de memória).

Se você tem uma limusine e está dirigindo sozinho, não vai encolher para caber apenas em você. Para fazer isso, você teria que vender o carro (leia: desalocar) e comprar um novo menor para você.

Continuando a analogia, você pode pensar na memória como um enorme estacionamento cheio de carros e, quando for ler, um motorista especializado treinado exclusivamente para o seu tipo de carro vai buscá-la. Se o seu carro puder mudar de tipo, dependendo das pessoas dentro dele, você precisará trazer uma série de motoristas toda vez que quiser comprar seu carro, pois eles nunca saberão que tipo de carro estará no local.

Em outras palavras, tentar determinar quanta memória você precisa ler em tempo de execução seria extremamente ineficiente e superaria o fato de que talvez você pudesse instalar mais alguns carros no estacionamento.

scohe001
fonte
10

Há algumas razões. Uma é a complexidade adicional para lidar com números de tamanho arbitrário e o desempenho que isso proporciona porque o compilador não pode mais otimizar com base no pressuposto de que todo int tem exatamente X bytes de comprimento.

Um segundo é que o armazenamento de tipos simples dessa maneira significa que eles precisam de um byte adicional para manter o comprimento. Portanto, um valor de 255 ou menos precisa, na verdade, de dois bytes neste novo sistema, não um, e no pior dos casos, agora você precisa de 5 bytes em vez de 4. Isso significa que o ganho de desempenho em termos de memória usada é menor do que você pode pense e, em alguns casos extremos, pode realmente ser uma perda líquida.

Uma terceira razão é que a memória do computador geralmente é endereçável em palavras , não em bytes. (Mas veja nota de rodapé). As palavras são um múltiplo de bytes, geralmente 4 em sistemas de 32 bits e 8 em sistemas de 64 bits. Você geralmente não consegue ler um byte individual, lê uma palavra e extrai o enésimo byte dessa palavra. Isso significa que a extração de bytes individuais de uma palavra exige um pouco mais de esforço do que apenas a leitura da palavra inteira e que é muito eficiente se toda a memória for dividida igualmente em pedaços de tamanho de palavra (ou seja, tamanho de 4 bytes). Porque, se você tiver números inteiros de tamanho arbitrário flutuando, poderá acabar com uma parte do número inteiro em uma palavra e outra na palavra seguinte, necessitando de duas leituras para obter o número inteiro completo.

Nota de rodapé: Para ser mais preciso, enquanto você endereça em bytes, a maioria dos sistemas ignora os bytes 'desiguais'. Ou seja, os endereços 0, 1, 2 e 3 leem a mesma palavra, 4, 5, 6 e 7 leem a palavra seguinte, e assim por diante.

Em uma nota não divulgada, é também por isso que os sistemas de 32 bits tinham no máximo 4 GB de memória. Os registradores usados ​​para endereçar locais na memória geralmente são grandes o suficiente para conter uma palavra, ou seja, 4 bytes, que tem um valor máximo de (2 ^ 32) -1 = 4294967295. 4294967296 bytes são 4 GB.

Buurman
fonte
8

Existem objetos que, em certo sentido, têm tamanho variável, na biblioteca padrão do C ++, como std::vector. No entanto, todos eles alocam dinamicamente a memória extra necessária. Se você escolher sizeof(std::vector<int>), obterá uma constante que não tem nada a ver com a memória gerenciada pelo objeto e, se você alocar uma matriz ou estrutura contendo std::vector<int>, ela reservará esse tamanho base em vez de colocar o armazenamento extra na mesma matriz ou estrutura . Existem algumas partes da sintaxe C que suportam algo como isto, principalmente matrizes e estruturas de comprimento variável, mas o C ++ não escolheu apoiá-las.

O padrão da linguagem define o tamanho do objeto dessa maneira para que os compiladores possam gerar código eficiente. Por exemplo, se tiver int4 bytes de comprimento em alguma implementação e você declarar acomo ponteiro ou matriz de intvalores, ele será a[i]traduzido no pseudocódigo, "desreferenciar o endereço a + 4 × i". Isso pode ser feito em tempo constante e é uma operação tão comum e importante que muitas arquiteturas de conjunto de instruções, incluindo x86 e as máquinas DEC PDP nas quais C foi originalmente desenvolvido, podem fazê-lo em uma única instrução de máquina.

Um exemplo comum do mundo real de dados armazenados consecutivamente como unidades de comprimento variável são as cadeias codificadas como UTF-8. (No entanto, o tipo subjacente de uma seqüência de caracteres UTF-8 ao compilador ainda é chare possui largura 1. Isso permite que as seqüências ASCII sejam interpretadas como UTF-8 válidas e que muitos códigos de biblioteca, como strlen()e strncpy()continuem funcionando.) A codificação de qualquer ponto de código UTF-8 pode ter de um a quatro bytes e, portanto, se você desejar o quinto ponto de código UTF-8 em uma sequência, ele poderá começar em qualquer lugar do quinto byte ao décimo sétimo byte dos dados. A única maneira de encontrá-lo é digitalizar desde o início da string e verificar o tamanho de cada ponto de código. Se você quer encontrar o quinto grafema, você também precisa verificar as classes de caracteres. Se você quiser encontrar o milionésimo caractere UTF-8 em uma string, precisará executar esse loop um milhão de vezes! Se você sabe que precisará trabalhar com índices com frequência, é possível percorrer a string uma vez e criar um índice dela - ou pode converter em uma codificação de largura fixa, como UCS-4. Encontrar o milionésimo caractere UCS-4 em uma string é apenas uma questão de adicionar quatro milhões ao endereço da matriz.

Outra complicação dos dados de comprimento variável é que, quando você os aloca, é necessário alocar a quantidade de memória que poderia usar, ou realocar dinamicamente conforme necessário. Alocar para o pior caso pode ser extremamente inútil. Se você precisar de um bloco consecutivo de memória, a realocação poderá forçar você a copiar todos os dados para um local diferente, mas permitir que a memória seja armazenada em blocos não consecutivos complica a lógica do programa.

Assim, é possível ter bignums de comprimento variável em vez de largura fixa short int, int, long inte long long int, mas seria ineficiente para alocar e usá-los. Além disso, todas as CPUs convencionais são projetadas para fazer aritmética em registros de largura fixa, e nenhuma possui instruções que operem diretamente em algum tipo de bignum de comprimento variável. Eles precisariam ser implementados em software, muito mais lentamente.

No mundo real, a maioria dos programadores (mas não todos) decidiu que os benefícios da codificação UTF-8, especialmente a compatibilidade, são importantes e que raramente nos preocupamos com outra coisa senão digitalizar uma string da frente para trás ou copiar blocos de memória de que os inconvenientes da largura variável são aceitáveis. Poderíamos usar elementos compactados de largura variável semelhantes ao UTF-8 para outras coisas. Mas raramente fazemos isso, e eles não estão na biblioteca padrão.

Davislor
fonte
7

Por que um tipo tem apenas um tamanho associado a ele, quando o espaço necessário para representar o valor pode ser menor que esse tamanho?

Principalmente por causa dos requisitos de alinhamento.

Conforme basic.align / 1 :

Os tipos de objeto têm requisitos de alinhamento que impõem restrições aos endereços nos quais um objeto desse tipo pode ser alocado.

Pense em um prédio que tenha muitos andares e cada andar tenha muitos quartos.
Cada quarto é do seu tamanho (um espaço fixo) capaz de armazenar N quantidade de pessoas ou objetos.
Com o tamanho da sala conhecido de antemão, torna o componente estrutural do edifício bem estruturado .

Se as salas não estiverem alinhadas, o esqueleto do edifício não será bem estruturado.

Joseph D.
fonte
7

Pode ser menor. Considere a função:

int foo()
{
    int bar = 1;
    int baz = 42;
    return bar+baz;
}

compila para código de montagem (g ++, x64, detalhes removidos)

$43, %eax
ret

Aqui, bare bazuse zero bytes para representar.

max630
fonte
5

então por que o myInt não ocuparia apenas 1 byte de memória?

Porque você disse para usar tanto. Ao usar um unsigned int, alguns padrões determinam que 4 bytes serão usados ​​e que o intervalo disponível para ele será de 0 a 4.294.967.295. Se você usasse um em unsigned charvez disso, provavelmente usaria apenas o byte de 1 bytes que está procurando (dependendo do padrão e o C ++ normalmente usa esses padrões).

Se não fosse por esses padrões, você teria que ter isso em mente: como o compilador ou a CPU deve saber usar apenas 1 byte em vez de 4? Posteriormente em seu programa, você pode adicionar ou multiplicar esse valor, o que exigiria mais espaço. Sempre que você faz uma alocação de memória, o sistema operacional precisa localizar, mapear e fornecer esse espaço (potencialmente trocando memória para RAM virtual); isso pode levar muito tempo. Se você alocar a memória com antecedência, não precisará aguardar a conclusão de outra alocação.

Quanto ao motivo pelo qual usamos 8 bits por byte, você pode dar uma olhada no seguinte: Qual é o histórico de por que bytes são oito bits?

Em uma nota lateral, você pode permitir que o número inteiro ultrapasse; mas você deve usar um número inteiro assinado, os padrões C \ C ++ declaram que o excesso de número inteiro resulta em um comportamento indefinido. Estouro de número inteiro

Blerg
fonte
5

Algo simples que muitas respostas parecem faltar:

porque atende aos objetivos de design do C ++.

A capacidade de calcular o tamanho de um tipo em tempo de compilação permite que um grande número de suposições simplificadoras sejam feitas pelo compilador e pelo programador, o que traz muitos benefícios, principalmente no que diz respeito ao desempenho. Obviamente, tipos de tamanho fixo têm armadilhas concomitantes, como excesso de número inteiro. É por isso que diferentes idiomas tomam diferentes decisões de design. (Por exemplo, números inteiros Python são essencialmente de tamanho variável.)

Provavelmente, o principal motivo pelo qual o C ++ se inclina tão fortemente aos tipos de tamanho fixo é seu objetivo de compatibilidade com o C. No entanto, como o C ++ é uma linguagem de tipo estatístico que tenta gerar código muito eficiente e evita adicionar coisas não especificadas explicitamente pelo programador, os tipos de tamanho fixo ainda fazem muito sentido.

Então, por que C optou por tipos de tamanho fixo em primeiro lugar? Simples. Ele foi projetado para escrever sistemas operacionais da era dos anos 70, software de servidor e utilitários; coisas que forneceram infraestrutura (como gerenciamento de memória) para outro software. Em um nível tão baixo, o desempenho é crítico, e o compilador também está fazendo exatamente o que você pede.

Artelius
fonte
5

Alterar o tamanho de uma variável exigiria realocação e isso geralmente não vale os ciclos adicionais da CPU em comparação com o desperdício de mais alguns bytes de memória.

As variáveis ​​locais ficam em uma pilha que é muito rápida de manipular quando essas variáveis ​​não mudam de tamanho. Se você decidiu expandir o tamanho de uma variável de 1 byte para 2 bytes, precisará mover tudo na pilha por um byte para criar esse espaço. Isso pode custar potencialmente muitos ciclos de CPU, dependendo de quantas coisas precisam ser movidas.

Outra maneira de fazer isso é fazer de cada variável um ponteiro para um local de pilha, mas você gastaria ainda mais ciclos de CPU e memória dessa maneira, na verdade. Os ponteiros são de 4 bytes (endereçamento de 32 bits) ou 8 bytes (endereçamento de 64 bits); portanto, você já está usando 4 ou 8 para o ponteiro e, em seguida, o tamanho real dos dados no heap. Ainda há um custo para realocação neste caso. Se você precisar realocar os dados do heap, pode ter sorte e ter espaço para expandi-los em linha, mas às vezes é necessário movê-los para outro lugar no heap para ter o bloco contíguo de memória do tamanho desejado.

É sempre mais rápido decidir quanta memória usar com antecedência. Se você pode evitar o dimensionamento dinâmico, obtém desempenho. Desperdiçar memória geralmente vale o ganho de desempenho. É por isso que os computadores têm toneladas de memória. :)

Chris Rollins
fonte
3

O compilador pode fazer muitas alterações no seu código, desde que as coisas ainda funcionem (a regra "no estado em que se encontra").

Seria possível usar uma instrução de movimentação literal de 8 bits em vez da mais longa (32/64 bits) necessária para mover um total int. No entanto, você precisaria de duas instruções para concluir o carregamento, pois seria necessário definir o registro como zero antes de fazer o carregamento.

É simplesmente mais eficiente (pelo menos de acordo com os principais compiladores) manipular o valor como 32 bits. Na verdade, ainda não vi um compilador x86 / x86_64 que faria carregamento de 8 bits sem montagem embutida.

No entanto, as coisas são diferentes quando se trata de 64 bits. Ao projetar as extensões anteriores (de 16 a 32 bits) de seus processadores, a Intel cometeu um erro. Aqui está uma boa representação de como eles são. O principal argumento aqui é que, quando você escreve para AL ou AH, o outro não é afetado (é justo, esse era o ponto e fazia sentido na época). Mas fica interessante quando eles o expandem para 32 bits. Se você escrever os bits inferiores (AL, AH ou AX), nada acontecerá com os 16 bits superiores do EAX, o que significa que, se você deseja promover a charpara a int, precisará limpar a memória primeiro, mas não tem como na verdade, usando apenas esses 16 bits principais, tornando esse "recurso" mais doloroso do que qualquer coisa.

Agora, com 64 bits, a AMD fez um trabalho muito melhor. Se você tocar em algo nos 32 bits inferiores, os 32 bits superiores serão simplesmente configurados para 0. Isso leva a algumas otimizações reais que você pode ver neste raio de ação . Você pode ver que carregar algo de 8 bits ou 32 bits é feito da mesma maneira, mas quando você usa variáveis ​​de 64 bits, o compilador usa uma instrução diferente, dependendo do tamanho real do seu literal.

Então, como você pode ver aqui, os compiladores podem alterar totalmente o tamanho real de sua variável dentro da CPU, se produzir o mesmo resultado, mas não faz sentido fazê-lo para tipos menores.

meneldal
fonte
correção: como se . Além disso, não vejo como, se uma carga / armazenamento menor pudesse ser usada, isso liberaria os outros bytes para uso - o que parece ser o que o OP se pergunta: não apenas evitando tocar na memória desnecessária pelo valor atual, mas ser capaz de dizer quantos bytes ler e mudar magicamente toda a RAM em tempo de execução, para que seja atingida uma idéia filosófica estranha de eficiência de espaço (não importa o custo de desempenho gigantesco!) ... 'resolver' isso. O que uma CPU / OS precisaria fazer seria tão complexo que responde à pergunta mais claramente da IMO.
Underscore_d
1
Você realmente não pode "economizar memória" nos registros. A menos que você esteja tentando fazer algo estranho abusando do AH e do AL, não poderá ter vários valores diferentes no mesmo registro de uso geral. Variáveis ​​locais geralmente ficam nos registradores e nunca vão para a RAM se não houver necessidade.
meneldal