byte + byte = int... почему?

Глядя на этот код C#:

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

Результат любой математики, выполненной на byte (или же short) типов неявно приводится к целому числу. Решением является явное приведение результата обратно к байту:

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

Что мне интересно, почему? Это архитектурно? Философская?

У нас есть:

  • int + int знак равно int
  • long + long знак равно long
  • float + float знак равно float
  • double + double знак равно double

Так почему не:

  • byte + byte знак равно byte
  • short + short знак равно short?

Немного предыстории: я выполняю длинный список вычислений для "малых чисел" (то есть < 8) и сохраняю промежуточные результаты в большом массиве. Использование байтового массива (вместо массива int) происходит быстрее (из-за попаданий в кэш). Но обширные броски байтов, распространяемые по коду, делают его намного более нечитаемым.

16 ответов

Третья строка вашего кода:

byte z = x + y;

на самом деле означает

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

Таким образом, в байтах нет операции +, байты сначала приводятся к целым числам, а результатом сложения двух целых чисел является (32-разрядное) целое число.

С точки зрения "почему это вообще происходит", это потому, что в C# нет никаких операторов, определенных для арифметики с байтами, sbyte, short или ushort, как уже говорили другие. Этот ответ о том, почему эти операторы не определены.

Я считаю, что это в основном ради производительности. Процессоры имеют собственные операции для выполнения арифметики с 32 битами очень быстро. Можно автоматически выполнить преобразование обратно из результата в байт, но это приведет к снижению производительности в том случае, если вы на самом деле не хотите такого поведения.

Я думаю, что это упоминается в одном из аннотированных стандартов C#. Ищу...

РЕДАКТИРОВАТЬ: досадно, я сейчас просмотрел аннотированную спецификацию ECMA C# 2, аннотированную спецификацию MS C# 3 и аннотацию CLI, и ни один из них не упомянул это, насколько я вижу. Я уверен, что видел причину, приведенную выше, но я ошарашен, если знаю, где. Извиняюсь, отзыв фанатов:(

Я думал, что видел это где-то раньше. Из этой статьи, The Old New Thing:

Предположим, мы жили в фантастическом мире, где операции с байтом приводили к байту.

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

В этом фэнтезийном мире ценность меня будет 16! Зачем? Поскольку оба операнда оператора + являются байтами, сумма "b+c" вычисляется как байт, что приводит к 16 из-за целочисленного переполнения. (И, как я отмечал ранее, целочисленное переполнение является новым вектором атаки безопасности.)

РЕДАКТИРОВАТЬ: Раймонд, по сути, защищает подход C и C++ изначально. В комментариях он защищает тот факт, что C# использует тот же подход на основе обратной совместимости языка.

C#

ECMA-334 утверждает, что сложение определяется только как допустимое для int+int, uint+uint, long+long и ulong+ulong (ECMA-334 14.7.4). Как таковые, это возможные операции, которые необходимо учитывать в отношении 14.4.2. Поскольку существуют неявные приведения от байта к int, uint, long и ulong, все члены функции сложения являются применимыми членами функции согласно 14.4.2.1. Мы должны найти лучшее неявное приведение по правилам в 14.4.2.3:

Приведение (C1) к int (T1) лучше, чем приведение (C2) к uint (T2) или ulong (T2), потому что:

  • Если T1 - int, а T2 - uint или ulong, C1 - лучшее преобразование.

Преобразование (C1) в int (T1) лучше, чем приведение (C2) к long (T2), потому что существует неявное приведение от int к long:

  • Если неявное преобразование из T1 в T2 существует, и неявное преобразование из T2 в T1 не существует, C1 - лучшее преобразование.

Следовательно, используется функция int + int, которая возвращает int.

Это очень долгий путь, чтобы сказать, что он очень глубоко скрыт в спецификации C#.

CLI

CLI работает только на 6 типах (int32, native int, int64, F, O и &). (ECMA-335, раздел 3, раздел 1.5)

Байт (int8) не является одним из этих типов и автоматически передается в int32 перед добавлением. (ECMA-335, раздел 3, раздел 1.6)

Ответы, указывающие на некоторую неэффективность добавления байтов и усечения результата обратно в байт, неверны. Процессоры x86 имеют инструкции, специально предназначенные для целочисленной работы в 8-битных количествах.

Фактически, для процессоров x86/64 выполнение 32-битных или 16-битных операций менее эффективно, чем 64-битных или 8-битных операций из-за байта префикса операнда, который должен быть декодирован. На 32-разрядных компьютерах выполнение 16-разрядных операций влечет за собой то же наказание, но для 8-разрядных операций все еще существуют специальные коды операций.

Многие архитектуры RISC имеют схожие родные эффективные инструкции. Те, которые, как правило, не имеют длины "хранить и преобразовать в значение со знаком в некотором бите".

Другими словами, это решение должно было основываться на восприятии типа байта, а не из-за неэффективности аппаратного обеспечения.

Я помню, как однажды читал что-то от Джона Скита (не могу найти его сейчас, я буду продолжать искать) о том, что байт на самом деле не перегружает оператор +. Фактически, при добавлении двух байтов, как в вашем примере, каждый байт фактически неявно преобразуется в int. Результатом этого, очевидно, является int. Теперь о том, ПОЧЕМУ это было разработано таким образом, я буду ждать, пока сам Джон Скит отправит сообщение:)

РЕДАКТИРОВАТЬ: нашел это! Отличная информация об этой самой теме здесь.

Это из-за переполнения и переноски.

Если вы добавите два 8-битных числа, они могут переполниться в 9-й бит.

Пример:

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

Я не знаю точно, но я предполагаю, что ints, longs, а такжеdoubles дают больше места, потому что они довольно большие, как есть. Кроме того, они кратны 4, что более эффективно для компьютеров, поскольку ширина внутренней шины данных составляет 4 байта или 32 бита (64 бита становятся все более распространенными в настоящее время). Байт и шорт немного более неэффективны, но они могут сэкономить место.

Из спецификации языка C# 1.6.7.5 7.2.6.2 Двоичные числовые продвижения он преобразует оба операнда в int, если не может вписать его в несколько других категорий. Я предполагаю, что они не перегружали оператор + для получения байта в качестве параметра, но хотели, чтобы он работал как-то нормально, поэтому они просто используют тип данных int.

Спецификация языка C#

Я подозреваю, что C# на самом деле вызывает operator+ определено на int (который возвращает int если вы не в checked блок), и неявно приведение обоих ваших bytes/shorts в ints, Вот почему поведение кажется противоречивым.

Это было, вероятно, практическое решение со стороны языковых дизайнеров. В конце концов, int - это Int32, 32-разрядное целое число со знаком. Всякий раз, когда вы выполняете целочисленную операцию над типом, меньшим, чем int, она все равно будет преобразована в 32-битное целое число со знаком большинства большинства 32-битных процессоров. Это, в сочетании с вероятностью переполнения маленьких целых чисел, вероятно, закрыло сделку. Это избавляет вас от рутинной проверки на предмет избыточного / недостаточного потока, и когда конечный результат выражения в байтах окажется в диапазоне, несмотря на тот факт, что на каком-то промежуточном этапе он будет вне диапазона, вы получите правильный результат.

Другая мысль: переполнение / переполнение этих типов должно быть смоделировано, поскольку оно не будет происходить естественным образом на наиболее вероятных целевых процессорах. Зачем беспокоиться?

Это по большей части мой ответ, относящийся к этой теме, который сначала был представлен на подобный вопрос здесь.

Все операции с целыми числами, меньшими, чем Int32, округляются до 32 бит до вычисления по умолчанию. Причина, по которой результатом является Int32, заключается в том, чтобы просто оставить его как есть после расчета. Если вы проверяете арифметические коды операций MSIL, то единственными целочисленными числовыми типами, с которыми они работают, являются Int32 и Int64. Это "по замыслу".

Если вы хотите получить результат обратно в формате Int16, это не имеет значения, если вы выполняете приведение в коде, или компилятор (гипотетически) выполняет преобразование "под капотом".

Например, чтобы сделать арифметику Int16:

short a = 2, b = 3;

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

Два числа расширились бы до 32 битов, были бы добавлены, затем урезаны до 16 битов, что и было задумано MS.

Преимущество использования коротких (или байтовых) данных - это, прежде всего, хранение в тех случаях, когда у вас большие объемы данных (графические данные, потоковая передача и т. Д.)

У меня есть тест производительности между байтом и int.
Со значениями 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);
    }
}

С байтовыми значениями:

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

Вот результат:
байт: 3,57 с 157 мес., 3,71 с 171 мес., 3,74 с 168 мес. с процессором ~ = 30%
int: 4,05 с 298 месяцев, 3,92 с 278 месяцев, 4,28 294 месяцев с процессором ~= 27%
Заключение:
байт использует больше ресурсов процессора, но это стоит меньше памяти и быстрее (возможно, из-за того, что выделяется меньше байтов)

Из кода.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;
}

Упростите с.NET 3.5 и выше:

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

Теперь вы можете сделать:

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

Добавление не определено для байтов. Поэтому они приводятся к int для дополнения. Это верно для большинства математических операций и байтов. (обратите внимание, что так было на старых языках, я полагаю, что сегодня это так).

Я думаю, что это проектное решение о том, какая операция была более распространенной... Если byte+byte = byte, возможно, гораздо больше людей будет обеспокоено необходимостью приводить к int, когда в качестве результата требуется int.

В дополнение ко всем другим замечательным комментариям, я подумал, что добавлю один маленький кусочек. Многие комментарии задавались вопросом, почему int, long и почти любой другой числовой тип также не следуют этому правилу... возвращают "больший" тип в ответ на арифметику.

Многие ответы были связаны с производительностью (ну, 32 бит быстрее, чем 8 бит). На самом деле, 8-битное число по-прежнему 32-битное число для 32-битного ЦП.... даже если вы добавите два байта, кусок данных, на котором работает процессор, будет 32-битным независимо, поэтому добавление целых не будет Быть "быстрее", чем добавлять два байта... это все равно для процессора. ТЕПЕРЬ, добавление двух целых будет БЫСТРЕЕ, чем добавление двух длинных на 32-битном процессоре, потому что добавление двух длинных требует большего количества микроопций, так как вы работаете с числами шире, чем у процессора

Я думаю, что основная причина побуждения байтовой арифметики к целым числам довольно ясна и прямолинейна: 8 бит просто не идут очень далеко!:D С 8 битами у вас есть диапазон без знака 0-255. Это не очень много места для работы... вероятность того, что вы столкнетесь с байтовыми ограничениями, ОЧЕНЬ высока при использовании их в арифметике. Однако вероятность того, что у вас закончатся биты при работе с целыми, длинными или двойными и т. Д., Значительно ниже... настолько мала, что мы очень редко сталкиваемся с необходимостью большего.

Автоматическое преобразование из байта в int логично, потому что масштаб байта очень мал. Автоматическое преобразование из int в long, float в double и т. Д. Не логично, поскольку эти числа имеют значительный масштаб.

Другие вопросы по тегам