byte + byte = int … ¿por qué?

Mirando este código de C #:

byte x = 1; byte y = 2; byte z = x + y; // ERROR: Cannot implicitly convert type 'int' to 'byte' 

El resultado de cualquier matemática realizada en tipos de byte (o short ) se convierte implícitamente en un entero. La solución es convertir explícitamente el resultado en un byte:

 byte z = (byte)(x + y); // this works 

Lo que me pregunto es por qué? ¿Es arquitectónico? ¿Filosófico?

Tenemos:

  • int + int = int
  • long + long = long
  • float + float = float
  • double + double = double

Entonces por qué no:

  • byte + byte = byte
  • short + short = short ?

Un poco de historia: estoy realizando una larga lista de cálculos sobre “números pequeños” (es decir, <8) y almacenando los resultados intermedios en una matriz grande. Usar una matriz de bytes (en lugar de una matriz int) es más rápido (debido a los hits de la caché). Pero los extensos byte-cast distribuidos a través del código lo hacen mucho más ilegible.

La tercera línea de tu fragmento de código:

 byte z = x + y; 

en realidad significa

 byte z = (int) x + (int) y; 

Por lo tanto, no hay operación + en bytes, los bytes se convierten primero en enteros y el resultado de la sum de dos enteros es un entero (de 32 bits).

En términos de “por qué sucede en absoluto” es porque no hay operadores definidos por C # para la aritmética con byte, sbyte, corto o ushort, tal como han dicho otros. Esta respuesta se trata de por qué esos operadores no están definidos.

Creo que es básicamente por el rendimiento. Los procesadores tienen operaciones nativas para hacer aritmética con 32 bits muy rápidamente. Volver a hacer la conversión desde el resultado a un byte automáticamente podría hacerse, pero daría lugar a penalizaciones de rendimiento en caso de que realmente no desee ese comportamiento.

Creo que esto se menciona en una de las normas anotadas de C #. Mirando…

EDITAR: molestamente, ahora he examinado la especificación anotada ECMA C # 2, la especificación anotada MS C # 3 y la especificación CLI de anotación, y ninguna de ellas menciona esto hasta donde puedo ver. Estoy seguro de que he visto la razón dada anteriormente, pero me volaron si sé dónde. Disculpas, fanáticos de referencia 🙁

Pensé que había visto esto en alguna parte antes. De este artículo, The Old New Thing :

Supongamos que vivimos en un mundo de fantasía donde las operaciones en ‘byte’ resultaron en ‘byte’.

 byte b = 32; byte c = 240; int i = b + c; // what is i? 

En este mundo de fantasía, ¡el valor de yo sería 16! ¿Por qué? Debido a que los dos operandos al operador + son ambos bytes, la sum “b + c” se calcula como un byte, lo que da como resultado 16 debido al desbordamiento de enteros. (Y, como noté antes, el desbordamiento de enteros es el nuevo vector de ataque de seguridad).

EDITAR : Raymond está defendiendo, esencialmente, el enfoque que C y C ++ tomaron originalmente. En los comentarios, defiende el hecho de que C # adopta el mismo enfoque, sobre la base de la compatibilidad hacia atrás del lenguaje.

DO#

ECMA-334 indica que la adición solo se define como legal en int + int, uint + uint, long + long y ulong + ulong (ECMA-334 14.7.4). Como tal, estas son las operaciones candidatas que deben considerarse con respecto a 14.4.2. Debido a que existen conversiones implícitas desde byte a int, uint, long y ulong, todos los miembros de la función de adición son miembros de función aplicables bajo 14.4.2.1. Tenemos que encontrar el mejor elenco implícito según las reglas en 14.4.2.3:

La conversión (C1) a int (T1) es mejor que la conversión (C2) a uint (T2) o ulong (T2) porque:

  • Si T1 es int y T2 es uint, o ulong, C1 es la mejor conversión.

Casting (C1) a int (T1) es mejor que casting (C2) a long (T2) porque hay un lanzamiento implícito de int a long:

  • Si existe una conversión implícita de T1 a T2, y no existe una conversión implícita de T2 a T1, C1 es la mejor conversión.

Por lo tanto, se usa la función int + int, que devuelve un int.

Lo cual es una forma muy larga de decir que está muy enterrado en la especificación C #.

CLI

La CLI funciona solo en 6 tipos (int32, native int, int64, F, O y &). (Partición ECMA-335 3 sección 1.5)

Byte (int8) no es uno de esos tipos, y se coacciona automáticamente a un int32 antes de la adición. (Partición ECMA-335 3 sección 1.6)

Las respuestas que indican una ineficiencia al agregar bytes y truncar el resultado a un byte son incorrectas. Los procesadores x86 tienen instrucciones diseñadas específicamente para operaciones enteras en cantidades de 8 bits.

De hecho, para los procesadores x86 / 64, realizar operaciones de 32 bits o 16 bits es menos eficiente que las operaciones de 64 bits u 8 bits debido al byte de prefijo del operando que debe decodificarse. En máquinas de 32 bits, realizar operaciones de 16 bits conlleva la misma penalización, pero todavía hay códigos de operación dedicados para operaciones de 8 bits.

Muchas architectures RISC tienen instrucciones nativas similares de palabra / byte eficientes. Aquellos que generalmente no tienen un store-and-convert-to-signed-value-of-some-bit-length.

En otras palabras, esta decisión debe haberse basado en la percepción de para qué es el tipo de byte, no debido a ineficiencias subyacentes del hardware.

Recuerdo que una vez leí algo de Jon Skeet (no puedo encontrarlo ahora, seguiré buscando) sobre cómo el byte en realidad no sobrecarga el operador +. De hecho, al agregar dos bytes como en su muestra, cada byte se está convirtiendo implícitamente a un int. El resultado de eso es obviamente un int. Ahora, en cuanto a POR QUÉ esto fue diseñado de esta manera, esperaré a Jon Skeet para que lo publique 🙂

EDIT: ¡Lo encontré! Gran información sobre este tema aquí .

Esto se debe al desbordamiento y acarreo.

Si agrega dos números de 8 bits, podrían desbordarse en el noveno bit.

Ejemplo:

  1111 1111 + 0000 0001 ----------- 1 0000 0000 

No estoy seguro, pero supongo que los ints , longs y doubles tienen más espacio porque son bastante grandes como son. Además, son múltiplos de 4, que son más eficientes para el manejo de las computadoras, debido a que el ancho del bus de datos interno es de 4 bytes o 32 bits (los 64 bits son cada vez más comunes) de ancho. Byte y short son un poco más ineficientes, pero pueden ahorrar espacio.

Desde la especificación del lenguaje C # 1.6.7.5 7.2.6.2 Promociones numéricas binarias convierte ambos operandos en int si no puede encajar en varias otras categorías. Supongo que no sobrecargaron el operador + para tomar el byte como un parámetro, pero quieren que actúe de forma un tanto normal para que simplemente usen el tipo de datos int.

Especificación del lenguaje C #

Mi sospecha es que C # está realmente llamando al operator+ definido en int (que devuelve un int menos que esté en un bloque checked ), e implícitamente convierte ambos bytes / shorts en ints . Es por eso que el comportamiento parece inconsistente.

Esta fue probablemente una decisión práctica por parte de los diseñadores de idiomas. Después de todo, un int es un Int32, un entero con signo de 32 bits. Cada vez que realice una operación entera en un tipo más pequeño que int, se convertirá en un int 32 bits firmado por la mayoría de las CPU de 32 bits de todos modos. Eso, combinado con la probabilidad de desbordamiento de enteros pequeños, probablemente selló el trato. Le ahorra la tarea rutinaria de verificar continuamente el exceso / defecto de flujo, y cuando el resultado final de una expresión en bytes estaría dentro del rango, a pesar del hecho de que en alguna etapa intermedia estaría fuera de rango, obtendrá una respuesta correcta. resultado.

Otro pensamiento: el sobre / bajo flujo en estos tipos tendría que simularse, ya que no ocurriría naturalmente en las CPU de destino más probables. ¿Por qué molestarse?

Esta es en su mayor parte mi respuesta que pertenece a este tema, presentada primero a una pregunta similar aquí .

Todas las operaciones con números enteros menores que Int32 se redondean a 32 bits antes del cálculo por defecto. La razón por la cual el resultado es Int32 es simplemente dejarlo tal como está después del cálculo. Si comprueba los códigos de operación aritméticos de MSIL, el único tipo numérico integral con el que operan son Int32 e Int64. Es “por diseño”.

Si desea el resultado en formato Int16, es irrelevante si realiza el molde en el código, o el comstackdor (hipotéticamente) emite la conversión “debajo del capó”.

Por ejemplo, para hacer aritmética Int16:

 short a = 2, b = 3; short c = (short) (a + b); 

Los dos números se expandirían a 32 bits, se agregarían y luego se truncarían de nuevo a 16 bits, que es lo que MS pretendía que fuera.

La ventaja de utilizar short (o byte) es principalmente el almacenamiento en los casos en que tenga cantidades masivas de datos (datos gráficos, transmisión, etc.)

Creo que es una decisión de diseño sobre qué operación era más común … Si byte + byte = byte tal vez a mucha más gente le moleste tener que convertir a int cuando se requiera una int como resultado.

La adición no está definida para bytes. Entonces se lanzan a int para la adición. Esto es cierto para la mayoría de las operaciones matemáticas y bytes. (tenga en cuenta que esta es la forma en que solía ser en los idiomas más antiguos, estoy asumiendo que es cierto hoy).

Desde el código de .NET Framework:

 // bytes private static object AddByte(byte Left, byte Right) { short num = (short) (Left + Right); if (num > 0xff) { return num; } return (byte) num; } // shorts (int16) private static object AddInt16(short Left, short Right) { int num = Left + Right; if ((num < = 0x7fff) && (num >= -32768)) { return (short) num; } return num; } 

Simplifica con .NET 3.5 y superior:

 public static class Extensions { public static byte Add(this byte a, byte b) { return (byte)(a + b); } } 

ahora puedes hacer:

 byte a = 1, b = 2, c; c = a.Add(b); 

Además de todos los otros excelentes comentarios, pensé en agregar un pequeño tidbit. Muchos comentarios se han preguntado por qué int, long y prácticamente cualquier otro tipo numérico no sigue esta regla … devuelve un tipo “más grande” en respuesta al aritmático.

Muchas respuestas han tenido que ver con el rendimiento (bueno, 32 bits es más rápido que 8 bits). En realidad, un número de 8 bits sigue siendo un número de 32 bits para una CPU de 32 bits … incluso si agrega dos bytes, la cantidad de datos en los que opera la CPU será de 32 bits independientemente … por lo que agregar los datos no va a funcionar. ser “más rápido” que agregar dos bytes … es lo mismo para la CPU. AHORA, agregar dos entradas será más rápido que agregar dos largos en un procesador de 32 bits, porque agregar dos largos requiere más microops porque se trabaja con números más amplios que la palabra del procesador.

Creo que la razón fundamental para hacer que la aritmética de bytes resulte en resultados es bastante clara y directa: 8bits simplemente no llega muy lejos. : D Con 8 bits, tiene un rango sin signo de 0-255. No hay mucho espacio para trabajar … la probabilidad de que te encuentres con limitaciones de bytes es MUY alta cuando los usas en aritmética. Sin embargo, la posibilidad de que te quedes sin bits cuando trabajas con ints, o longs, o dobles, etc. es significativamente más baja … lo suficientemente baja como para que rara vez encontremos la necesidad de más.

La conversión automática de byte a int es lógica porque la escala de un byte es muy pequeña. La conversión automática de int a long, float a double, etc. no es lógica porque esos números tienen una escala significativa.