Подтвердить что ты не робот

Почему компилятор оценивает остаток MinValue% -1, отличный от времени выполнения?

Я думаю, что это похоже на ошибку в компиляторе С#.

Рассмотрим этот код (внутри метода):

const long dividend = long.MinValue;
const long divisor = -1L;
Console.WriteLine(dividend % divisor);

Он компилируется без ошибок (или предупреждений). Похоже на ошибку. При запуске печатает 0 на консоли.

Тогда без const код:

long dividend = long.MinValue;
long divisor = -1L;
Console.WriteLine(dividend % divisor);

Когда это выполняется, оно корректно приводит к тому, что OverflowException выбрасывается.

Спецификация языка С# упоминает этот случай специально и говорит, что должен быть выброшен System.OverflowException. Он не зависит от контекста checked или unchecked (похоже, ошибка с константами постоянной времени компиляции оператору остатка совпадает с checked и unchecked).

Такая ошибка возникает с int (System.Int32), а не только long (System.Int64).

Для сравнения, компилятор обрабатывает dividend / divisor с операндами const намного лучше, чем dividend % divisor.

Мои вопросы:

Я прав, это ошибка? Если да, это известная ошибка, которую они не хотят исправлять (из-за обратной совместимости, даже если глупо использовать % -1 с постоянной времени компиляции -1)? Или мы должны сообщать об этом, чтобы они могли исправить его в следующих версиях компилятора С#?

4b9b3361

Ответ 1

Этот угловой регистр очень конкретно рассматривается в компиляторе. Наиболее релевантные комментарии и код в Roslyn source:

// Although remainder and division always overflow at runtime with arguments int.MinValue/long.MinValue and -1     
// (regardless of checked context) the constant folding behavior is different.     
// Remainder never overflows at compile time while division does.    
newValue = FoldNeverOverflowBinaryOperators(kind, valueLeft, valueRight);

и

// MinValue % -1 always overflows at runtime but never at compile time    
case BinaryOperatorKind.IntRemainder:
    return (valueRight.Int32Value != -1) ? valueLeft.Int32Value % valueRight.Int32Value : 0;
case BinaryOperatorKind.LongRemainder:
    return (valueRight.Int64Value != -1) ? valueLeft.Int64Value % valueRight.Int64Value : 0;

Также поведение устаревшей версии компилятора С++, возвращаясь к версии 1. Из дистрибутива SSCLI v1.0, исходный файл clr/src/csharp/sccomp/fncbind.cpp:

case EK_MOD:
    // if we don't check this, then 0x80000000 % -1 will cause an exception...
    if (d2 == -1) {
        result = 0;
    } else {
        result = d1 % d2;
    }
    break;

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

Ответ 2

Я думаю, что это не ошибка; это скорее, как компилятор С# вычисляет % (это предположение). Кажется, что компилятор С# сначала вычисляет % для положительных чисел, затем применяет знак. Имея Abs(long.MinValue + 1) == Abs(long.MaxValue), если мы напишем:

static long dividend = long.MinValue + 1;
static long divisor = -1L;
Console.WriteLine(dividend % divisor);

Теперь мы увидим 0 как правильный ответ, потому что теперь Abs(dividend) == Abs(long.MaxValue), находящийся в диапазоне.

Почему это работает, когда мы объявляем его как значение const? (Опять угадай) Кажется, что компилятор С# фактически вычисляет выражение во время компиляции и не считает тип константы и действует на него как BigInteger или что-то (ошибка?). Потому что, если мы объявим такую ​​функцию, как:

static long Compute(long l1, long l2)
{
    return l1 % l2;
}

И вызовите Console.WriteLine(Compute(dividend, divisor));, мы получим одно и то же исключение. И снова, если мы объявим константу следующим образом:

const long dividend = long.MinValue + 1;

Мы не получили бы исключения.