¿Es una buena práctica usar tipos de datos más pequeños para las variables para ahorrar memoria?

32

Cuando aprendí el lenguaje C ++ por primera vez, aprendí que además de int, float, etc., existían versiones más pequeñas o más grandes de estos tipos de datos dentro del lenguaje. Por ejemplo, podría llamar a una variable x

int x;
or 
short int x;

La principal diferencia es que short int toma 2 bytes de memoria, mientras que int toma 4 bytes, y short int tiene un valor menor, pero también podríamos llamar a esto para hacerlo aún más pequeño:

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

lo cual es aún más restrictivo.

Mi pregunta aquí es si es una buena práctica usar tipos de datos separados según los valores que tome su variable dentro del programa. ¿Es una buena idea declarar siempre las variables de acuerdo con estos tipos de datos?

Bugster
fuente
3
¿ Conoces el patrón de diseño Flyweight ? "un objeto que minimiza el uso de memoria al compartir la mayor cantidad de datos posible con otros objetos similares; es una forma de usar objetos en grandes cantidades cuando una simple representación repetida usaría una cantidad inaceptable de memoria ..."
gnat
55
Con la configuración estándar del compilador de empaquetamiento / alineación, las variables se alinearán a límites de 4 bytes de todos modos, por lo que puede que no haya ninguna diferencia.
nikie
36
Caso clásico de optimización prematura.
scarfridge
1
@nikie: pueden estar alineados en un límite de 4 bytes en un procesador x86, pero esto no es cierto en general. MSP430 coloca char en cualquier dirección de byte y todo lo demás en una dirección de byte par. Creo que AVR-32 y ARM Cortex-M son lo mismo.
uɐɪ
3
La segunda parte de su pregunta implica que agregar de unsignedalguna manera hace que un número entero ocupe menos espacio, lo que por supuesto es falso. Tendrá el mismo recuento de valores representables discretos (más o menos 1 dependiendo de cómo se represente el signo), pero se desplazó exclusivamente a positivo.
underscore_d

Respuestas:

41

La mayoría de las veces el costo del espacio es insignificante y no debe preocuparse por ello, sin embargo, debe preocuparse por la información adicional que está dando al declarar un tipo. Por ejemplo, si usted:

unsigned int salary;

Está dando información útil a otro desarrollador: el salario no puede ser negativo.

La diferencia entre short, int, long rara vez va a causar problemas de espacio en su aplicación. Es más probable que accidentalmente haga la suposición falsa de que un número siempre encajará en algún tipo de datos. Probablemente sea más seguro usar siempre int a menos que esté 100% seguro de que sus números siempre serán muy pequeños. Incluso entonces, es poco probable que le ahorre una cantidad notable de espacio.

Oleksi
fuente
55
Es cierto que rara vez causará problemas en estos días, pero si está diseñando una biblioteca o una clase que otro desarrollador usará, bueno, eso es otro asunto. Quizás necesiten almacenamiento para un millón de estos objetos, en cuyo caso la diferencia es grande: 4 MB en comparación con 2 MB solo para este campo.
dodgy_coder
30
Usar unsigneden este caso es una mala idea: no solo el salario no puede ser negativo, sino que la diferencia entre dos salarios tampoco puede ser negativa. (En general, la utilización sin firmar para nada más que de bits haciendo girar y tener un comportamiento definido en caso de desbordamiento es una mala idea.)
zvrba
16
@zvrba: La diferencia entre dos salarios no es en sí un salario, por lo que es legítimo usar un tipo diferente que esté firmado.
JeremyP
12
@JeremyP Sí, pero si está utilizando C (y parece que esto también es cierto en C ++), la resta entera sin signo da como resultado un int sin signo , que no puede ser negativo. Podría convertirse en el valor correcto si lo convierte en un int con signo, pero el resultado del cálculo es un int sin signo. Consulte también esta respuesta para obtener más rarezas de cálculo con signo / sin signo, por lo que nunca debe usar variables sin signo a menos que realmente esté girando bits.
Tacroy
55
@zvrba: La diferencia es una cantidad monetaria pero no un salario. Ahora podría argumentar que un salario también es una cantidad monetaria (limitada a números positivos y 0 al validar la entrada, que es lo que haría la mayoría de la gente), pero la diferencia entre dos salarios no es en sí un salario.
JeremyP
29

El OP no dijo nada sobre el tipo de sistema para el que están escribiendo programas, pero supongo que el OP estaba pensando en una PC típica con GB de memoria desde que se menciona C ++. Como dice uno de los comentarios, incluso con ese tipo de memoria, si tiene varios millones de elementos de un tipo, como una matriz, el tamaño de la variable puede marcar la diferencia.

Si ingresa al mundo de los sistemas embebidos, que no está realmente fuera del alcance de la pregunta, ya que el OP no lo limita a las PC, entonces el tamaño de los tipos de datos es muy importante. Acabo de terminar un proyecto rápido en un microcontrolador de 8 bits que tiene solo 8K palabras de memoria de programa y 368 bytes de RAM. Ahí, obviamente, cada byte cuenta. Uno nunca usa una variable más grande de lo que necesita (tanto desde el punto de vista del espacio como del tamaño del código: los procesadores de 8 bits usan muchas instrucciones para manipular datos de 16 y 32 bits). ¿Por qué usar una CPU con recursos tan limitados? En grandes cantidades, pueden costar tan poco como una cuarta parte.

Actualmente estoy haciendo otro proyecto integrado con un microcontrolador basado en MIPS de 32 bits que tiene 512K bytes de flash y 128K bytes de RAM (y cuesta alrededor de $ 6 en cantidad). Al igual que con una PC, el tamaño de datos "natural" es de 32 bits. Ahora se vuelve más eficiente, en términos de código, usar ints para la mayoría de las variables en lugar de caracteres o cortos. Pero una vez más, cualquier tipo de matriz o estructura debe considerarse si se justifican los tipos de datos más pequeños. A diferencia de los compiladores para sistemas más grandes, es más probable que las variables en una estructura se empaqueten en un sistema embebido. Me aseguro de intentar siempre poner primero todas las variables de 32 bits, luego 16 bits, luego 8 bits para evitar cualquier "agujero".

tcrosley
fuente
10
+1 por el hecho de que se aplican diferentes reglas a los sistemas integrados. El hecho de que se mencione C ++ no significa que el objetivo sea una PC. Uno de mis proyectos recientes fue escrito en C ++ en un procesador con 32k de RAM y 256K de Flash.
uɐɪ
13

La respuesta depende de su sistema. En general, aquí están las ventajas y desventajas de usar tipos más pequeños:

Ventajas

  • Los tipos más pequeños usan menos memoria en la mayoría de los sistemas.
  • Los tipos más pequeños dan cálculos más rápidos en algunos sistemas. Particularmente cierto para flotante vs doble en muchos sistemas. Y los tipos int más pequeños también proporcionan un código significativamente más rápido en las CPU de 8 o 16 bits.

Desventajas

  • Muchas CPU tienen requisitos de alineación. Algunos acceden a datos alineados más rápido que los no alineados. Algunos deben tener los datos alineados para poder acceder a ellos. Los tipos enteros más grandes equivalen a una unidad alineada, por lo que probablemente no estén desalineados. Esto significa que el compilador podría verse obligado a poner sus enteros más pequeños en los más grandes. Y si los tipos más pequeños son parte de una estructura más grande, es posible que el compilador inserte silenciosamente varios bytes de relleno en cualquier parte de la estructura para corregir la alineación.
  • Peligrosas conversiones implícitas. C y C ++ tienen varias reglas oscuras y peligrosas sobre cómo se promueven las variables a las más grandes, implícitamente sin un tipo de conversión. Hay dos conjuntos de reglas de conversión implícitas entrelazadas entre sí, llamadas "reglas de promoción de enteros" y "conversiones aritméticas habituales". Lea más sobre ellos aquí . Estas reglas son una de las causas más comunes de errores en C y C ++. Puede evitar muchos problemas simplemente usando el mismo tipo entero en todo el programa.

Mi consejo es que te guste esto:

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.

Alternativamente, puede usar int_leastn_to int_fastn_tdesde stdint.h, donde n es el número 8, 16, 32 o 64. int_leastn_ttipo significa "Quiero que esto sea al menos n bytes pero no me importa si el compilador lo asigna como un tipo más grande para adaptarse a la alineación ".

int_fastn_t significa "Quiero que esto tenga una longitud de n bytes, pero si hace que mi código se ejecute más rápido, el compilador debe usar un tipo más grande que el especificado".

En general, los diversos tipos stdint.h son una práctica mucho mejor que los simples int, etc., porque son portátiles. La intención intera no darle un ancho específico únicamente para hacerlo portátil. Pero en realidad, es difícil de portar porque nunca se sabe qué tan grande será en un sistema específico.


fuente
Spot en relación con la alineación. En mi proyecto actual, el uso gratuito de uint8_t en un MSP430 de 16 bits bloqueó el MCU de maneras misteriosas (lo más probable es que el acceso desalineado ocurriera en algún lugar, tal vez por culpa de GCC, tal vez no), simplemente reemplazando todo uint8_t con 'unsigned' eliminó los bloqueos. El uso de tipos de 8 bits en arcos de> 8 bits si no es fatal es al menos ineficiente: el compilador genera instrucciones adicionales 'y reg, 0xff'. Use 'int / unsigned' para la portabilidad y libere al compilador de restricciones adicionales.
alexei
11

Dependiendo de cómo funcione el sistema operativo específico, generalmente espera que la memoria se asigne sin optimizar, de modo que cuando solicita un byte, una palabra u otro tipo de datos pequeños, el valor ocupa un registro completo, todo es muy propio. Sin embargo, cómo funciona su compilador o intérprete para interpretar esto es otra cosa, por lo que si compilara un programa en C #, por ejemplo, el valor podría ocupar físicamente un registro para sí mismo, sin embargo, el valor se verificará en el límite para asegurarse de que no intente almacenar un valor que exceda los límites del tipo de datos deseado.

En cuanto al rendimiento, y si es realmente pedante con respecto a estas cosas, es probable que sea más rápido simplemente usar el tipo de datos que más se aproxime al tamaño del registro objetivo, pero luego se pierde todo ese encantador azúcar sintáctico que hace que trabajar con variables sea tan fácil .

Cómo te ayuda esto? Bueno, realmente depende de ti decidir qué tipo de situación estás codificando. Para casi todos los programas que he escrito, es suficiente simplemente confiar en su compilador para optimizar las cosas y usar el tipo de datos que sea más útil para usted. Si necesita alta precisión, use los tipos de datos de punto flotante más grandes. Si trabaja solo con valores positivos, probablemente pueda usar un entero sin signo, pero en su mayor parte, simplemente usar el tipo de datos int es suficiente.

Sin embargo, si tiene algunos requisitos de datos muy estrictos, como escribir un protocolo de comunicaciones o algún tipo de algoritmo de cifrado, el uso de tipos de datos con control de rango puede ser muy útil, especialmente si está tratando de evitar problemas relacionados con desbordamientos / subestimaciones de datos o valores de datos no válidos.

La única otra razón por la que puedo pensar fuera de mi cabeza para usar tipos de datos específicos es cuando estás tratando de comunicar la intención dentro de tu código. Si usa una abreviatura, por ejemplo, le está diciendo a otros desarrolladores que está permitiendo números positivos y negativos dentro de un rango de valores muy pequeño.

S.Robins
fuente
6

Como comentó scarfridge , esta es una

Caso clásico de optimización prematura .

Intentar optimizar el uso de la memoria podría afectar otras áreas de rendimiento, y las reglas de oro de la optimización son:

La primera regla de optimización del programa: no lo hagas .

La segunda regla de optimización de programas (¡solo para expertos!): No lo hagas todavía ".

- Michael A. Jackson

Para saber si ahora es el momento de optimizar, se requiere evaluación comparativa y pruebas. Necesita saber dónde su código está siendo ineficiente, para poder orientar sus optimizaciones.

Para determinar si la versión optimizada del código es realmente mejor que la implementación ingenua en un momento dado, debe compararlos lado a lado con los mismos datos.

Además, recuerde que el hecho de que una implementación dada sea más eficiente en la generación actual de CPU, no significa que siempre sea ​​así. Mi respuesta a la pregunta ¿Es importante la microoptimización al codificar? detalla un ejemplo de la experiencia personal donde una optimización obsoleta resultó en una desaceleración del orden de magnitud.

En muchos procesadores, los accesos de memoria no alineados son significativamente más costosos que los accesos de memoria alineados. Empacar un par de cortos en su estructura puede significar que su programa tiene que realizar la operación de empaque / desempaque cada vez que toque cualquiera de los valores.

Por esta razón, los compiladores modernos ignoran sus sugerencias. Como Nikie comenta:

Con la configuración estándar del compilador de empaquetamiento / alineación, las variables se alinearán a límites de 4 bytes de todos modos, por lo que puede que no haya ninguna diferencia.

Segundo adivina tu compilador bajo tu propio riesgo.

Hay un lugar para tales optimizaciones, cuando se trabaja con conjuntos de datos de terabytes o microcontroladores integrados, pero para la mayoría de nosotros, no es realmente una preocupación.

Mark Booth
fuente
3

La principal diferencia es que short int toma 2 bytes de memoria, mientras que int toma 4 bytes, y short int tiene un valor menor, pero también podríamos llamar a esto para hacerlo aún más pequeño:

Esto es incorrecto. No puede hacer suposiciones acerca de cuántos bytes contiene cada tipo, aparte de charser un byte y al menos 8 bits por byte, junto con que el tamaño de cada tipo sea mayor o igual que el anterior.

Los beneficios de rendimiento son increíblemente minúsculos para las variables de la pila: es probable que estén alineados / rellenados de todos modos.

Debido a esto, shorty longprácticamente no tengo uso hoy en día, y casi siempre es mejor usarlo int.


Por supuesto, también hay uno stdint.hque está perfectamente bien cuando intno se corta. Si alguna vez está asignando grandes matrices de enteros / estructuras, entonces intX_ttiene sentido ya que puede ser eficiente y confiar en el tamaño del tipo. Esto no es prematuro, ya que puede ahorrar megabytes de memoria.

Pubby
fuente
1
En realidad, con la llegada de los entornos de 64 bits, longpuede ser diferente a int. Si su compilador es LP64, inttiene 32 bits y long64 bits y encontrará que ints todavía puede estar alineado a 4 bytes (mi compilador lo hace, por ejemplo).
JeremyP
1
@ JeremyP Sí, ¿dije lo contrario o algo así?
Pubby
Su última oración que dice corto y largo prácticamente no tiene uso. Long ciertamente tiene un uso, aunque solo sea como el tipo base deint64_t
JeremyP
@ JeremyP: Puedes vivir bien con int y por mucho tiempo.
gnasher729
@ gnasher729: ¿Qué utiliza si necesita una variable que pueda contener valores de más de 65 mil, pero nunca tanto como mil millones? int32_t, int_fast32_ty longson todas buenas opciones, long longes un desperdicio y intno es portátil.
Ben Voigt
3

Esto será desde un punto de vista de OOP y / o empresarial / aplicación y podría no ser aplicable en ciertos campos / dominios, pero quiero plantear el concepto de obsesión primitiva .

Es una buena idea usar diferentes tipos de datos para diferentes tipos de información en su aplicación. Sin embargo, probablemente NO sea una buena idea usar los tipos integrados para esto, a menos que tenga algunos problemas de rendimiento graves (que se han medido y verificado, etc.).

Si queremos modelar temperaturas en Kelvin en nuestra aplicación, PODRÍAMOS usar una ushorto uintalgo similar para denotar que "la noción de grados negativos Kelvin es absurda y un error de lógica de dominio". La idea detrás de esto es sólida, pero no vas hasta el final. Lo que nos dimos cuenta es que no podemos tener valores negativos, por lo que es útil si podemos obtener el compilador para asegurarnos de que nadie asigne un valor negativo a una temperatura Kelvin. También es cierto que no puede realizar operaciones bit a bit en temperaturas. Y no puede agregar una medida de peso (kg) a una temperatura (K). Pero si modela la temperatura y la masa como uints, podemos hacer exactamente eso.

El uso de tipos incorporados para modelar nuestras entidades DOMINIO está destinado a generar un código desordenado y algunas comprobaciones perdidas e invariantes rotos. Incluso si un tipo captura ALGUNA parte de la entidad (no puede ser negativa), seguramente perderá otras (no se puede usar en expresiones aritméticas arbitrarias, no se puede tratar como una matriz de bits, etc.)

La solución es definir nuevos tipos que encapsulan los invariantes. De esta manera, puede asegurarse de que el dinero es dinero y las distancias son distancias, y no puede sumarlos, y no puede crear una distancia negativa, pero PUEDE crear una cantidad negativa de dinero (o una deuda). Por supuesto, estos tipos utilizarán los tipos integrados internamente, pero esto está oculto para los clientes. En relación con su pregunta sobre el rendimiento / consumo de memoria, este tipo de cosas puede permitirle cambiar la forma en que las cosas se almacenan internamente sin cambiar la interfaz de sus funciones que operan en las entidades de su dominio, en caso de que se dé cuenta, a shortes demasiado maldito grande.

sara
fuente
1

Sí, por supuesto. Es una buena idea usarlo uint_least8_tpara diccionarios, matrices de constantes enormes, buffers, etc. Es mejor usarlo uint_fast8_tpara fines de procesamiento.

uint8_least_t(almacenamiento) -> uint8_fast_t(procesamiento) -> uint8_least_t(almacenamiento).

Por ejemplo, está tomando un símbolo de 8 bits source, códigos de 16 bits dictionariesy unos 32 bits constants. Entonces estás procesando operaciones de 10-15 bits con ellos y salidas de 8 bits destination.

Imaginemos que tiene que procesar 2 gigabytes de source. La cantidad de operaciones de bits es enorme. Recibirá una gran bonificación de rendimiento si cambia a tipos rápidos durante el procesamiento. Los tipos rápidos pueden ser diferentes para cada familia de CPU. Puede incluir stdint.hy el uso uint_fast8_t, uint_fast16_t, uint_fast32_t, etc.

Podrías usar en uint_least8_tlugar de uint8_tportabilidad. Pero nadie sabe realmente qué CPU moderna utilizará esta función. La máquina VAC es una pieza de museo. Entonces quizás sea una exageración.

puchu
fuente
1
Si bien puede tener un punto con los tipos de datos que enumeró, debe explicar por qué son mejores en lugar de simplemente indicar que son. Para las personas como yo que no están familiarizadas con esos tipos de datos, tuve que buscarlos en Google para entender de qué están hablando.
Peter M