Las implementaciones pueden diferir entre los tamaños reales de los tipos, pero en la mayoría, los tipos como unsigned int y float son siempre de 4 bytes. Pero, ¿por qué un tipo siempre ocupa una cierta cantidad de memoria sin importar su valor? Por ejemplo, si creé el siguiente entero con el valor 255
int myInt = 255;
Luego myInt
ocuparía 4 bytes con mi compilador. Sin embargo, el valor real 255
puede representarse con solo 1 byte, entonces ¿por qué myInt
no ocuparía solo 1 byte de memoria? O la forma más generalizada de preguntar: ¿por qué un tipo tiene un solo tamaño asociado cuando el espacio requerido para representar el valor puede ser menor que ese tamaño?
unsinged
valor 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.unsigned int
valor.std::vector<X>
siempre tiene el mismo tamaño,sizeof(std::vector<X>)
es decir, es una constante de tiempo de compilación.Respuestas:
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:
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:
compila (con GCC 8.1 y
-fomit-frame-pointer -O3
por simplicidad) para:esto significa:
int num
pará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 nadaimul
), que es muy rápidaEditar: 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
con el código equivalente para un ancho no estándar
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
unsigned
ouint32_t
tipo, 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.
fuente
git
los metadatos) o realmente los están guardando en la memoria, ocasionalmente necesitan acceder aleatoriamente o modificar algunos, pero no la mayoría los valores (como en los motores de renderizado HTML + CSS) y, por lo tanto, solo se pueden evitar utilizando algo como VLQ in situ.Porque los tipos representan fundamentalmente el almacenamiento, y se definen en términos del valor máximo que pueden contener, no el valor actual.
La analogía muy simple sería una casa: una casa tiene un tamaño fijo, independientemente de cuántas personas vivan en ella, y también hay un código de construcción que estipula el número máximo de personas que pueden vivir en una casa de cierto tamaño.
Sin embargo, incluso si una sola persona está viviendo en una casa que puede acomodar a 10, el tamaño de la casa no se verá afectado por el número actual de ocupantes.
fuente
Es una optimización y simplificación.
Puede tener objetos de tamaño fijo. Por lo tanto, almacenar el valor.
O puede tener objetos de tamaño variable. Pero almacenando valor y tamaño.
objetos de tamaño fijo
El código que manipula el número no necesita preocuparse por el tamaño. Asume que siempre usa 4 bytes y hace que el código sea muy simple.
Objetos de tamaño dinámico
El código que el número manipulado debe comprender al leer una variable que debe leer el valor y el tamaño. Use el tamaño para asegurarse de que todos los bits altos estén en cero en el registro.
Cuando coloque el valor nuevamente en la memoria si el valor no ha excedido su tamaño actual, simplemente coloque el valor nuevamente en la memoria. Pero si el valor se ha reducido o aumentado, debe mover la ubicación de almacenamiento del objeto a otra ubicación en la memoria para asegurarse de que no se desborde. Ahora tiene que rastrear la posición de ese número (ya que puede moverse si crece demasiado para su tamaño). También debe realizar un seguimiento de todas las ubicaciones de variables no utilizadas para que puedan reutilizarse.
Resumen
El código generado para objetos de tamaño fijo es mucho más simple.
Nota
La compresión utiliza el hecho de que 255 encajará en un byte. Existen esquemas de compresión para almacenar grandes conjuntos de datos que utilizarán activamente diferentes valores de tamaño para diferentes números. Pero como no se trata de datos en vivo, no tiene las complejidades descritas anteriormente. Utiliza menos espacio para almacenar los datos a un costo de comprimir / descomprimir los datos para su almacenamiento.
fuente
int
almacenarán la cantidad de elementos en esa matriz. Esoint
tendrá un tamaño fijo nuevamente.Porque en un lenguaje como C ++, un objetivo de diseño es que las operaciones simples se compilen en instrucciones simples de la máquina.
Todos los conjuntos de instrucciones de CPU convencionales funcionan con tipos de ancho fijo , y si desea hacer tipos de ancho variable , debe hacer varias instrucciones de máquina para manejarlos.
En cuanto a por qué el hardware de la computadora subyacente es así: es porque es más simple y más eficiente para muchos casos (pero no para todos).
Imagine la computadora como un trozo de cinta:
Si simplemente le dice a la computadora que mire el primer byte de la cinta,
xx
¿cómo sabe si el tipo se detiene allí o si pasa al siguiente byte? Si tiene un número como255
(hexadecimalFF
) o un número como65535
(hexadecimalFFFF
), el primer byte es siempreFF
.Entonces, ¿cómo lo sabes? Debe agregar lógica adicional y "sobrecargar" el significado de al menos un bit o valor de byte para indicar que el valor continúa al siguiente byte. Esa lógica nunca es "gratuita", ya sea que la emule en el software o agregue un montón de transistores adicionales a la CPU para hacerlo.
Los tipos de lenguajes de ancho fijo como C y C ++ reflejan eso.
No tiene por qué ser así, y los lenguajes más abstractos que están menos preocupados por la asignación a un código de máxima eficiencia son libres de usar codificaciones de ancho variable (también conocidas como "Cantidades de longitud variable" o VLQ) para los tipos numéricos.
Lectura adicional: si busca "cantidad de longitud variable", puede encontrar algunos ejemplos de dónde ese tipo de codificación es realmente eficiente y vale la pena la lógica adicional. Por lo general, es cuando necesita almacenar una gran cantidad de valores que pueden estar en cualquier lugar dentro de un amplio rango, pero la mayoría de los valores tienden a un pequeño subrango.
Tenga en cuenta que si un compilador puede demostrar que puede escapar almacenando el valor en una cantidad menor de espacio sin romper ningún código (por ejemplo, es una variable solo visible internamente dentro de una sola unidad de traducción), y su heurística de optimización sugiere que ' Será más eficiente en el hardware de destino, está completamente permitido optimizarlo en consecuencia y almacenarlo en una cantidad menor de espacio, siempre que el resto del código funcione "como si" hiciera lo normal.
Pero , cuando el código tiene que interactuar con otro código que podría compilarse por separado, los tamaños deben mantenerse consistentes o garantizar que cada parte del código siga la misma convención.
Porque si no es consistente, existe esta complicación: ¿
int x = 255;
qué sucede si tengo pero luego en el código que hagox = y
? Siint
pudiera ser de ancho variable, el compilador tendría que saber de antemano para preasignar la cantidad máxima de espacio que necesitará. Eso no siempre es posible, porque ¿quéy
pasa si se pasa un argumento de otro código que se compila por separado?fuente
Java usa clases llamadas "BigInteger" y "BigDecimal" para hacer exactamente esto, al igual que la interfaz de clase GMP C ++ de C ++ aparentemente (gracias Digital Trauma). Puede hacerlo usted mismo en prácticamente cualquier idioma si lo desea.
Las CPU siempre han tenido la capacidad de usar BCD (decimal codificado en binario) que está diseñado para admitir operaciones de cualquier longitud (pero tiende a operar manualmente en un byte a la vez, lo que sería LENTO según los estándares actuales de GPU).
¿La razón por la que no usamos estas u otras soluciones similares? Actuación. Sus lenguajes de mayor rendimiento no pueden permitirse el lujo de expandir una variable en medio de una operación de ciclo cerrado; sería muy no determinista.
En situaciones de almacenamiento y transporte masivo, los valores empaquetados son a menudo el ÚNICO tipo de valor que usaría. Por ejemplo, un paquete de música / video que se transmite a su computadora puede gastar un poco para especificar si el siguiente valor es 2 bytes o 4 bytes como una optimización de tamaño.
Sin embargo, una vez que está en su computadora donde se puede usar, la memoria es barata, pero la velocidad y la complicación de las variables redimensionables no es ... esa es realmente la única razón.
fuente
Porque sería muy complicado y el cálculo pesado tener tipos simples con tamaños dinámicos. No estoy seguro de que esto sea posible.
La computadora tendría que verificar cuántos bits toma el número después de cada cambio de su valor. Serían muchas operaciones adicionales. Y sería mucho más difícil realizar cálculos cuando no se conocen los tamaños de las variables durante la compilación.
Para admitir tamaños dinámicos de variables, la computadora tendría que recordar cuántos bytes tiene una variable en este momento, lo que ... requeriría memoria adicional para almacenar esa información. Y esta información tendría que analizarse antes de cada operación en la variable para elegir la instrucción de procesador correcta.
Para comprender mejor cómo funciona la computadora y por qué las variables tienen tamaños constantes, aprenda los conceptos básicos del lenguaje ensamblador.
Aunque, supongo que sería posible lograr algo así con los valores constexpr. Sin embargo, esto haría que el código fuera menos predecible para un programador. Supongo que algunas optimizaciones del compilador pueden hacer algo así, pero se lo ocultan a un programador para mantener las cosas simples.
Describí aquí solo los problemas relacionados con el desempeño de un programa. Omití todos los problemas que tendrían que resolverse para ahorrar memoria al reducir el tamaño de las variables. Honestamente, no creo que sea posible.
En conclusión, el uso de variables más pequeñas que las declaradas solo tiene sentido si se conocen sus valores durante la compilación. Es bastante probable que los compiladores modernos hagan eso. En otros casos, causaría demasiados problemas difíciles o incluso irresolubles.
fuente
56
y la multiplicamos por alguna variable de 2 bytes. En algunas arquitecturas, la operación de 64 bits sería más pesada para el compilador, por lo que el compilador podría optimizar eso para realizar una multiplicación de solo 16 bits.Esto se conoce como codificación de longitud variable , hay varias codificaciones definidas, por ejemplo, VLQ . Sin embargo, uno de los más famosos es probablemente UTF-8 : UTF-8 codifica puntos de código en un número variable de bytes, de 1 a 4.
Como siempre en ingeniería, se trata de compensaciones. No existe una solución que solo tenga ventajas, por lo que debe equilibrar las ventajas y las compensaciones al diseñar su solución.
El diseño que se decidió fue utilizar tipos fundamentales de tamaño fijo, y el hardware / idiomas simplemente voló desde allí.
Entonces, ¿cuál es el debilidad fundamental de la codificación variable , que provocó su rechazo en favor de más esquemas de memoria? Sin direccionamiento aleatorio .
¿Cuál es el índice del byte en el que el cuarto punto de código comienza en una cadena UTF-8?
Depende de los valores de los puntos de código anteriores, se requiere un escaneo lineal.
¿Seguramente hay esquemas de codificación de longitud variable que son mejores en direccionamiento aleatorio?
Sí, pero también son más complicados. Si hay uno ideal, nunca lo he visto todavía.
¿El direccionamiento aleatorio realmente importa de todos modos?
¡Oh si!
La cuestión es que cualquier tipo de agregado / matriz se basa en tipos de tamaño fijo:
struct
? Direccionamiento aleatorio!Lo que significa que esencialmente tiene la siguiente compensación:
Tipos de tamaño fijo O escaneos de memoria lineal
fuente
La memoria de la computadora se subdivide en fragmentos direccionados consecutivamente de cierto tamaño (a menudo 8 bits, y denominados bytes), y la mayoría de las computadoras están diseñadas para acceder eficientemente a secuencias de bytes que tienen direcciones consecutivas.
Si la dirección de un objeto nunca cambia dentro de la vida útil del objeto, entonces el código dado su dirección puede acceder rápidamente al objeto en cuestión. Sin embargo, una limitación esencial con este enfoque es que si se asigna una dirección para la dirección X, y luego se asigna otra dirección para la dirección Y, que está a N bytes de distancia, entonces X no podrá crecer más de N bytes en la vida útil de Y, a menos que X o Y se muevan. Para que X se mueva, sería necesario que todo en el universo que contiene la dirección de X se actualice para reflejar la nueva, y también para que Y se mueva. Si bien es posible diseñar un sistema para facilitar tales actualizaciones (tanto Java como .NET lo manejan bastante bien) es mucho más eficiente trabajar con objetos que permanecerán en la misma ubicación durante toda su vida útil,
fuente
La respuesta corta es: Porque el estándar C ++ lo dice.
La respuesta larga es: lo que puede hacer en una computadora está en última instancia limitado por el hardware. Por supuesto, es posible codificar un número entero en un número variable de bytes para el almacenamiento, pero luego leerlo requeriría instrucciones especiales de la CPU para que funcionen, o podría implementarlo en el software, pero sería muy lento. Las operaciones de tamaño fijo están disponibles en la CPU para cargar valores de anchos predefinidos, no hay ninguno para anchos variables.
Otro punto a considerar es cómo funciona la memoria de la computadora. Digamos que su tipo entero podría ocupar entre 1 y 4 bytes de almacenamiento. Suponga que almacena el valor 42 en su número entero: ocupa 1 byte y lo coloca en la dirección de memoria X. Luego almacena su próxima variable en la ubicación X + 1 (no estoy considerando la alineación en este punto) y así sucesivamente . Más tarde, decide cambiar su valor a 6424.
¡Pero esto no cabe en un solo byte! Entonces, ¿Qué haces? ¿Dónde pones el resto? Ya tienes algo en X + 1, así que no puedes colocarlo allí. ¿En algún otro lugar? ¿Cómo sabrás más tarde dónde? La memoria de la computadora no admite la semántica de inserción: ¡no puede simplemente colocar algo en un lugar y dejar todo a un lado para dejar espacio!
Aparte: de lo que estás hablando es realmente del área de compresión de datos. Existen algoritmos de compresión para empacar todo más, por lo que al menos algunos de ellos considerarán no usar más espacio para su entero del que necesita. Sin embargo, los datos comprimidos no son fáciles de modificar (si es posible) y simplemente se vuelven a comprimir cada vez que se realizan cambios.
fuente
Al hacer esto, se obtienen beneficios sustanciales de rendimiento en tiempo de ejecución. Si tuviera que operar con tipos de tamaño variable, tendría que decodificar cada número antes de realizar la operación (las instrucciones del código de la máquina suelen ser de ancho fijo), realizar la operación y luego encontrar un espacio en la memoria lo suficientemente grande como para contener el resultado. Esas son operaciones muy difíciles. Es mucho más fácil simplemente almacenar todos los datos de manera poco eficiente.
Esto no siempre es así. Considere el protocolo Protobuf de Google. Los Protobufs están diseñados para transmitir datos de manera muy eficiente. Disminuir el número de bytes transmitidos vale el costo de instrucciones adicionales cuando se opera con los datos. En consecuencia, los protobufs usan una codificación que codifica enteros en 1, 2, 3, 4 o 5 bytes, y los enteros más pequeños toman menos bytes. Sin embargo, una vez que se recibe el mensaje, se desempaqueta en un formato entero de tamaño fijo más tradicional que es más fácil de operar. Es solo durante la transmisión de red que usan un número entero de longitud variable tan eficiente en el espacio.
fuente
Me gusta la analogía de la casa de Sergey , pero creo que la analogía de un automóvil sería mejor.
Imagine tipos variables como tipos de automóviles y personas como datos. Cuando buscamos un auto nuevo, elegimos el que mejor se adapte a nuestro propósito. ¿Queremos un automóvil inteligente pequeño que solo pueda caber en una o dos personas? ¿O una limusina para transportar más personas? Ambos tienen sus ventajas y desventajas como la velocidad y el consumo de combustible (piense en la velocidad y el uso de la memoria).
Si tiene una limusina y maneja solo, no se reducirá para adaptarse solo a usted. Para hacer eso, tendría que vender el automóvil (léase: desasignar) y comprar uno nuevo más pequeño para usted.
Continuando con la analogía, puede pensar en la memoria como un enorme estacionamiento lleno de automóviles, y cuando va a leer, un chofer especializado entrenado exclusivamente para su tipo de automóvil va a buscarlo. Si su automóvil pudiera cambiar de tipo dependiendo de las personas que lo componen, necesitaría traer una gran cantidad de choferes cada vez que quisiera obtener su automóvil, ya que nunca sabrían qué tipo de automóvil se sentará en el lugar.
En otras palabras, tratar de determinar la cantidad de memoria que necesita leer en el tiempo de ejecución sería enormemente ineficiente y superaría el hecho de que tal vez podría acomodar algunos autos más en su estacionamiento.
fuente
Hay unas pocas razones. Una es la complejidad añadida para manejar números de tamaño arbitrario y el impacto en el rendimiento que esto da porque el compilador ya no puede optimizar en base al supuesto de que cada int tiene exactamente X bytes de longitud.
Una segunda es que almacenar tipos simples de esta manera significa que necesitan un byte adicional para mantener la longitud. Por lo tanto, un valor de 255 o menos realmente necesita dos bytes en este nuevo sistema, no uno, y en el peor de los casos ahora necesita 5 bytes en lugar de 4. Esto significa que el rendimiento ganado en términos de memoria utilizada es menor de lo que podría pensar y, en algunos casos extremos, en realidad podría ser una pérdida neta.
Una tercera razón es que la memoria de la computadora generalmente es direccionable en palabras , no en bytes. (Pero ver nota al pie). Las palabras son múltiplos de bytes, generalmente 4 en sistemas de 32 bits y 8 en sistemas de 64 bits. Por lo general, no puede leer un byte individual, lee una palabra y extrae el enésimo byte de esa palabra. Esto significa que extraer bytes individuales de una palabra requiere un poco más de esfuerzo que simplemente leer la palabra completa y que es muy eficiente si toda la memoria se divide uniformemente en fragmentos del tamaño de una palabra (es decir, de 4 bytes). Porque, si tiene números enteros de tamaño arbitrario flotando, podría terminar con una parte del número entero en una palabra y otra en la siguiente palabra, que necesitará dos lecturas para obtener el número entero.
Nota al pie de página: para ser más precisos, mientras que usted abordó en bytes, la mayoría de los sistemas ignoraron los bytes 'desiguales'. Es decir, la dirección 0, 1, 2 y 3 leen la misma palabra, 4, 5, 6 y 7 leen la siguiente palabra, y así sucesivamente.
En una nota no publicada, esta es también la razón por la cual los sistemas de 32 bits tenían un máximo de 4 GB de memoria. Los registros utilizados para direccionar ubicaciones en la memoria suelen ser lo suficientemente grandes como para contener una palabra, es decir, 4 bytes, que tiene un valor máximo de (2 ^ 32) -1 = 4294967295. 4294967296 bytes son 4 GB.
fuente
Hay objetos que, en cierto sentido, tienen un tamaño variable, en la biblioteca estándar de C ++, como
std::vector
. Sin embargo, todos estos asignan dinámicamente la memoria adicional que necesitarán. Si lo hacesizeof(std::vector<int>)
, obtendrá una constante que no tiene nada que ver con la memoria administrada por el objeto, y si asigna una matriz o estructura que contengastd::vector<int>
, se reservará este tamaño base en lugar de colocar el almacenamiento adicional en la misma matriz o estructura . Hay algunas piezas de sintaxis de C que admiten algo como esto, especialmente matrices y estructuras de longitud variable, pero C ++ no eligió admitirlas.El estándar de lenguaje define el tamaño del objeto de esa manera para que los compiladores puedan generar código eficiente. Por ejemplo, si
int
tiene 4 bytes de longitud en alguna implementación, y declaraa
como puntero o conjunto deint
valores, entonces sea[i]
traduce en el pseudocódigo, "desreferenciar la dirección a + 4 × i". Esto se puede hacer en tiempo constante, y es una operación tan común e importante que muchas arquitecturas de conjuntos de instrucciones, incluidas x86 y las máquinas DEC PDP en las que se desarrolló originalmente C, pueden hacerlo en una sola instrucción de máquina.Un ejemplo común del mundo real de datos almacenados consecutivamente como unidades de longitud variable son las cadenas codificadas como UTF-8. (Sin embargo, el tipo subyacente de una cadena UTF-8 para el compilador es inmóvil
char
y tiene ancho 1. Esto permite que las cadenas ASCII se interpreten como UTF-8 válidas, y una gran cantidad de código de biblioteca comostrlen()
ystrncpy()
que continúen funcionando). La codificación de cualquier punto de código UTF-8 puede tener una longitud de uno a cuatro bytes y, por lo tanto, si desea el quinto punto de código UTF-8 en una cadena, podría comenzar desde el quinto byte hasta el decimoséptimo byte de los datos. La única forma de encontrarlo es escanear desde el principio de la cadena y verificar el tamaño de cada punto de código. Si quieres encontrar el quinto grafema, también debe verificar las clases de caracteres. Si quisieras encontrar el millonésimo personaje UTF-8 en una cadena, ¡necesitarías ejecutar este bucle un millón de veces! Si sabe que necesitará trabajar con índices con frecuencia, puede atravesar la cadena una vez y crear un índice, o puede convertirla a una codificación de ancho fijo, como UCS-4. Encontrar el millonésimo carácter UCS-4 en una cadena es solo cuestión de agregar cuatro millones a la dirección de la matriz.Otra complicación con los datos de longitud variable es que, cuando los asigna, necesita asignar la mayor cantidad de memoria posible, o bien reasignar dinámicamente según sea necesario. Asignar para el peor de los casos podría ser extremadamente derrochador. Si necesita un bloque de memoria consecutivo, la reasignación podría obligarlo a copiar todos los datos en una ubicación diferente, pero permitir que la memoria se almacene en fragmentos no consecutivos complica la lógica del programa.
Por lo tanto, es posible tener bignums de longitud variable en lugar de ancho fijo
short int
,int
,long int
ylong long int
, pero sería ineficaz para asignar y utilizar ellos. Además, todas las CPU convencionales están diseñadas para realizar operaciones aritméticas en registros de ancho fijo, y ninguna tiene instrucciones que operen directamente en algún tipo de bignum de longitud variable. Esos tendrían que implementarse en software, mucho más lentamente.En el mundo real, la mayoría (pero no todos) los programadores han decidido que los beneficios de la codificación UTF-8, especialmente la compatibilidad, son importantes, y que rara vez nos preocupamos por otra cosa que no sea escanear una cadena de adelante hacia atrás o copiar bloques de memoria de que los inconvenientes de ancho variable son aceptables. Podríamos usar elementos empaquetados de ancho variable similares a UTF-8 para otras cosas. Pero rara vez lo hacemos, y no están en la biblioteca estándar.
fuente
Principalmente debido a los requisitos de alineación.
Según basic.align / 1 :
Piense en un edificio que tiene muchos pisos y cada piso tiene muchas habitaciones.
Cada habitación es de su tamaño (un espacio fijo) capaz de contener N cantidad de personas u objetos.
Con el tamaño de la sala conocido de antemano, hace que el componente estructural del edificio esté bien estructurado .
Si las habitaciones no están alineadas, entonces el esqueleto del edificio no estará bien estructurado.
fuente
Puede ser menos. Considere la función:
se compila en código de ensamblaje (g ++, x64, detalles eliminados)
Aquí,
bar
ybaz
termina usando cero bytes para representar.fuente
Porque le dijiste que usara tanto. Cuando se usa un
unsigned int
, algunos estándares dictan que se usarán 4 bytes y que el rango disponible será de 0 a 4,294,967,295. Si usara ununsigned char
lugar, probablemente solo estaría usando el 1 byte que está buscando (según el estándar y C ++ normalmente usa estos estándares).Si no fuera por estos estándares, tendría que tener esto en cuenta: ¿cómo se supone que el compilador o la CPU deben usar solo 1 byte en lugar de 4? Más adelante en su programa, puede agregar o multiplicar ese valor, lo que requeriría más espacio. Cada vez que realiza una asignación de memoria, el sistema operativo tiene que buscar, asignar y darle ese espacio (potencialmente intercambiando memoria a RAM virtual también); Esto puede llevar mucho tiempo. Si asigna la memoria de antemano, no tendrá que esperar a que se complete otra asignación.
En cuanto a la razón por la que usamos 8 bits por byte, puede ver esto: ¿Cuál es el historial de por qué los bytes son de ocho bits?
En una nota al margen, podría permitir que el número entero se desborde; pero si usa un entero con signo, los estándares C \ C ++ establecen que los desbordamientos de enteros resultan en un comportamiento indefinido. Desbordamiento de enteros
fuente
Algo simple que la mayoría de las respuestas parecen perder:
porque se adapta a los objetivos de diseño de C ++.
Ser capaz de calcular el tamaño de un tipo en el momento de la compilación permite que el compilador y el programador realicen una gran cantidad de suposiciones simplificadoras, lo que trae muchos beneficios, particularmente con respecto al rendimiento. Por supuesto, los tipos de tamaño fijo tienen dificultades concomitantes como el desbordamiento de enteros. Es por eso que diferentes lenguajes toman diferentes decisiones de diseño. (Por ejemplo, los enteros de Python son esencialmente de tamaño variable).
Probablemente la razón principal por la que C ++ se apoya tan fuertemente en los tipos de tamaño fijo es su objetivo de compatibilidad con C. Sin embargo, dado que C ++ es un lenguaje de tipo estático que intenta generar código muy eficiente y evita agregar cosas que el programador no especifica explícitamente, los tipos de tamaño fijo aún tienen mucho sentido.
Entonces, ¿por qué C optó por los tipos de tamaño fijo en primer lugar? Sencillo. Fue diseñado para escribir sistemas operativos, software de servidor y utilidades de los años 70; cosas que proporcionaron infraestructura (como gestión de memoria) para otro software. A un nivel tan bajo, el rendimiento es crítico, y también lo hace el compilador haciendo exactamente lo que le dices.
fuente
Cambiar el tamaño de una variable requeriría una reasignación y esto generalmente no vale los ciclos de CPU adicionales en comparación con el desperdicio de unos pocos bytes más de memoria.
Las variables locales van en una pila que es muy rápida de manipular cuando esas variables no cambian de tamaño. Si decidió que desea expandir el tamaño de una variable de 1 byte a 2 bytes, entonces tiene que mover todo en la pila por un byte para dejar ese espacio. Potencialmente, eso puede costar muchos ciclos de CPU dependiendo de cuántas cosas hay que mover.
Otra forma de hacerlo es convirtiendo cada variable en un puntero a una ubicación de almacenamiento dinámico, pero en realidad desperdiciaría aún más ciclos de CPU y memoria de esta manera. Los punteros son 4 bytes (direccionamiento de 32 bits) u 8 bytes (direccionamiento de 64 bits), por lo que ya está utilizando 4 u 8 para el puntero, luego el tamaño real de los datos en el montón. Todavía hay un costo para la reasignación en este caso. Si necesita reasignar datos de almacenamiento dinámico, puede tener suerte y tener espacio para expandirlos en línea, pero a veces tiene que moverlo a otro lugar del almacenamiento dinámico para tener el bloque de memoria contigua del tamaño que desea.
Siempre es más rápido decidir cuánta memoria usar de antemano. Si puede evitar el tamaño dinámico, obtendrá rendimiento. El desperdicio de memoria generalmente vale la ganancia de rendimiento. Es por eso que las computadoras tienen toneladas de memoria. :)
fuente
El compilador puede realizar muchos cambios en su código, siempre y cuando las cosas sigan funcionando (la regla "tal como está").
Sería posible utilizar una instrucción de movimiento literal de 8 bits en lugar de la más larga (32/64 bits) requerida para mover un completo
int
. Sin embargo, necesitaría dos instrucciones para completar la carga, ya que primero tendría que establecer el registro en cero antes de realizar la carga.Simplemente es más eficiente (al menos según los compiladores principales) manejar el valor como 32 bits. En realidad, aún no he visto un compilador x86 / x86_64 que haría una carga de 8 bits sin ensamblaje en línea.
Sin embargo, las cosas son diferentes cuando se trata de 64 bits. Al diseñar las extensiones anteriores (de 16 a 32 bits) de sus procesadores, Intel cometió un error. Aquí hay una buena representación de cómo se ven. La conclusión principal aquí es que cuando escribe a AL o AH, el otro no se ve afectado (bastante bien, ese era el punto y tenía sentido en ese entonces). Pero se pone interesante cuando lo expandieron a 32 bits. Si escribe los bits inferiores (AL, AH o AX), no pasa nada con los 16 bits superiores de EAX, lo que significa que si desea promover
char
unint
, primero necesita borrar esa memoria, pero no tiene forma de en realidad solo usa estos 16 bits principales, lo que hace que esta "característica" sea más dolorosa que otra cosaAhora con 64 bits, AMD hizo un trabajo mucho mejor. Si tocas algo en los 32 bits inferiores, los 32 bits superiores simplemente se establecen en 0. Esto lleva a algunas optimizaciones reales que puedes ver en este godbolt . Puede ver que cargar algo de 8 bits o 32 bits se hace de la misma manera, pero cuando usa variables de 64 bits, el compilador usa una instrucción diferente dependiendo del tamaño real de su literal.
Como puede ver aquí, los compiladores pueden cambiar totalmente el tamaño real de su variable dentro de la CPU si produciría el mismo resultado, pero no tiene sentido hacerlo para tipos más pequeños.
fuente