¿Por qué Java tiene primitivas para números de diferentes tamaños?

20

En Java hay tipos primitivos para byte, short, inty longy lo mismo para floaty double. ¿Por qué es necesario que una persona establezca cuántos bytes se deben usar para un valor primitivo? ¿No podría determinarse el tamaño dinámicamente dependiendo de qué tan grande fue el número pasado?

Hay dos razones por las que puedo pensar:

  1. Establecer dinámicamente el tamaño de los datos significaría que también debería ser capaz de cambiar dinámicamente. ¿Esto podría causar problemas de rendimiento?
  2. Quizás el programador no quiera que alguien pueda usar un número mayor que cierto tamaño y esto les permite limitarlo.

Todavía creo que podría haber mucho que ganar simplemente usando un solo inty floattipo, ¿hubo alguna razón específica por la que Java decidió no seguir esta ruta?

yitzih
fuente
44
Para los que votan negativamente, agregaría que esta pregunta está relacionada con una pregunta que los investigadores del compilador están buscando responder .
rwong
Entonces, si agregó a un número, ¿cree que el tipo debería cambiarse dinámicamente? ¿Incluso quiero cambiar el tipo? Si el número se inicializa como intUnknown alpha = a + b; ¿entiendes que sería un poco difícil para el compilador? ¿Por qué es esto específico de Java?
paparazzo
@Paparazzi Existen lenguajes de programación y entornos de ejecución (compiladores, intérpretes, etc.) existentes que almacenarán el entero de ancho dinámico en función de cuán grande es el valor real (por ejemplo, el resultado de la operación de suma). Las consecuencias son que: el código que se ejecutará en la CPU se vuelve más complicado; el tamaño de ese entero se vuelve dinámico; leer un entero de ancho dinámico de la memoria puede requerir más de un viaje; Las estructuras (objetos) y las matrices que contienen enteros de ancho dinámico dentro de sus campos / elementos también pueden tener un tamaño dinámico.
rwong
1
@tofro no entiendo. Simplemente envíe el número en el formato que desee: decimal, binario, etc. La serialización es una preocupación completamente ortogonal.
cabeza de jardín
1
@gardenhead Es ortogonal, sí, pero ... solo considere el caso en el que desea comunicarse entre un servidor escrito en Java y un cliente escrito en C. Por supuesto, esto se puede resolver con una infraestructura dedicada. Por ejemplo, hay cosas como developers.google.com/protocol-buffers . Pero este es un gran mazo para la pequeña nuez de transferir un número entero a través de la red. (Lo sé, este no es un argumento fuerte aquí, pero tal vez sea un punto a considerar: discutir los detalles está más allá del alcance de los comentarios).
Marco13

Respuestas:

16

Al igual que muchos aspectos del diseño del lenguaje, se trata de un equilibrio entre la elegancia y el rendimiento (sin mencionar alguna influencia histórica de los idiomas anteriores).

Alternativas

Ciertamente es posible (y bastante simple) crear un lenguaje de programación que tenga un solo tipo de números naturales nat. Casi todos los lenguajes de programación utilizados para el estudio académico (por ejemplo, PCF, Sistema F) tienen este tipo de número único, que es la solución más elegante, como suponía. Pero el diseño del lenguaje en la práctica no se trata solo de elegancia; También debemos considerar el rendimiento (la medida en que se considera el rendimiento depende de la aplicación prevista del lenguaje). El rendimiento comprende limitaciones de tiempo y espacio.

Limitaciones de espacio

Permitir que el programador elija el número de bytes por adelantado puede ahorrar espacio en programas con limitaciones de memoria. Si todos sus números van a ser menores que 256, entonces puede usar 8 veces más bytes que longs, o usar el almacenamiento guardado para objetos más complejos. El desarrollador estándar de aplicaciones Java no tiene que preocuparse por estas restricciones, pero sí aparecen.

Eficiencia

Incluso si ignoramos el espacio, todavía estamos limitados por la CPU, que solo tiene instrucciones que operan en un número fijo de bytes (8 bytes en una arquitectura de 64 bits). Eso significa que incluso proporcionar un solo tipo de 8 bytes longharía que la implementación del lenguaje sea significativamente más simple que tener un tipo de número natural ilimitado, al poder asignar operaciones aritméticas directamente a una sola instrucción subyacente de la CPU. Si permite que el programador use números arbitrariamente grandes, entonces una sola operación aritmética debe asignarse a una secuencia de instrucciones complejas de la máquina, lo que ralentizaría el programa. Este es el punto (1) que mencionaste.

Tipos de punto flotante

La discusión hasta ahora solo se ha referido a los enteros. Los tipos de punto flotante son una bestia compleja, con una semántica y casos extremos extremadamente sutiles. Por lo tanto, a pesar de que podría fácilmente reemplazar int, long, short, y bytecon un solo nattipo, no está claro cuál es el tipo de números de punto flotante, incluso es . No son números reales, obviamente, ya que los números reales no pueden existir en un lenguaje de programación. Tampoco son números bastante racionales (aunque es sencillo crear un tipo racional si se desea). Básicamente, IEEE decidió una forma de aproximar un poco los números reales, y todos los lenguajes (y programadores) se han quedado con ellos desde entonces.

Finalmente:

Quizás el programador no quiera que alguien pueda usar un número mayor que cierto tamaño y esto les permite limitarlo.

Esta no es una razón válida. En primer lugar, no puedo pensar en ninguna situación en la que los tipos puedan codificar naturalmente los límites numéricos, sin mencionar que las posibilidades son astronómicamente bajas de que los límites que el programador quiere imponer corresponderían exactamente a los tamaños de cualquiera de los tipos primitivos.

cabeza de jardín
fuente
2
La verdadera clave del hecho de que tenemos flotadores es que tenemos hardware dedicado para ellos
jk.
También la codificación de límites numéricos en un tipo sucede absolutamente en lenguajes de tipo dependiente y, en menor medida, en otros idiomas, por ejemplo, como enums
jk.
3
Las enumeraciones no son equivalentes a los enteros. Las enumeraciones son solo un modo de uso de los tipos de suma. El hecho de que algunos idiomas codifiquen enums de forma transparente como números enteros es una falla del lenguaje, no una característica explotable.
cabeza de jardín
1
No estoy familiarizado con Ada. ¿Podría restringir enteros a cualquier tipo, por ejemplo type my_type = int (7, 2343)?
cabeza de jardín
1
Sí. La sintaxis sería: type my_type es range 7..2343
Devsman el
9

La razón es muy simple: eficiencia . De múltiples maneras.

  1. Tipos de datos nativos: cuanto más se acerquen los tipos de datos de un idioma a los tipos de datos subyacentes del hardware, más eficiente se considera que es el idioma. (No en el sentido de que sus programas serán necesariamente eficientes, pero en el sentido de que, si realmente sabe lo que está haciendo, puede escribir un código que se ejecutará tan eficientemente como el hardware puede ejecutarlo). Los tipos de datos ofrecidos por Java corresponden a bytes, palabras, palabras dobles y palabras cuádruples del hardware más popular que existe. Ese es el camino más eficiente.

  2. Gastos indirectos injustificados en sistemas de 32 bits: si se hubiera tomado la decisión de asignar todo a un tamaño fijo de 64 bits, esto habría impuesto una gran penalización a las arquitecturas de 32 bits que necesitan considerablemente más ciclos de reloj para realizar un 64- operación de bits que una operación de 32 bits.

  3. Desperdicio de memoria: hay mucho hardware por ahí que no es demasiado exigente con la alineación de la memoria (las arquitecturas Intel x86 y x64 son ejemplos de eso), por lo que una matriz de 100 bytes en ese hardware puede ocupar solo 100 bytes de memoria. Sin embargo, si ya no tiene un byte y tiene que usar un largo, la misma matriz ocupará un orden de magnitud más memoria. Y los conjuntos de bytes son muy comunes.

  4. Cálculo de tamaños de números: su noción de determinar el tamaño de un número entero dinámicamente dependiendo de qué tan grande fue el número pasado es demasiado simplista; no hay un solo punto de "pasar" un número; el cálculo de qué tan grande debe ser un número debe realizarse en tiempo de ejecución, en cada operación que pueda requerir un resultado de un tamaño mayor: cada vez que incrementa un número, cada vez que agrega dos números, cada vez que multiplica dos números, etc.

  5. Operaciones en números de diferentes tamaños: posteriormente, tener números de tamaños potencialmente diferentes flotando en la memoria complicaría todas las operaciones: incluso para comparar simplemente dos números, el tiempo de ejecución primero tendría que verificar si ambos números a comparar son iguales tamaño, y si no, cambie el tamaño del más pequeño para que coincida con el tamaño del más grande.

  6. Operaciones que requieren tamaños de operando específicos: Ciertas operaciones de bits confían en que el entero tenga un tamaño específico. Al no tener un tamaño específico predeterminado, estas operaciones tendrían que ser emuladas.

  7. Sobrecarga del polimorfismo: cambiar el tamaño de un número en tiempo de ejecución esencialmente significa que tiene que ser polimórfico. Esto a su vez significa que no puede ser una primitiva de tamaño fijo asignada en la pila, tiene que ser un objeto, asignado en el montón. Eso es terriblemente ineficiente. (Vuelva a leer # 1 arriba).

Mike Nakis
fuente
6

Para evitar repetir los puntos que se han discutido en otras respuestas, intentaré esbozar múltiples perspectivas.

Desde la perspectiva del diseño del lenguaje

  • Es ciertamente posible diseñar e implementar un lenguaje de programación y su entorno de ejecución que acomode automáticamente los resultados de operaciones enteras que no se ajustan al ancho de la máquina.
  • Es la elección del diseñador de lenguaje si hacer que esos enteros de ancho dinámico sean el tipo de entero predeterminado para este idioma.
  • Sin embargo, el diseñador de lenguaje tiene que considerar los siguientes inconvenientes:
    • La CPU tendrá que ejecutar más código, lo que lleva más tiempo. Sin embargo, es posible optimizar para el caso más frecuente en el que el número entero cabe dentro de una sola palabra de máquina. Ver la representación del puntero etiquetado .
    • El tamaño de ese entero se vuelve dinámico.
    • Leer un entero de ancho dinámico de la memoria puede requerir más de un viaje.
    • Las estructuras (objetos) y las matrices que contienen enteros de ancho dinámico dentro de sus campos / elementos tendrán un tamaño total (ocupado) que también es dinámico.

Razones históricas

Esto ya se discute en el artículo de Wikipedia sobre la historia de Java, y también se discute brevemente en la respuesta de Marco13 .

Yo señalaría que:

  • Los diseñadores de idiomas deben hacer malabarismos entre una mentalidad estética y una pragmática. La mentalidad estética quiere diseñar un lenguaje que no sea propenso a problemas bien conocidos, como desbordamientos de enteros. La mentalidad pragmática le recuerda al diseñador que el lenguaje de programación debe ser lo suficientemente bueno como para implementar aplicaciones de software útiles e interactuar con otras partes de software que se implementan en diferentes idiomas.
  • Los lenguajes de programación que pretenden capturar la cuota de mercado de los lenguajes de programación más antiguos podrían ser más propensos a ser pragmáticos. Una posible consecuencia es que están más dispuestos a incorporar o tomar prestados construcciones y estilos de programación existentes de esos lenguajes más antiguos.

Razones de eficiencia

¿Cuándo importa la eficiencia?

  • Cuando tiene la intención de anunciar un lenguaje de programación como apto para el desarrollo de aplicaciones a gran escala.
  • Cuando necesita trabajar en millones y miles de millones de artículos pequeños, en los que se suma cada bit de eficiencia.
  • Cuando necesita competir con otro lenguaje de programación, su lenguaje debe tener un rendimiento decente: no necesita ser el mejor, pero ciertamente ayuda a mantenerse cerca del mejor rendimiento.

Eficiencia de almacenamiento (en memoria o en disco)

  • La memoria de la computadora fue una vez un recurso escaso. En esos viejos tiempos, el tamaño de los datos de la aplicación que podía ser procesado por una computadora estaba limitado por la cantidad de memoria de la computadora, aunque eso podría discutirse usando una programación inteligente (que costaría más implementarla).

Eficiencia de ejecución (dentro de la CPU, o entre la CPU y la memoria)

  • Ya discutido en la respuesta de gardenhead .
  • Si un programa necesita procesar conjuntos muy grandes de números pequeños almacenados consecutivamente, la eficiencia de la representación en memoria tiene un efecto directo en su rendimiento de ejecución, porque la gran cantidad de datos hace que el rendimiento entre la CPU y la memoria se convierta en un cuello de botella. En este caso, empaquetar datos más densamente significa que una sola búsqueda de línea de caché puede recuperar más datos.
  • Sin embargo, este razonamiento no se aplica si los datos no se almacenan o procesan consecutivamente.

La necesidad de lenguajes de programación para proporcionar una abstracción para enteros pequeños, incluso si se limita a contextos específicos

  • Estas necesidades a menudo surgen en el desarrollo de bibliotecas de software, incluidas las propias bibliotecas estándar del lenguaje. A continuación se presentan varios de estos casos.

Interoperabilidad

  • A menudo, los lenguajes de programación de nivel superior deben interactuar con el sistema operativo o piezas de software (bibliotecas) escritos en otros lenguajes de nivel inferior. Estos lenguajes de nivel inferior a menudo se comunican usando "structs" , que es una especificación rígida del diseño de memoria de un registro que consta de campos de diferentes tipos.
  • Por ejemplo, un lenguaje de nivel superior puede necesitar especificar que cierta función extranjera acepta una charmatriz de tamaño 256. (Ejemplo).
  • Algunas abstracciones utilizadas por los sistemas operativos y los sistemas de archivos requieren el uso de secuencias de bytes.
  • Algunos lenguajes de programación optan por proporcionar funciones de utilidad (por ejemplo BitConverter) para ayudar al empaquetado y desempaquetado de enteros estrechos en flujos de bits y flujos de bytes.
  • En estos casos, los tipos enteros más estrechos no necesitan ser un tipo primitivo integrado en el lenguaje. En cambio, se pueden proporcionar como un tipo de biblioteca.

Manejo de cuerdas

  • Hay aplicaciones cuyo propósito principal de diseño es manipular cadenas. Por lo tanto, la eficiencia del manejo de cadenas es importante para ese tipo de aplicaciones.

Manejo de formato de archivo

  • Se diseñaron muchos formatos de archivo con una mentalidad tipo C. Como tal, prevaleció el uso de campos de ancho estrecho.

Conveniencia, calidad del software y responsabilidad del programador.

  • Para muchos tipos de aplicaciones, el ensanchamiento automático de enteros en realidad no es una característica deseable. Tampoco lo es la saturación ni la envoltura (módulo).
  • Muchos tipos de aplicaciones se beneficiarán de la especificación explícita del programador de los valores permitidos más grandes en varios puntos críticos del software, como a nivel API.

Considere el siguiente escenario.

  • Una API de software acepta una solicitud JSON. La solicitud contiene una matriz de solicitudes secundarias. Toda la solicitud JSON se puede comprimir con el algoritmo Deflate.
  • Un usuario malintencionado crea una solicitud JSON que contiene mil millones de solicitudes secundarias. Todas las solicitudes secundarias son idénticas; El usuario malintencionado pretende que el sistema grabe algunos ciclos de CPU haciendo un trabajo inútil. Debido a la compresión, estas solicitudes secundarias idénticas se comprimen a un tamaño total muy pequeño.
  • Es obvio que un límite predefinido en el tamaño comprimido de los datos no es suficiente. En cambio, la API necesita imponer un límite predefinido en el número de solicitudes secundarias que puede contener, y / o un límite predefinido en el tamaño desinflado de los datos.

A menudo, el software que puede escalar de forma segura muchos órdenes de magnitud debe diseñarse para ese propósito, con una complejidad creciente. No llega automáticamente incluso si se elimina el problema del desbordamiento de enteros. Esto llega a un círculo completo que responde a la perspectiva del diseño del lenguaje: a menudo, el software que se niega a realizar un trabajo cuando se produce un desbordamiento entero involuntario (arrojando un error o excepción) es mejor que el software que cumple automáticamente con operaciones astronómicamente grandes.

Esto significa la perspectiva del OP,

¿Por qué es necesario que una persona establezca cuántos bytes se deben usar para un valor primitivo?

no es correcto. Al programador se le debe permitir, y a veces se le requiere, especificar la magnitud máxima que puede tomar un valor entero, en partes críticas del software. Como señala la respuesta de gardenhead , los límites naturales impuestos por los tipos primitivos no son útiles para este propósito; el lenguaje debe proporcionar formas para que los programadores declaren magnitudes y apliquen tales límites.

rwong
fuente
2

Todo proviene del hardware.

Un byte es la unidad de memoria direccionable más pequeña en la mayoría del hardware.

Cada tipo que acaba de mencionar está construido a partir de un múltiplo de bytes.

Un byte es de 8 bits. Con eso puedes expresar 8 booleanos pero no puedes buscar solo uno a la vez. Se dirige a 1, se dirige a los 8.

Y solía ser así de simple, pero luego pasamos de un bus de 8 bits a un bus de 16, 32 y ahora de 64 bits.

Lo que significa que si bien aún podemos direccionar en el nivel de bytes, ya no podemos recuperar un solo byte de la memoria sin obtener sus bytes vecinos.

Frente a este hardware, los diseñadores de idiomas eligieron permitirnos elegir tipos que nos permitieran elegir tipos que se ajustaran al hardware.

Puede afirmar que ese detalle puede y debe abstraerse, especialmente en un lenguaje que apunta a ejecutarse en cualquier hardware. Esto tendría problemas de rendimiento ocultos, pero puede que tenga razón. Simplemente no sucedió de esa manera.

Java realmente intenta hacer esto. Los bytes se promueven automáticamente a Ints. Un hecho que te volverá loco la primera vez que intentes hacer un trabajo de cambio de bits serio en él.

Entonces, ¿por qué no funcionó bien?

El gran argumento de venta de Java en el pasado es que podrías sentarte con un algoritmo C bien conocido, escribirlo en Java y con pequeños ajustes funcionaría. Y C está muy cerca del hardware.

Mantener ese tamaño y abstraer el tamaño de los tipos integrales simplemente no funcionó en conjunto.

Entonces podrían haberlo hecho. Simplemente no lo hicieron.

Quizás el programador no quiera que alguien pueda usar un número mayor que cierto tamaño y esto les permite limitarlo.

Este es un pensamiento válido. Hay métodos para hacer esto. La función de sujeción para uno. Un lenguaje podría llegar a romper límites arbitrarios en sus tipos. Y cuando esos límites se conocen en tiempo de compilación, eso permitiría optimizaciones en cómo se almacenan esos números.

Java simplemente no es ese lenguaje.

naranja confitada
fuente
" Un lenguaje podría llegar a establecer límites arbitrarios en sus tipos " Y, de hecho, Pascal tiene una forma de esto con los tipos de subrango.
Peter Taylor
1

Probablemente, una razón importante de por qué existen estos tipos en Java es simple y angustiosamente no técnica:

¡C y C ++ también tenían estos tipos!

Aunque es difícil proporcionar una prueba de que esta es la razón, hay al menos algunas pruebas sólidas: la especificación del lenguaje Oak (versión 0.2) contiene el siguiente pasaje:

3.1 Tipos enteros

Los enteros en el lenguaje Oak son similares a los de C y C ++, con dos excepciones: todos los tipos de enteros son independientes de la máquina, y algunas de las definiciones tradicionales se han cambiado para reflejar los cambios en el mundo desde que se introdujo C. Los cuatro tipos enteros tienen anchos de 8, 16, 32 y 64 bits, y están firmados a menos que el unsignedmodificador los prefija .

Entonces la pregunta podría reducirse a:

¿Por qué se inventaron short, int y long en C?

No estoy seguro de si la respuesta a la pregunta de la carta es satisfactoria en el contexto de la pregunta que se hizo aquí. Pero en combinación con las otras respuestas aquí, podría quedar claro que puede ser beneficioso tener estos tipos (independientemente de si su existencia en Java es solo un legado de C / C ++).

Las razones más importantes en las que puedo pensar son

  • Un byte es la unidad de memoria direccionable más pequeña (como ya mencionó CandiedOrange). A bytees el bloque de construcción elemental de datos, que se puede leer desde un archivo o a través de la red. Debe existir alguna representación explícita de esto (y existe en la mayoría de los idiomas, incluso cuando a veces viene disfrazado).

  • Es cierto que, en la práctica, tendría sentido representar todos los campos y variables locales usando un solo tipo, y llamar a este tipo int. Hay una pregunta relacionada al respecto en stackoverflow: ¿Por qué la API de Java usa int en lugar de short o byte? . Como mencioné en mi respuesta allí, una justificación para tener los tipos más pequeños ( bytey short) es que puede crear matrices de estos tipos: Java tiene una representación de matrices que todavía está bastante "cerca del hardware". A diferencia de otros lenguajes (y en contraste con las matrices de objetos, como una Integer[n]matriz), una int[n]matriz no es una colección de referencias donde los valores están dispersos por todo el montón. En cambio, lo haráen la práctica, sea un bloque consecutivo de n*4bytes: un trozo de memoria con un tamaño y un diseño de datos conocidos. Cuando tiene la opción de almacenar 1000 bytes en una colección de objetos de valor entero de tamaño arbitrario, o en un byte[1000](que toma 1000 bytes), este último puede ahorrar algo de memoria. (Algunas otras ventajas de esto pueden ser más sutiles y solo resultar obvias al interactuar Java con bibliotecas nativas)


Con respecto a los puntos sobre los que ha preguntado específicamente:

¿No podría determinarse el tamaño dinámicamente dependiendo de qué tan grande fue el número pasado?

Establecer dinámicamente el tamaño de los datos significaría que también debería ser capaz de cambiar dinámicamente. ¿Esto podría causar problemas de rendimiento?

Es probable que sea posible establecer dinámicamente el tamaño de las variables, si se considera diseñar un lenguaje de programación completamente nuevo desde cero. No soy un experto en la construcción de compiladores, pero creo que sería difícil manipular sensiblemente colecciones de tipos que cambian dinámicamente, especialmente cuando se tiene un lenguaje fuertemente tipado. Por lo tanto, probablemente se reduciría a todos los números que se almacenan en un "tipo de datos de números de precisión genéricos y arbitrarios", lo que ciertamente tendría un impacto en el rendimiento. Por supuesto, no son lenguajes de programación que están fuertemente tipado y / u ofrecen tipos de números de tamaño arbitrario, pero no creo que hay un verdadero lenguaje de programación de propósito general que fue de esta manera.


Notas al margen:

  • Es posible que se haya preguntado sobre el unsignedmodificador que se mencionó en la especificación Oak. De hecho, también contiene una observación: " unsignedaún no está implementado; puede que nunca lo esté". . Y tenían razón.

  • Además de preguntarse por qué C / C ++ tenía estos diferentes tipos de enteros, es posible que se pregunte por qué los confundieron tan horriblemente que nunca se sabe cuántos bits inttiene. Las justificaciones para esto generalmente están relacionadas con el rendimiento y se pueden buscar en otros lugares.

Marco13
fuente
0

Ciertamente muestra que aún no se le ha enseñado sobre rendimiento y arquitecturas.

  • Primero, no todos los procesadores pueden manejar los tipos grandes, por lo tanto, debe conocer las limitaciones y trabajar con eso.
  • Segundo, los tipos más pequeños significan más rendimiento al realizar operaciones.
  • Además, el tamaño es importante, si tiene que almacenar datos en un archivo o base de datos, el tamaño afectará tanto el rendimiento como el tamaño final de todos los datos, por ejemplo, supongamos que tiene una tabla con 15 columnas y termina con varias millones de registros. La diferencia entre elegir un tamaño pequeño y necesario para cada columna o elegir solo el tipo más grande será una diferencia de posibles Gigas de datos y tiempo en el desempeño de las operaciones.
  • Además, se aplica en cálculos complejos, donde el tamaño de los datos que se procesan tendrá un gran impacto, como en los juegos, por ejemplo.

Ignorando la importancia del tamaño de los datos siempre afecta el rendimiento, debe utilizar tantos recursos como sea necesario, pero no más, siempre.

Esa es la diferencia entre un programa o sistema que hace cosas realmente simples y es increíblemente ineficiente que requiere muchos recursos y hace que el uso de ese sistema sea realmente costoso; o un sistema que hace mucho, pero funciona más rápido que otros y es realmente barato de ejecutar.

Néstor Mata Cuthbert
fuente
0

Hay un par de buenas razones.

(1) mientras que el almacenamiento de un byte variable frente a un largo es insignificante, el almacenamiento de millones en una matriz es muy significativo.

(2) la aritmética "nativa de hardware" basada en tamaños enteros particulares puede ser mucho más eficiente, y para algunos algoritmos en algunas plataformas, eso puede ser importante.

ddyer
fuente