byte + byte = int ... ¿por qué?

365

Mirando este código 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 byte(o short) tipos se devuelve implícitamente a un entero. La solución es devolver explícitamente el resultado a 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 antecedentes: estoy realizando una larga lista de cálculos sobre "números pequeños" (es decir, <8) y almacenando los resultados intermedios en una gran matriz. Usar una matriz de bytes (en lugar de una matriz int) es más rápido (debido a los éxitos de caché). Pero los extensos conjuntos de bytes distribuidos a través del código lo hacen mucho más ilegible.

Robert Cartaino
fuente
10
Lo que sería útil aquí no es el conocimiento de Eric del estándar ; es su conocimiento del diseño del lenguaje; que no porque Pero sí, la respuesta de Eric sería bastante definitiva :)
Jon Skeet
143
Las diversas reflexiones a continuación son una aproximación razonable de las consideraciones de diseño. Más en general: no pienso en los bytes como "números"; Pienso en ellos como patrones de bits que podrían interpretarse como números, caracteres o colores o lo que sea. Si va a hacer cálculos matemáticos y tratarlos como números, entonces tiene sentido mover el resultado a un tipo de datos que se interprete más comúnmente como un número.
Eric Lippert
28
@Eric: Eso tiene mucho sentido para el byte, pero probablemente no tenga tanto sentido para short / ushort.
Jon Skeet
23
@Eric: byte1 | byte2no los trata en absoluto como números. Esto los trata con precisión como patrones de bits. Entiendo su punto de vista, pero sucede que cada vez que hice aritmética en bytes en C #, en realidad los estaba tratando como bits, no como números, y este comportamiento siempre está en el camino.
Roman Starkov

Respuestas:

228

La tercera línea de su 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 suma de dos enteros es un entero (32 bits).

azheglov
fuente
He intentado el código a continuación pero todavía no funciona. byte z = (byte) x + (byte) y;
Anónimo
10
eso es porque no hay operación + para bytes (ver arriba). Pruebe el byte z = (byte) ((int) x + (int) y)
azheglov el
35
Esta tiene que ser la respuesta más correcta y concisa. No hay ningún operando para agregar entre bytes, por lo que en lugar de explicar por qué "agregar dos bytes" funciona o no ( nunca sucedió ), esto muestra claramente por qué el resultado es un int, porque lo único que sucedió es una suma de 2 ints .
RichardTheKiwi
2
Me mareé leyendo todas las otras respuestas (sin ofender al Sr. Jon Skeet). Esto me pareció la respuesta más simple que describe correctamente lo que sucede debajo del capó. ¡Gracias!
rayryeng
Aquí hay una respuesta que escribí en otro lugar que contiene un programa para identificar cuándo se intproduce esta promoción automática impulsada por el compilador : stackoverflow.com/a/43578929/4561887
Gabriel Staples
172

En términos de "por qué sucede en absoluto" es porque no hay operadores definidos por C # para aritmética con byte, sbyte, short o ushort, tal como lo han dicho otros. Esta respuesta es acerca de por qué esos operadores no están definidos.

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

Creo que esto se menciona en uno de los estándares anotados de C #. Mirando...

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

Jon Skeet
fuente
14
Lamento decir eso, pero creo que esta no es la mejor respuesta.
VVS
42
¿Ha rechazado cada respuesta que considera que no es la mejor? ;)
Jon Skeet
55
(Solo para aclarar, realmente no voy a intentarlo. Parece que todos tienen sus propios criterios para rechazar el voto, y eso está bien. Solo rechazo una respuesta si creo que es activamente perjudicial en lugar de simplemente no ideal. )
Jon Skeet
21
Utilizo la votación como un instrumento para obtener la "mejor" respuesta a la cima. En realidad, descubrí que no dijiste mucho en tu respuesta, que fue la razón principal de mi voto negativo. Otra razón puede ser mi sentimiento subjetivo de que su representante le da una gran ventaja cuando se trata de votar y está llegando a la cima de "mejores" respuestas.
VVS
23
En mi opinión, la mejor manera de obtener la "mejor" respuesta a la cima es votarlo. Para ser honesto, creo que la respuesta es más informativo que aquí el comentario de Eric en la pregunta ... pero aparte de eso, para la perspectiva del diseño (en contraposición a la "lo que el compilador de hacer" perspectiva) Creo que no hay es mucho responder más allá del "rendimiento". En particular, realmente no compro el argumento "evita el desbordamiento" (17 votos) ya que sugeriría int + int = long.
Jon Skeet
68

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

Supongamos que vivimos en un mundo de fantasía donde las operaciones en 'byte' dieron como resultado 'byte'.

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

¡En este mundo de fantasía, el valor de i sería 16! ¿Por qué? Debido a que los dos operandos para el operador + son ambos bytes, la suma "b + c" se calcula como un byte, lo que resulta en 16 debido al desbordamiento de enteros. (Y, como señalé anteriormente, el desbordamiento de enteros es el nuevo vector de ataque de seguridad).

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

Michael Petrotta
fuente
42
Con números enteros si los agregamos y se desborda, no lo convierte automáticamente como un tipo de datos diferente, entonces, ¿por qué hacerlo con byte?
Ryan
2
Con ints se desborda. Intente agregar int.MaxValue + 1 y obtenga -2147483648 en lugar de 2147483648.
David Basarab
8
@ Longhorn213: Sí, eso es lo que dice Ryan: las matemáticas int pueden desbordarse, pero las matemáticas int no devuelven largos.
Michael Petrotta
28
Exactamente. Si esto pretende ser una medida de seguridad, es una muy mal implementada;)
Jon Skeet
55
@Ryan: "perezoso" es un cargo bastante fuerte contra los diseñadores de lenguaje C #, por algo tan básico como las matemáticas primitivas. Si desea acusarlos de algo, haga que sea "excesiva compatibilidad con C / C ++".
Michael Petrotta
58

C#

ECMA-334 establece que la suma 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 a considerar con respecto a 14.4.2. Debido a que hay conversiones implícitas de byte a int, uint, long y ulong, todos los miembros de la función de suma son miembros de la función aplicables según 14.4.2.1. Tenemos que encontrar el mejor elenco implícito de 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.

La conversión (C1) a int (T1) es mejor que la conversión (C2) a larga (T2) porque hay una conversión implícita de int a larga:

  • 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 utiliza la función int + int, que devuelve un int.

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

CLI

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

Byte (int8) no es uno de esos tipos y se convierte automáticamente en int32 antes de la adición. (ECMA-335 partición 3 sección 1.6)

Alun Harford
fuente
Que la ECMA solo especifique esas operaciones particulares no evitaría que un lenguaje implemente otras reglas. VB.NET permitirá de manera útil byte3 = byte1 And byte2sin una conversión, pero sin ayuda arrojará una excepción de tiempo de ejecución si int1 = byte1 + byte2arroja un valor superior a 255. No sé si algún idioma permitiría byte3 = byte1+byte2y arrojaría una excepción cuando exceda 255, pero no arrojará una excepción si int1 = byte1+byte2rinde un valor en el rango 256-510.
supercat
26

Las respuestas que indican cierta ineficiencia al agregar bytes y truncar el resultado a un byte son incorrectas. Los procesadores x86 tienen instrucciones diseñadas específicamente para la operación de enteros 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 u 8 bits debido al byte de prefijo de 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 arquitecturas RISC tienen instrucciones eficientes de palabras / bytes nativos similares. Aquellos que generalmente no tienen un valor de almacenar y convertir a valor firmado de una longitud de bits.

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

Christopher
fuente
+1; si tan solo esta percepción no estuviera mal cada vez que alguna vez he cambiado y OR dos bytes en C # ...
Roman Starkov
No debería haber ningún costo de rendimiento para truncar el resultado. En el ensamblaje x86, es solo la diferencia entre copiar un byte del registro o cuatro bytes del registro.
Jonathan Allen el
1
@ JonathanAllen Exactamente. La única diferencia es, irónicamente, cuando se realiza una conversión de ampliación . El diseño actual incurre en una penalización de rendimiento para ejecutar la instrucción de ampliación (ya sea extensión firmada o extensión no firmada)
reirab
" percepción de para qué es el tipo de byte ": eso puede explicar este comportamiento para byte(y char), pero no para lo shortque semánticamente es claramente un número.
sonríe el
13

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

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

BFree
fuente
9

Esto se debe a 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 ints, longsy se doublesles da más espacio porque son bastante grandes. Además, son múltiplos de 4, que son más eficientes para las computadoras, debido a que el ancho del bus de datos interno es de 4 bytes o 32 bits (64 bits se está volviendo más frecuente ahora) de ancho. Byte y short son un poco más ineficientes, pero pueden ahorrar espacio.

Samoz
fuente
23
Pero los tipos de datos más grandes no siguen el mismo comportamiento.
Inisheer
12
Los problemas de desbordamiento están a un lado. Si tuviera que tomar su lógica y aplicarla al lenguaje, entonces todos los tipos de datos devolverían un tipo de datos más grande después de la aritmética de suma, lo que definitivamente NO es el caso. int + int = int, long + long = long. Creo que la pregunta se refiere a la inconsistencia.
Joseph
Ese fue mi primer pensamiento, pero ¿por qué int + int = long? Entonces no estoy comprando el argumento del "posible desbordamiento" ... todavía <grin>.
Robert Cartaino
11
Ah, y sobre el argumento "posible desbordamiento", ¿por qué no byte + byte = short?
Robert Cartaino
A) ¿Por qué funciona de la manera en que funciona dadas las reglas de C #? Vea mi respuesta a continuación. B) ¿Por qué fue diseñado de la manera que es? Probablemente solo consideraciones de usabilidad, basadas en juicios subjetivos sobre la forma en que la mayoría de las personas tienden a usar ints y bytes.
mqp
5

De la especificación del lenguaje C # 1.6.7.5 7.2.6.2 Promociones numéricas binarias convierte ambos operandos a int si no puede encajarlo en varias otras categorías. Supongo que no sobrecargaron el operador + para tomar el byte como parámetro, pero quieren que actúe de manera algo normal, por lo que solo usan el tipo de datos int.

Especificación de lenguaje C #

Ryan
fuente
4

Mi sospecha es que C # en realidad está llamando a lo operator+definido en int(que devuelve un a intmenos que esté en un checkedbloque), y está emitiendo implícitamente ambos bytes/ shortsa ints. Es por eso que el comportamiento parece inconsistente.

mqp
fuente
3
Empuja ambos bytes en la pila, luego llama al comando "agregar". En IL, agregue "come" los dos valores y los reemplace con un int.
Jonathan Allen el
3

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. Siempre que realice una operación entera en un tipo más pequeño que int, la mayoría de las CPU de 32 bits la convertirá en un int con signo de 32 bits. Eso, combinado con la probabilidad de desbordar enteros pequeños, probablemente cerró el trato. Le ahorra la tarea de verificar continuamente el flujo excesivo o insuficiente, y cuando el resultado final de una expresión en bytes estaría dentro del rango, a pesar de que en alguna etapa intermedia estaría fuera del rango, obtendrá un resultado correcto resultado.

Otro pensamiento: el flujo excesivo / insuficiente en estos tipos tendría que ser simulado, ya que no ocurriría naturalmente en las CPU objetivo más probables. ¿Por qué molestarse?

PeterAllenWebb
fuente
2

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

Todas las operaciones con números integrales más pequeños que Int32 se redondean hasta 32 bits antes del cálculo por defecto. La razón por la cual el resultado es Int32 es simplemente dejarlo como está después del cálculo. Si marca los códigos de operación aritméticos de MSIL, el único tipo numérico integral con el que operan es Int32 e Int64. Es "por diseño".

Si desea que el resultado vuelva a estar en formato Int16, es irrelevante si realiza la conversión en código, o el compilador (hipotéticamente) emite la conversión "bajo el 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 nuevamente a 16 bits, que es como MS pretendía que fuera.

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

Kenan EK
fuente
1

La suma no está definida para bytes. Por lo tanto, se envían a int para la adición. Esto es cierto para la mayoría de las operaciones matemáticas y bytes. (tenga en cuenta que así es como solía estar en los idiomas más antiguos, supongo que hoy es cierto).

Jim C
fuente
0

Creo que es una decisión de diseño sobre qué operación era más común ... Si byte + byte = byte quizás mucha más gente se molestará al tener que enviar a int cuando se requiere un int como resultado.

fortran
fuente
2
Por una vez me molesta lo contrario :) Parece que siempre necesito el resultado del byte, así que siempre tengo que emitir.
Roman Starkov
Excepto que no tienes que enviar a int. El reparto es implícito. Solo la otra forma es explícita.
Niki
1
@nikie Creo que no entendiste mi respuesta. Si agregar dos bytes produciría un byte, para evitar desbordamientos, alguien tendría que lanzar los operandos (no el resultado) a int antes de la adición.
fortran
0

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;
}

Simplifique 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);

serhio
fuente
0

He probado el rendimiento entre byte e int.
Con valores int:

class Program
{
    private int a,b,c,d,e,f;

    public Program()
    {
        a = 1;
        b = 2;
        c = (a + b);
        d = (a - b);
        e = (b / a);
        f = (c * b);
    }

    static void Main(string[] args)
    {
        int max = 10000000;
        DateTime start = DateTime.Now;
        Program[] tab = new Program[max];

        for (int i = 0; i < max; i++)
        {
            tab[i] = new Program();
        }
        DateTime stop = DateTime.Now;

        Debug.WriteLine(stop.Subtract(start).TotalSeconds);
    }
}

Con valores de bytes:

class Program
{
    private byte a,b,c,d,e,f;

    public Program()
    {
        a = 1;
        b = 2;
        c = (byte)(a + b);
        d = (byte)(a - b);
        e = (byte)(b / a);
        f = (byte)(c * b);
    }

    static void Main(string[] args)
    {
        int max = 10000000;
        DateTime start = DateTime.Now;
        Program[] tab = new Program[max];

        for (int i = 0; i < max; i++)
        {
            tab[i] = new Program();
        }
        DateTime stop = DateTime.Now;

        Debug.WriteLine(stop.Subtract(start).TotalSeconds);
    }
}

Aquí el resultado:
byte: 3.57s 157mo, 3.71s 171mo, 3.74s 168mo con CPU ~ = 30%
int: 4.05s 298mo, 3.92s 278mo, 4.28 294mo con CPU ~ = 27%
Conclusión: el
byte usa más la CPU pero cuesta menos memoria y es más rápido (tal vez porque hay menos bytes para asignar)

puipuix
fuente
-1

Además de todos los otros excelentes comentarios, pensé que agregaría un pequeño dato. 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 a la aritmética.

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 porción de datos en la que opera la CPU será de 32 bits independientemente ... por lo que agregar ints no va a ser "más rápido" que agregar dos bytes ... todo es lo mismo a 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 ya que está trabajando con números más anchos que la palabra del procesador.

Creo que la razón fundamental para hacer que la aritmética de bytes produzca ints es bastante clara y directa: ¡8 bits simplemente no llega muy lejos! : D Con 8 bits, tiene un rango sin signo de 0-255. Eso no es mucho espacio para trabajar ... la probabilidad de que encuentres limitaciones de bytes es MUY alta cuando las usas en aritmética. Sin embargo, la posibilidad de que se quede sin bits cuando trabaje con ints, o longs, o dobles, etc. es significativamente menor ... lo suficientemente baja como para que raramente 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.

jrista
fuente
Esto todavía no explica por qué byte - byteregresa int, o por qué no lanzan a short...
KthProg
¿Por qué quieres que la suma devuelva un tipo diferente que la resta? Si se byte + bytedevuelve int, debido a que 255 + cualquier cosa es mayor de lo que puede contener un byte, no tiene sentido que ningún byte menos cualquier otro byte devuelva otra cosa que no sea un int desde un punto de vista de consistencia de tipo de retorno.
jrista
No lo haría, solo muestra que la razón anterior probablemente no sea correcta. Si tuviera que ver con "encajar" en el resultado, entonces la byteresta devolvería a byte, y la suma de bytes devolvería a short( byte+ bytesiempre encajará en a short). Si se tratara de consistencia como usted dice, entonces shortsería suficiente para ambas operaciones en lugar de hacerlo int. Claramente, hay una mezcla de razones, no todas ellas necesariamente bien pensadas. O bien, la razón de rendimiento que figura a continuación puede ser más precisa.
KthProg