octet + octet = int… pourquoi?

365

En regardant ce code C #:

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

Le résultat de tout calcul effectué sur byte(ou short) les types est implicitement restitué en entier. La solution consiste à convertir explicitement le résultat en octet:

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

Je me demande pourquoi? Est-ce architectural? Philosophique?

On a:

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

Alors pourquoi pas:

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

Un peu de contexte: j'effectue une longue liste de calculs sur des "petits nombres" (c'est-à-dire <8) et je stocke les résultats intermédiaires dans un grand tableau. L'utilisation d'un tableau d'octets (au lieu d'un tableau int) est plus rapide (en raison des accès au cache). Mais les octets étendus diffusés dans le code le rendent encore plus illisible.

Robert Cartaino
la source
10
Ce n'est pas la connaissance d'Eric de la norme qui serait utile ici - c'est sa connaissance de la conception du langage; quoi pas pourquoi. Mais oui, la réponse d'Eric serait assez définitive :)
Jon Skeet
143
Les différentes réflexions ci-dessous sont une approximation raisonnable des considérations de conception. Plus généralement: je ne considère pas les octets comme des "nombres"; Je les considère comme des motifs de bits qui pourraient être interprétés comme des nombres, ou des caractères, ou des couleurs ou autre. Si vous allez faire des calculs sur eux et les traiter comme des nombres, il est logique de déplacer le résultat dans un type de données qui est plus communément interprété comme un nombre.
Eric Lippert
28
@Eric: Cela a beaucoup de sens pour l'octet, mais probablement moins pour le short / ushort.
Jon Skeet
23
@Eric: byte1 | byte2ne les traite pas du tout comme des nombres. Cela les traite précisément comme des modèles de bits. Je comprends votre point de vue, mais il se trouve que chaque fois que je faisais de l'arithmétique sur des octets en C #, je les traitais en fait comme des bits, pas des nombres, et ce comportement est toujours gênant.
Roman Starkov

Réponses:

228

La troisième ligne de votre extrait de code:

byte z = x + y;

signifie en fait

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

Il n'y a donc pas d'opération + sur les octets, les octets sont d'abord convertis en entiers et le résultat de l'addition de deux entiers est un entier (32 bits).

azheglov
la source
J'ai essayé le code ci-dessous mais cela ne fonctionne toujours pas. octet z = (octet) x + (octet) y;
Anonyme
10
c'est parce qu'il n'y a pas d'opération + pour les octets (voir ci-dessus). Essayez l'octet z = (octet) ((int) x + (int) y)
azheglov
35
Cela doit être la réponse la plus correcte et la plus concise. Il n'y a pas d'opérande à ajouter entre octets, donc au lieu d'expliquer pourquoi "l'ajout de deux octets" fonctionne ou non ( cela ne s'est jamais produit ), cela montre clairement pourquoi le résultat est un entier, car la seule chose qui s'est produite est un ajout de 2 pouces .
RichardTheKiwi
2
J'ai eu le vertige en lisant toutes les autres réponses (sans offenser M. Jon Skeet). J'ai trouvé que c'était la réponse la plus simple qui décrit correctement ce qui se passe sous le capot. Merci!
rayryeng
Voici une réponse que j'ai écrite ailleurs qui contient un programme pour identifier le moment où se produit cette promotion automatique pilotée par le compilateur int: stackoverflow.com/a/43578929/4561887
Gabriel Staples
172

En termes de "pourquoi cela se produit", c'est parce qu'il n'y a aucun opérateur défini par C # pour l'arithmétique avec octet, sbyte, court ou ushort, comme d'autres l'ont dit. Cette réponse explique pourquoi ces opérateurs ne sont pas définis.

Je crois que c'est essentiellement pour des raisons de performance. Les processeurs ont des opérations natives pour faire de l'arithmétique avec 32 bits très rapidement. La conversion automatique du résultat en octet pourrait être effectuée, mais entraînerait des pénalités de performances dans le cas où vous ne souhaitez pas réellement ce comportement.

Je pense que cela est mentionné dans l'une des normes annotées C #. Vous cherchez ...

EDIT: De manière ennuyeuse, j'ai maintenant parcouru la spécification ECMA C # 2 annotée, la spécification MS C # 3 annotée et la spécification CLI d'annotation, et aucune d'entre elles ne mentionne cela pour autant que je puisse voir. Je suis sûr d' avoir vu la raison donnée ci-dessus, mais je suis stupéfait si je sais où. Excuses, fans de référence :(

Jon Skeet
la source
14
Je suis désolé de le dire, mais je trouve que ce n'est pas la meilleure réponse.
VVS
42
Avez-vous dévalorisé chaque réponse que vous estimez ne pas être la meilleure? ;)
Jon Skeet
55
(Juste pour clarifier, je ne me lance pas vraiment contre vous. Il semble que chacun ait ses propres critères pour le vote négatif, et ça va. Je ne retire une réponse que si je pense qu'elle est activement nuisible plutôt que simplement non idéale. )
Jon Skeet
21
J'utilise le vote comme un instrument pour obtenir la "meilleure" réponse au sommet. En fait, j'ai trouvé que vous n'avez pas dit grand-chose du tout dans votre réponse, ce qui était la principale raison de mon vote négatif. Une autre raison peut-être mon sentiment subjectif que votre représentant vous donne un gros bonus quand il s'agit de voter et que vous arrivez en tête de "meilleures" réponses.
VVS
23
OMI, la meilleure façon d'obtenir la "meilleure" réponse en haut est de voter pour cela. Pour être honnête, je pense que la réponse la plus informative ici est le commentaire d'Eric dans la question ... mais à part cela, pour la perspective de conception (par opposition à la perspective "ce que fait le compilateur"), je ne pense pas qu'il y ait beaucoup répondre au-delà de la «performance». En particulier, je n'achète vraiment pas l'argument "il empêche le débordement" (17 votes) car cela suggérerait int + int = long.
Jon Skeet
68

Je pensais avoir déjà vu ça quelque part. De cet article, The Old New Thing :

Supposons que nous vivions dans un monde fantastique où les opérations sur «octet» ont abouti à «octet».

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

Dans ce monde fantastique, la valeur de i serait 16! Pourquoi? Étant donné que les deux opérandes de l'opérateur + sont tous deux des octets, la somme "b + c" est calculée comme un octet, ce qui se traduit par 16 en raison d'un débordement d'entier. (Et, comme je l'ai noté précédemment, le débordement d'entier est le nouveau vecteur d'attaque de sécurité.)

EDIT : Raymond défend essentiellement l'approche C et C ++ adoptée à l'origine. Dans les commentaires, il défend le fait que C # adopte la même approche, pour des raisons de compatibilité descendante du langage.

Michael Petrotta
la source
42
Avec des entiers si nous les ajoutons et qu'il déborde, il ne le convertit pas automatiquement en un type de données différent, alors pourquoi le faire avec octet?
Ryan
2
Avec les pouces, il déborde. Essayez d'ajouter int.MaxValue + 1, vous obtenez -2147483648 au lieu de 2147483648.
David Basarab
8
@ Longhorn213: Oui, c'est ce que Ryan dit: les mathématiques int peuvent déborder, mais les mathématiques int ne reviennent pas longtemps.
Michael Petrotta
28
Exactement. Si cela est censé être une mesure de sécurité, c'est une mesure très mal mise en œuvre;)
Jon Skeet
5
@Ryan: "paresseux" est une charge assez lourde à porter contre les concepteurs du langage C #, pour quelque chose d'aussi basique que les mathématiques primitives. Si vous voulez les accuser de quelque chose, faites-en une "rétrocompatibilité excessive avec C / C ++".
Michael Petrotta
58

C #

L'ECMA-334 déclare que l'addition est uniquement définie comme légale sur int + int, uint + uint, long + long et ulong + ulong (ECMA-334 14.7.4). En tant que telles, ce sont les opérations candidates à considérer en ce qui concerne 14.4.2. Parce qu'il y a des transtypages implicites d'octet en entier, uint, long et ulong, tous les membres de fonction d'addition sont des membres de fonction applicables sous 14.4.2.1. Nous devons trouver la meilleure distribution implicite par les règles du 14.4.2.3:

La conversion (C1) en int (T1) est meilleure que la conversion (C2) en uint (T2) ou ulong (T2) car:

  • Si T1 est int et T2 est uint, ou ulong, C1 est la meilleure conversion.

Le cast (C1) en int (T1) est meilleur que le cast (C2) en long (T2) car il y a un cast implicite de int en long:

  • S'il existe une conversion implicite de T1 à T2 et qu'aucune conversion implicite de T2 à T1 n'existe, C1 est la meilleure conversion.

Par conséquent, la fonction int + int est utilisée, qui renvoie un int.

Ce qui est très long pour dire qu'il est enfoui très profondément dans la spécification C #.

CLI

La CLI fonctionne uniquement sur 6 types (int32, int natif, int64, F, O et &). (ECMA-335 partition 3 section 1.5)

L'octet (int8) ne fait pas partie de ces types et est automatiquement contraint à un int32 avant l'ajout. (ECMA-335 partition 3 section 1.6)

Alun Harford
la source
Le fait que l'ECMA spécifie uniquement ces opérations particulières n'empêcherait pas une langue de mettre en œuvre d'autres règles. VB.NET autorisera utilement byte3 = byte1 And byte2sans transtypage, mais lèvera inutilement une exception d'exécution si int1 = byte1 + byte2renvoie une valeur supérieure à 255. Je ne sais pas si des langages autoriseraient byte3 = byte1+byte2et lèveraient une exception lorsque cela dépasse 255, mais ne lèveraient pas d'exception si les int1 = byte1+byte2rendements une valeur comprise entre 256 et 510.
supercat
26

Les réponses indiquant une inefficacité en ajoutant des octets et en tronquant le résultat en un octet sont incorrectes. Les processeurs x86 ont des instructions spécialement conçues pour un fonctionnement entier sur des quantités de 8 bits.

En fait, pour les processeurs x86 / 64, l'exécution d'opérations 32 bits ou 16 bits est moins efficace que les opérations 64 bits ou 8 bits en raison de l'octet de préfixe d'opérande qui doit être décodé. Sur les machines 32 bits, l'exécution d'opérations 16 bits entraîne la même pénalité, mais il existe toujours des opcodes dédiés pour les opérations 8 bits.

De nombreuses architectures RISC ont des instructions natives efficaces par mot / octet. Ceux qui n'ont généralement pas de valeur de stockage et de conversion en valeur signée d'une longueur de bit.

En d'autres termes, cette décision doit avoir été basée sur la perception de la destination du type d'octet, et non en raison de l'inefficacité sous-jacente du matériel.

Christophe
la source
+1; si seulement cette perception n'était pas erronée à chaque fois que je changeais et que j'avais deux octets en C # ...
Roman Starkov
Il ne devrait pas y avoir de coût de performance pour tronquer le résultat. Dans l'assemblage x86, c'est juste la différence entre copier un octet du registre ou quatre octets du registre.
Jonathan Allen
1
@JonathanAllen Exactement. Ironiquement, la seule différence est lors de l'exécution d'une conversion d' élargissement . La conception actuelle entraîne une pénalité de performance pour exécuter l'instruction d'élargissement (extension signée ou extension non signée)
reirab
" perception de la fonction du type d'octet " - Cela peut expliquer ce comportement pour byte(et char), mais pas pour shortlequel sémantiquement est clairement un nombre.
smls
13

Je me souviens d'avoir lu quelque chose de Jon Skeet (je ne le trouve pas maintenant, je continuerai à chercher) sur la façon dont l'octet ne surcharge pas réellement l'opérateur +. En fait, lors de l'ajout de deux octets comme dans votre exemple, chaque octet est en fait implicitement converti en entier. Le résultat est évidemment un int. Maintenant, pourQUOI cela a été conçu de cette façon, j'attendrai que Jon Skeet lui-même poste :)

EDIT: Je l'ai trouvé! Excellentes informations sur ce même sujet ici .

BFree
la source
9

C'est à cause du débordement et des portages.

Si vous ajoutez deux nombres à 8 bits, ils peuvent déborder dans le 9ème bit.

Exemple:

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

Je ne sais pas avec certitude, mais je suppose que ints, longset doubleson leur donne plus d'espace car ils sont assez grands comme ça. En outre, ce sont des multiples de 4, qui sont plus efficaces à gérer pour les ordinateurs, car la largeur du bus de données interne est de 4 octets ou 32 bits (64 bits devient de plus en plus répandu maintenant). Octet et court sont un peu plus inefficaces, mais ils peuvent économiser de l'espace.

samoz
la source
23
Mais les types de données plus volumineux ne suivent pas le même comportement.
Inisheer
12
Les problèmes de débordement sont un aparté. Si vous deviez prendre votre logique et l'appliquer au langage, tous les types de données renverraient un type de données plus grand après l'arithmétique d'addition, ce qui n'est certainement PAS le cas. int + int = int, long + long = long. Je pense que la question concerne l'incohérence.
Joseph
C'était ma première pensée, mais alors pourquoi int + int = long? Donc je n'achète pas l'argument "débordement possible" ... encore <grin>.
Robert Cartaino
11
Oh, et à propos de l'argument "débordement possible", pourquoi pas octet + octet = court?
Robert Cartaino
A) Pourquoi ça marche comme ça vu les règles de C #? Voir ma réponse ci-dessous. B) Pourquoi a-t-il été conçu tel quel? Probablement des considérations de convivialité, basées sur des jugements subjectifs sur la façon dont la plupart des gens ont tendance à utiliser les octets et les octets.
mqp
5

A partir de la spécification de langage C # 1.6.7.5 7.2.6.2 Promotions numériques binaires, il convertit les deux opérandes en int s'il ne peut pas les intégrer dans plusieurs autres catégories. Je suppose qu'ils n'ont pas surchargé l'opérateur + pour prendre l'octet comme paramètre, mais veulent qu'il agisse quelque peu normalement, donc ils utilisent simplement le type de données int.

Spécification du langage C #

Ryan
la source
4

Je soupçonne que C # appelle en fait le operator+défini sur int(qui renvoie un intsauf si vous êtes dans un checkedbloc), et transforme implicitement vos deux bytes/ shortsen ints. C'est pourquoi le comportement semble incohérent.

mqp
la source
3
Il pousse les deux octets sur la pile, puis il appelle la commande "add". En IL, ajoutez "mange" les deux valeurs et remplacez-les par un int.
Jonathan Allen
3

C'était probablement une décision pratique de la part des concepteurs de langage. Après tout, un int est un Int32, un entier signé 32 bits. Chaque fois que vous effectuez une opération entière sur un type plus petit que int, il sera de toute façon converti en 32 bits signé int par la plupart des CPU 32 bits. Cela, combiné à la probabilité de débordement de petits entiers, a probablement scellé l'affaire. Cela vous évite la corvée de vérifier en permanence le débordement / sous-débit, et lorsque le résultat final d'une expression sur les octets serait à portée, malgré le fait qu'à un stade intermédiaire, il serait hors de portée, vous obtenez un correct résultat.

Une autre pensée: le débordement / sous-débit sur ces types devrait être simulé, car il ne se produirait pas naturellement sur les CPU cibles les plus probables. Pourquoi s'embêter?

PeterAllenWebb
la source
2

C'est en grande partie ma réponse qui se rapporte à ce sujet, soumise d'abord à une question similaire ici .

Toutes les opérations dont les nombres entiers sont inférieurs à Int32 sont arrondies à 32 bits avant le calcul par défaut. La raison pour laquelle le résultat est Int32 est simplement de le laisser tel quel après le calcul. Si vous vérifiez les opcodes arithmétiques MSIL, le seul type numérique intégral avec lequel ils fonctionnent est Int32 et Int64. C'est "par conception".

Si vous souhaitez que le résultat revienne au format Int16, cela n'a pas d'importance si vous effectuez le transtypage en code, ou si le compilateur (hypotétiquement) émet la conversion "sous le capot".

Par exemple, pour faire de l'arithmétique Int16:

short a = 2, b = 3;

short c = (short) (a + b);

Les deux nombres s'élargiraient à 32 bits, seraient ajoutés, puis tronqués à 16 bits, c'est ainsi que MS le voulait.

L'avantage de l'utilisation courte (ou octet) est principalement le stockage dans les cas où vous avez des quantités massives de données (données graphiques, streaming, etc.)

Kenan EK
la source
1

L'addition n'est pas définie pour les octets. Ils sont donc convertis en int pour l'ajout. Cela est vrai pour la plupart des opérations et octets mathématiques. (notez que c'est comme ça dans les langues plus anciennes, je suppose que cela reste vrai aujourd'hui).

Jim C
la source
0

Je pense que c'est une décision de conception sur laquelle l'opération était la plus courante ... Si octet + octet = octet, peut-être que beaucoup plus de gens seront gênés par le transtypage en int lorsqu'un int est requis.

fortran
la source
2
Pour une fois, je suis gêné dans l'autre sens :) J'ai toujours l'air d'avoir besoin du résultat en octets, donc je dois toujours lancer.
Roman Starkov
Sauf que vous n'avez pas besoin de transtyper en int. Le casting est implicite. Seul l'autre sens est explicite.
Niki
1
@nikie Je pense que vous n'avez pas compris ma réponse. Si l'ajout de deux octets produirait un octet, afin d'éviter les débordements, quelqu'un devrait transtyper les opérandes (et non le résultat) en entier avant l'ajout.
fortran
0

À partir du code .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;
}

Simplifiez avec .NET 3.5 et supérieur:

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

maintenant vous pouvez faire:

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

serhio
la source
0

J'ai testé les performances entre octet et int.
Avec des valeurs 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);
    }
}

Avec des valeurs d'octets:

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

Voici le résultat:
octet: 3,57s 157mo, 3,71s 171mo, 3,74s 168mo avec CPU ~ = 30%
int: 4,05s 298mo, 3,92s 278mo, 4,28 294mo avec CPU ~ = 27%
Conclusion: les
octets utilisent plus le CPU mais il coûte moins de mémoire et c'est plus rapide (peut-être parce qu'il y a moins d'octets à allouer)

puipuix
la source
-1

En plus de tous les autres excellents commentaires, j'ai pensé que j'ajouterais une petite friandise. Beaucoup de commentaires se sont demandé pourquoi int, long et à peu près tout autre type numérique ne suit pas également cette règle ... retourne un type "plus gros" en réponse à l'arithmatique.

Beaucoup de réponses ont trait à la performance (enfin, 32 bits est plus rapide que 8 bits). En réalité, un nombre 8 bits est toujours un nombre 32 bits pour un processeur 32 bits .... même si vous ajoutez deux octets, le bloc de données sur lequel le processeur opère sera de 32 bits malgré tout ... donc l'ajout d'ints ne va pas être "plus rapide" que d'ajouter deux octets ... c'est tout de même la CPU. MAINTENANT, l'ajout de deux pouces sera plus rapide que l'ajout de deux longs sur un processeur 32 bits, car l'ajout de deux longs nécessite plus de microops car vous travaillez avec des nombres plus larges que le mot du processeur.

Je pense que la raison fondamentale pour laquelle l'arithmétique des octets se traduit par des nombres entiers est assez claire et simple: 8 bits ne va tout simplement pas très loin! : D Avec 8 bits, vous avez une plage non signée de 0 à 255. Ce n'est pas beaucoup de place pour travailler avec ... la probabilité que vous allez rencontrer des limitations d'octets est TRÈS élevée lorsque vous les utilisez en arithmétique. Cependant, la chance que vous allez manquer de bits lorsque vous travaillez avec des pouces, ou des longs, ou des doubles, etc. est considérablement plus faible ... suffisamment faible pour que nous rencontrions très rarement le besoin de plus.

La conversion automatique d'octet en entier est logique car l'échelle d'un octet est si petite. La conversion automatique de int en long, float en double, etc. n'est pas logique car ces nombres ont une échelle significative.

jrista
la source
Cela n'explique toujours pas pourquoi les byte - byteretours int, ou pourquoi ils ne font pas de casting pour short...
KthProg
Pourquoi voudriez-vous que l'addition renvoie un type différent de la soustraction? Si byte + byterenvoie int, car 255 + tout ce qui est supérieur à ce qu'un octet peut contenir, cela n'a pas de sens qu'un octet moins un autre octet renvoie autre chose qu'un entier du point de vue de la cohérence du type de retour.
jrista
Je ne le ferais pas, cela montre simplement que la raison ci-dessus n'est probablement pas correcte. Si cela avait à voir avec "l'ajustement" dans le résultat, alors la bytesoustraction retournerait a byte, et l'addition d'octets retournerait a short( byte+ bytes'inscrira toujours dans a short). S'il s'agissait de cohérence comme vous le dites, shortcela suffirait toujours pour les deux opérations plutôt que int. De toute évidence, il y a un mélange de raisons, pas nécessairement toutes bien pensées. Ou, la raison des performances donnée ci-dessous peut être plus précise.
KthProg