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 myInt
ocuparia 4 bytes com meu compilador. No entanto, o valor real 255
pode ser representado com apenas 1 byte. Por que myInt
nã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?
unsinged
valor 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.unsigned int
valor.std::vector<X>
sempre tem o mesmo tamanho, ou seja,sizeof(std::vector<X>)
é uma constante em tempo de compilação.Respostas:
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:
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
int
variá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:
compila (com o GCC 8.1 e
-fomit-frame-pointer -O3
por simplicidade) para:isso significa:
int num
parâ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 nadaimul
), que é muito rápidaEditar: 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
com o código equivalente para uma largura fora do padrão
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
unsigned
ou douint32_t
tipo, 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).
fonte
git
metadados) 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.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.
fonte
É 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.
fonte
int
armazenam o número de elementos nessa matriz. Isso porint
si só terá um tamanho fixo novamente.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:
Se você simplesmente instrui o computador a observar o primeiro byte na fita,
xx
como ele sabe se o tipo para ou não por aí ou prossegue para o próximo byte? Se você possui um número como255
(hexadecimalFF
) ou um número como65535
(hexadecimalFFFF
), o primeiro byte é sempreFF
.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çox = y
? Seint
pudesse 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 sey
um argumento for passado de outro trecho de código compilado separadamente?fonte
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.
fonte
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.
fonte
56
e 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.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.
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:
struct
? Endereçamento aleatório!O que significa que você tem essencialmente o seguinte compromisso:
Tipos de tamanho fixo OU Varreduras de memória linear
fonte
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,
fonte
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.
fonte
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.
fonte
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.
fonte
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.
fonte
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ê escolhersizeof(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 contendostd::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
int
4 bytes de comprimento em alguma implementação e você declarara
como ponteiro ou matriz deint
valores, 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 é
char
e possui largura 1. Isso permite que as seqüências ASCII sejam interpretadas como UTF-8 válidas e que muitos códigos de biblioteca, comostrlen()
estrncpy()
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 int
elong 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.
fonte
Principalmente por causa dos requisitos de alinhamento.
Conforme basic.align / 1 :
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.
fonte
Pode ser menor. Considere a função:
compila para código de montagem (g ++, x64, detalhes removidos)
Aqui,
bar
ebaz
use zero bytes para representar.fonte
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 emunsigned char
vez 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
fonte
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.
fonte
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. :)
fonte
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
char
para aint
, 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.
fonte