É uma boa prática usar tipos de dados menores para variáveis ​​para economizar memória?

32

Quando aprendi a linguagem C ++ pela primeira vez, aprendi que além de int, float etc., existiam versões menores ou maiores desses tipos de dados na linguagem. Por exemplo, eu poderia chamar uma variável x

int x;
or 
short int x;

A principal diferença é que short int ocupa 2 bytes de memória, enquanto int ocupa 4 bytes, e short int possui um valor menor, mas também podemos chamar isso para torná-lo ainda menor:

int x;
short int x;
unsigned short int x;

o que é ainda mais restritivo.

Minha pergunta aqui é se é uma boa prática usar tipos de dados separados de acordo com os valores que sua variável assume dentro do programa. É uma boa ideia sempre declarar variáveis ​​de acordo com esses tipos de dados?

Bugster
fonte
3
você conhece o padrão de design do Flyweight ? "um objeto que minimiza o uso da memória compartilhando o máximo de dados possível com outros objetos semelhantes; é uma maneira de usar objetos em grandes números quando uma simples representação repetida usaria uma quantidade inaceitável de memória ..."
gnat
5
Com as configurações padrão do compilador de compactação / alinhamento, as variáveis ​​serão alinhadas aos limites de 4 bytes de qualquer maneira, portanto pode não haver nenhuma diferença.
Nikie 17/04/12
36
Caso clássico de otimização prematura.
scarfridge
1
@nikie - eles podem estar alinhados em um limite de 4 bytes em um processador x86, mas isso não é verdade em geral. O MSP430 coloca char em qualquer endereço de byte e tudo o mais em um endereço de byte uniforme. Eu acho que AVR-32 e ARM Cortex-M são os mesmos.
uɐɪ
3
A segunda parte da sua pergunta implica que adicionar de unsignedalguma forma faz com que um número inteiro ocupe menos espaço, o que é obviamente falso. Ele terá a mesma contagem de valores representáveis ​​discretos (dar ou receber 1, dependendo de como o sinal é representado), mas apenas mudando exclusivamente para o positivo.
underscore_d

Respostas:

41

Na maioria das vezes, o custo do espaço é insignificante e você não deve se preocupar com isso; no entanto, você deve se preocupar com as informações extras que você está fornecendo ao declarar um tipo. Por exemplo, se você:

unsigned int salary;

Você está fornecendo uma informação útil a outro desenvolvedor: o salário não pode ser negativo.

A diferença entre short, int, long raramente causa problemas de espaço no seu aplicativo. É mais provável que você acidentalmente suponha que um número sempre caiba em algum tipo de dados. Provavelmente é mais seguro sempre usar int, a menos que você tenha 100% de certeza de que seus números sempre serão muito pequenos. Mesmo assim, é improvável economizar uma quantidade notável de espaço.

Oleksi
fonte
5
É verdade que raramente causará problemas nos dias de hoje, mas se você estiver projetando uma biblioteca ou uma classe que outro desenvolvedor usará, bem, isso é outra questão. Talvez eles precisem de armazenamento para um milhão desses objetos. Nesse caso, a diferença é grande - 4 MB em comparação com 2 MB apenas para esse campo.
dodgy_coder
30
Usar unsignedneste caso é uma má ideia: não apenas o salário não pode ser negativo, mas a diferença entre dois salários também não pode ser negativa. (Em geral, usar unsigned para nada, mas bit-twiddling e ter comportamento definido no estouro é uma má idéia.)
zvrba
15
@zvrba: A diferença entre dois salários não é em si um salário e, portanto, é legítimo usar um tipo diferente que é assinado.
precisa saber é o seguinte
12
@ JeremyP Sim, mas se você estiver usando C (e parece que isso também é verdade em C ++), a subtração de número inteiro não assinado resulta em um int não assinado , que não pode ser negativo. Ele pode se transformar no valor certo se você o converter em um int assinado, mas o resultado do cálculo é um int não assinado. Veja também esta resposta para mais estranhezas de computação assinadas / não assinadas - e é por isso que você nunca deve usar variáveis ​​não assinadas, a menos que esteja realmente mexendo nos bits.
Tacroy
5
@zvrba: A diferença é uma quantidade monetária, mas não um salário. Agora você pode argumentar que um salário também é uma quantidade monetária (restrita a números positivos e 0, validando a entrada que é o que a maioria das pessoas faria), mas a diferença entre dois salários não é, em si, um salário.
precisa saber é o seguinte
29

O OP não disse nada sobre o tipo de sistema para o qual está escrevendo programas, mas presumo que o OP estivesse pensando em um PC típico com GB de memória, já que o C ++ é mencionado. Como um dos comentários diz, mesmo com esse tipo de memória, se você tiver vários milhões de itens de um tipo - como uma matriz -, o tamanho da variável poderá fazer a diferença.

Se você entrar no mundo dos sistemas embarcados - o que não está realmente fora do escopo da questão, já que o OP não o limita aos PCs -, o tamanho dos tipos de dados é muito importante. Acabei de terminar um projeto rápido em um microcontrolador de 8 bits que possui apenas 8K palavras de memória de programa e 368 bytes de RAM. Lá, obviamente, cada byte conta. Nunca se usa uma variável maior do que o necessário (tanto do ponto de vista do espaço quanto do tamanho do código - os processadores de 8 bits usam muitas instruções para manipular dados de 16 e 32 bits). Por que usar uma CPU com recursos tão limitados? Em grandes quantidades, eles podem custar apenas um quarto.

Atualmente, estou fazendo outro projeto incorporado com um microcontrolador de 32 bits baseado em MIPS que possui 512K bytes de flash e 128K bytes de RAM (e custa cerca de US $ 6 em quantidade). Como em um PC, o tamanho "natural" dos dados é de 32 bits. Agora, torna-se mais eficiente, em termos de código, usar ints para a maioria das variáveis, em vez de caracteres ou curtos. Mais uma vez, porém, qualquer tipo de matriz ou estrutura deve ser considerado se tipos de dados menores são necessários. Ao contrário de compiladores para sistemas maiores, é mais provável variáveis em uma estrutura vai ser embalado em um sistema embarcado. Tomo o cuidado de sempre tentar colocar todas as variáveis ​​de 32 bits primeiro, depois 16 e 8 bits para evitar "buracos".

tcrosley
fonte
10
+1 pelo fato de regras diferentes se aplicarem a sistemas incorporados. O fato de o C ++ ser mencionado não significa que o destino é um PC. Um dos meus projetos recentes foi escrito em C ++ em um processador com 32k de RAM e 256K de Flash.
uɐɪ
13

A resposta depende do seu sistema. Geralmente, aqui estão as vantagens e desvantagens do uso de tipos menores:

Vantagens

  • Tipos menores usam menos memória na maioria dos sistemas.
  • Tipos menores fornecem cálculos mais rápidos em alguns sistemas. Particularmente verdadeiro para float vs double em muitos sistemas. E tipos int menores também fornecem código significativamente mais rápido em CPUs de 8 ou 16 bits.

Desvantagens

  • Muitas CPUs possuem requisitos de alinhamento. Alguns acessam dados alinhados mais rapidamente que os desalinhados. Alguns devem ter os dados alinhados para poder acessá-los. Os tipos inteiros maiores são iguais a uma unidade alinhada e, portanto, provavelmente não estão desalinhados. Isso significa que o compilador pode ser forçado a colocar números inteiros menores em números maiores. E se os tipos menores fizerem parte de uma estrutura maior, você poderá inserir vários bytes de preenchimento silenciosamente em qualquer lugar da estrutura pelo compilador, para corrigir o alinhamento.
  • Conversões implícitas perigosas. C e C ++ têm várias regras obscuras e perigosas sobre como as variáveis ​​são promovidas para as maiores, implicitamente sem uma conversão de tipo. Existem dois conjuntos de regras de conversão implícitas entrelaçadas entre si, denominadas "regras de promoção inteira" e "conversões aritméticas comuns". Leia mais sobre eles aqui . Essas regras são uma das causas mais comuns de erros em C e C ++. Você pode evitar muitos problemas simplesmente usando o mesmo tipo inteiro em todo o programa.

Meu conselho é assim:

system                             int types

small/low level embedded system    stdint.h with smaller types
32-bit embedded system             stdint.h, stick to int32_t and uint32_t.
32-bit desktop system              Only use (unsigned) int and long long.
64-bit system                      Only use (unsigned) int and long long.

Como alternativa, você pode usar the int_leastn_tou int_fastn_tfrom stdint.h, onde n é o número 8, 16, 32 ou 64. int_leastn_ttype significa "Quero que sejam pelo menos n bytes, mas não me importo se o compilador o alocar como um tipo maior para se adequar ao alinhamento ".

int_fastn_t significa "Eu quero que isso tenha n bytes de comprimento, mas se o código for executado mais rapidamente, o compilador deve usar um tipo maior que o especificado".

Geralmente, os vários tipos stdint.h são muito melhores práticas do que simples int, etc, porque são portáteis. A intenção intera não fornecer uma largura especificada apenas para torná-lo portátil. Mas, na realidade, é difícil portar porque você nunca sabe o tamanho que será em um sistema específico.


fonte
Spot sobre o alinhamento. No meu projeto atual, o uso gratuito do uint8_t em um MSP430 de 16 bits travou o MCU de maneiras misteriosas (o acesso desalinhado provavelmente aconteceu em algum lugar, talvez a falha do GCC, talvez não) - apenas substituir todo o uint8_t por 'não assinado' eliminou as falhas. O uso de tipos de 8 bits em arcos de> 8 bits, se não fatal, é pelo menos ineficiente: o compilador gera instruções adicionais 'e reg, 0xff'. Use 'int / unsigned' para portabilidade e liberte o compilador de restrições extras.
187 alexei
11

Dependendo de como o sistema operacional específico funciona, geralmente você espera que a memória seja alocada sem otimização, de modo que, quando você pede um byte, uma palavra ou outro tipo de dado pequeno a ser alocado, o valor ocupa um registro inteiro. próprio. Como seu compilador ou intérprete trabalha para interpretar isso, no entanto, é outra coisa; portanto, se você compilar um programa em C #, por exemplo, o valor poderá ocupar fisicamente um registro para si mesmo; no entanto, o valor será verificado nos limites para garantir que você não tente armazenar um valor que excederá os limites do tipo de dados pretendido.

Em termos de desempenho, e se você é realmente pedante sobre essas coisas, provavelmente é mais rápido simplesmente usar o tipo de dados que mais se aproxima do tamanho do registro de destino, mas você perde todo esse adorável açúcar sintático que facilita o trabalho com variáveis .

Como isso te ajuda? Bem, depende de você decidir que tipo de situação você está codificando. Para quase todos os programas que já escrevi, basta confiar no seu compilador para otimizar as coisas e usar o tipo de dados que é mais útil para você. Se você precisar de alta precisão, use os tipos de dados maiores de ponto flutuante. Se estiver trabalhando apenas com valores positivos, provavelmente você poderá usar um número inteiro não assinado, mas, na maioria das vezes, basta usar o tipo de dados int.

Se, no entanto, você tiver alguns requisitos de dados muito rígidos, como escrever um protocolo de comunicação ou algum tipo de algoritmo de criptografia, o uso de tipos de dados com intervalo verificado pode ser muito útil, principalmente se você estiver tentando evitar problemas relacionados a excedentes / subunidos de dados ou valores de dados inválidos.

A única outra razão pela qual posso pensar em detalhes para usar tipos de dados específicos é quando você está tentando comunicar a intenção dentro do seu código. Se você usar uma abreviação, por exemplo, estará dizendo a outros desenvolvedores que está permitindo números positivos e negativos dentro de um intervalo de valores muito pequeno.

S.Robins
fonte
6

Como comentou scarfridge , este é um

Caso clássico de otimização prematura .

Tentar otimizar o uso da memória pode afetar outras áreas de desempenho, e as regras de ouro da otimização são:

A primeira regra de otimização de programa: não faça isso .

A Segunda Regra da Otimização de Programas (somente para especialistas!): Não faça isso ainda . "

- Michael A. Jackson

Para saber se agora é a hora de otimizar, é necessário fazer comparações e testes. Você precisa saber onde seu código está sendo ineficiente, para poder direcionar suas otimizações.

Para determinar se a versão otimizada do código é realmente melhor do que a implementação ingênua a qualquer momento, você precisa compará-las lado a lado com os mesmos dados.

Além disso, lembre-se de que apenas porque uma determinada implementação é mais eficiente na geração atual de CPUs, não significa que sempre será assim. Minha resposta à pergunta A micro-otimização é importante ao codificar? detalha um exemplo da experiência pessoal em que uma otimização obsoleta resultou em uma desaceleração em ordem de magnitude.

Em muitos processadores, os acessos à memória não alinhados são significativamente mais caros do que os acessos à memória alinhados. A inserção de alguns curtos em sua estrutura pode significar apenas que seu programa deve executar a operação de empacotar / descompactar toda vez que você tocar em qualquer valor.

Por esse motivo, os compiladores modernos ignoram suas sugestões. Como comenta nikie :

Com as configurações padrão do compilador de compactação / alinhamento, as variáveis ​​serão alinhadas aos limites de 4 bytes de qualquer maneira, portanto pode não haver nenhuma diferença.

Segundo, adivinhe o seu compilador por sua conta e risco.

Há um lugar para essas otimizações, ao trabalhar com conjuntos de dados de terabytes ou microcontroladores incorporados, mas para a maioria de nós, isso não é realmente uma preocupação.

Mark Booth
fonte
3

A principal diferença é que short int ocupa 2 bytes de memória, enquanto int ocupa 4 bytes, e short int possui um valor menor, mas também podemos chamar isso para torná-lo ainda menor:

Isto está incorreto. Você não pode fazer suposições sobre quantos bytes cada tipo contém, além de charter um byte e pelo menos 8 bits por byte, além de o tamanho de cada tipo ser maior ou igual ao anterior.

Os benefícios de desempenho são incrivelmente minúsculos para as variáveis ​​de pilha - elas provavelmente serão alinhadas / preenchidas de qualquer maneira.

Por causa disso, shorte longpraticamente não há uso hoje em dia, e você quase sempre está melhor usando int.


Claro, também há o stdint.hque é perfeitamente bom de usar quando intnão o corta. Se você estiver alocando enormes matrizes de números inteiros / estruturas, então intX_tfaz sentido, pois você pode ser eficiente e confiar no tamanho do tipo. Isso não é prematuro, pois você pode economizar megabytes de memória.

Pubby
fonte
1
Na verdade, com o advento de ambientes de 64 bits, longpode ser diferente int. Se o seu compilador for LP64, inttiver 32 bits e long64 bits, você descobrirá que ints ainda podem estar alinhados em 4 bytes (meu compilador, por exemplo).
precisa saber é o seguinte
1
@ JeremyP Sim, eu disse o contrário ou algo assim?
Pubby
Sua última frase que afirma curta e longa praticamente não tem utilidade. Longo certamente tem um uso, mesmo que apenas como o tipo de baseint64_t
JeremyP
@ JeremyP: Você pode viver bem com int e por muito tempo.
gnasher729
@ gnasher729: O que você usa se precisar de uma variável que possa conter valores superiores a 65 mil, mas nunca tanto quanto um bilhão? int32_t,, int_fast32_te longsão todas boas opções, long longé apenas um desperdício e intnão é portátil.
Ben Voigt
3

Isso será de um tipo de ponto de vista de OOP e / ou empresa / aplicativo e pode não ser aplicável em certos campos / domínios, mas eu meio que quero trazer à tona o conceito de obsessão primitiva .

É uma boa ideia usar diferentes tipos de dados para diferentes tipos de informações em seu aplicativo. No entanto, provavelmente NÃO é uma boa ideia usar os tipos internos para isso, a menos que você tenha alguns problemas sérios de desempenho (que foram medidos e verificados e assim por diante).

Se quisermos modelar temperaturas em Kelvin em nosso aplicativo, PODEMOS usar um ushortou uintalgo semelhante para denotar que "a noção de graus negativos Kelvin é absurda e um erro de lógica de domínio". A idéia por trás disso é sólida, mas você não está indo até o fim. O que percebemos é que não podemos ter valores negativos, por isso é útil podermos obter o compilador para garantir que ninguém atribua um valor negativo a uma temperatura Kelvin. Também é verdade que você não pode executar operações bit a bit em temperaturas. E você não pode adicionar uma medida de peso (kg) a uma temperatura (K). Mas se você modelar temperatura e massa como uints, podemos fazer exatamente isso.

O uso de tipos internos para modelar nossas entidades DOMAIN provavelmente causará algum código confuso, verificações perdidas e invariantes quebrados. Mesmo que um tipo capture ALGUM parte da entidade (não pode ser negativo), ele provavelmente perderá outras (não pode ser usado em expressões aritméticas arbitrárias, não pode ser tratado como uma matriz de bits etc.)

A solução é definir novos tipos que encapsulam os invariantes. Dessa forma, você pode ter certeza de que dinheiro é dinheiro e distâncias são distâncias, e não pode adicioná-las, e não pode criar uma distância negativa, mas PODE criar uma quantidade negativa de dinheiro (ou uma dívida). Obviamente, esses tipos usarão os tipos internos internamente, mas isso está oculto para os clientes. Em relação à sua pergunta sobre desempenho / consumo de memória, esse tipo de coisa pode permitir que você altere como as coisas são armazenadas internamente, sem alterar a interface de suas funções que operam nas entidades do seu domínio, caso você descubra isso, a shorté demais ampla.

sara
fonte
1

Sim, claro. É uma boa idéia usar uint_least8_tpara dicionários, matrizes constantes enormes, buffers etc. É melhor usar uint_fast8_tpara fins de processamento.

uint8_least_t(armazenamento) -> uint8_fast_t(processamento) -> uint8_least_t(armazenamento).

Por exemplo, você está usando o símbolo de 8 bits source, códigos de 16 bits dictionariese 32 bits constants. Do que você está processando operações de 10 a 15 bits com elas e produz 8 bits destination.

Vamos imaginar que você precise processar 2 gigabytes de source. A quantidade de operações de bits é enorme. Você receberá um ótimo bônus de desempenho se mudar para tipos rápidos durante o processamento. Tipos rápidos podem ser diferentes para cada família de CPU. Você pode incluir stdint.he usar uint_fast8_t, uint_fast16_t, uint_fast32_t, etc.

Você poderia usar em uint_least8_tvez de uint8_tportabilidade. Mas ninguém sabe realmente qual CPU moderna usará esse recurso. A máquina VAC é uma peça de museu. Então talvez seja um exagero.

puchu
fonte
1
Embora você possa entender os tipos de dados listados, explique por que eles são melhores, em vez de apenas afirmar que são. Para pessoas como eu que não estão familiarizadas com esses tipos de dados, tive que pesquisá-los no Google para entender do que você está falando.
Peter M