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 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:
int square(int num) {
return num * num;
}
compila (com o GCC 8.1 e -fomit-frame-pointer -O3
por simplicidade) para:
square(int):
imul edi, edi
mov eax, edi
ret
isso significa:
- o
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 nada
- a multiplicação é uma única instrução (
imul
), que é muito rápida
- 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 unsigned
ou do uint32_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).
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.