Se supone que el compilador produce ensamblador (y en última instancia, código de máquina) para alguna máquina, y generalmente C ++ intenta simpatizar con esa máquina.
Simpatizar con la máquina subyacente significa más o menos: facilitar la escritura de código C ++ que se asignará de manera eficiente a las operaciones que la máquina puede ejecutar rápidamente. Por lo tanto, queremos proporcionar acceso a los tipos de datos y operaciones que son rápidos y "naturales" en nuestra plataforma de hardware.
Concretamente, considere una arquitectura de máquina específica. Tomemos la actual familia Intel x86.
El Manual del desarrollador de software de arquitecturas Intel® 64 e IA-32 vol 1 ( enlace ), sección 3.4.1 dice:
Los registros de propósito general de 32 bits EAX, EBX, ECX, EDX, ESI, EDI, EBP y ESP se proporcionan para contener los siguientes elementos:
• Operandos para operaciones lógicas y aritméticas.
• Operandos para el cálculo de direcciones.
• Punteros de memoria
Entonces, queremos que el compilador use estos registros EAX, EBX, etc. cuando compila aritmética de enteros simples de C ++. Esto significa que cuando declaro un int, debería ser algo compatible con estos registros, para poder usarlos de manera eficiente.
Los registros son siempre del mismo tamaño (aquí, 32 bits), así que mi int variables siempre serán también de 32 bits. Usaré el mismo diseño (little-endian) para no tener que hacer una conversión cada vez que cargue un valor variable en un registro, o almacene un registro nuevamente en una variable.
Usando godbolt podemos ver exactamente qué hace el compilador para algún código trivial:
int square(int num) {
return num * num;
}
compila (con GCC 8.1 y -fomit-frame-pointer -O3por simplicidad) para:
square(int):
imul edi, edi
mov eax, edi
ret
esto significa:
- el
int numparámetro se pasó en el registro EDI, lo que significa que es exactamente el tamaño y el diseño que Intel espera para un registro nativo. La función no tiene que convertir nada
- la multiplicación es una sola instrucción (
imul), que es muy rápida
- devolver el resultado es simplemente una cuestión de copiarlo en otro registro (la persona que llama espera que el resultado se ponga en EAX)
Editar: podemos agregar una comparación relevante para mostrar la diferencia usando marcas de diseño no nativas. El caso más simple es almacenar valores en algo diferente al ancho nativo.
Usando Godbolt nuevamente, podemos comparar una simple multiplicación nativa
unsigned mult (unsigned x, unsigned y)
{
return x*y;
}
mult(unsigned int, unsigned int):
mov eax, edi
imul eax, esi
ret
con el código equivalente para un ancho no estándar
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 las instrucciones adicionales están relacionadas con la conversión del formato de entrada (dos enteros sin signo de 31 bits) al formato que el procesador puede manejar de forma nativa. Si quisiéramos almacenar el resultado nuevamente en un valor de 31 bits, habría otra o dos instrucciones para hacerlo.
Esta complejidad adicional significa que solo se molestaría con esto cuando el ahorro de espacio es muy importante. En este caso, solo estamos guardando dos bits en comparación con el uso del nativo unsignedo uint32_ttipo, lo que habría generado un código mucho más simple.
Una nota sobre tamaños dinámicos:
El ejemplo anterior sigue siendo valores de ancho fijo en lugar de ancho variable, pero el ancho (y la alineación) ya no coinciden con los registros nativos.
La plataforma x86 tiene varios tamaños nativos, incluidos 8 bits y 16 bits además del principal de 32 bits (estoy pasando por alto el modo de 64 bits y varias otras cosas por simplicidad).
Estos tipos (char, int8_t, uint8_t, int16_t, etc.) también son directamente compatibles con la arquitectura, en parte por compatibilidad con versiones anteriores de 8086/286/386 / etc. etc. conjuntos de instrucciones.
Sin duda, es el caso que elegir el tamaño fijo natural más pequeño que sea suficiente, puede ser una buena práctica: todavía son rápidas, las instrucciones se cargan y almacenan, aún obtienes aritmética nativa a toda velocidad, e incluso puedes mejorar el rendimiento al Reducción de errores de caché.
Esto es muy diferente a la codificación de longitud variable: he trabajado con algunos de estos y son horribles. Cada carga se convierte en un bucle en lugar de una sola instrucción. Cada tienda es también un bucle. Cada estructura es de longitud variable, por lo que no puede usar matrices de forma natural.
Una nota adicional sobre eficiencia
En comentarios posteriores, ha estado usando la palabra "eficiente", por lo que puedo decir con respecto al tamaño de almacenamiento. A veces elegimos minimizar el tamaño de almacenamiento; puede ser importante cuando guardamos una gran cantidad de valores en archivos o los enviamos a través de una red. La compensación es que necesitamos cargar esos valores en los registros para hacer algo con ellos, y realizar la conversión no es gratis.
Cuando hablamos de eficiencia, necesitamos saber qué estamos optimizando y cuáles son las compensaciones. El uso de tipos de almacenamiento no nativos es una forma de cambiar la velocidad de procesamiento por espacio, y a veces tiene sentido. El uso de almacenamiento de longitud variable (al menos para tipos aritméticos), intercambia más velocidad de procesamiento (y complejidad de código y tiempo de desarrollador) para un ahorro de espacio a menudo mínimo.
La penalización de velocidad que paga por esto significa que solo vale la pena cuando necesita minimizar absolutamente el ancho de banda o el almacenamiento a largo plazo, y para esos casos generalmente es más fácil usar un formato simple y natural, y luego simplemente comprimirlo con un sistema de uso general (como zip, gzip, bzip2, xy o lo que sea).
tl; dr
Cada plataforma tiene una arquitectura, pero puede crear un número esencialmente ilimitado de formas diferentes de representar datos. No es razonable que ningún idioma proporcione un número ilimitado de tipos de datos integrados. Entonces, C ++ proporciona acceso implícito al conjunto de tipos de datos nativos y naturales de la plataforma, y le permite codificar cualquier otra representación (no nativa) usted mismo.
unsingedvalor más grande que puede representarse con 1 byte es255. 2) Considere la sobrecarga de calcular el tamaño de almacenamiento óptimo y reducir / expandir el área de almacenamiento de una variable a medida que cambia el valor.