Я тестировал этот код на https://dotnetfiddle.net/ :
using System;
public class Program
{
const float scale = 64 * 1024;
public static void Main()
{
Console.WriteLine(unchecked((uint)(ulong)(1.2 * scale * scale + 1.5 * scale)));
Console.WriteLine(unchecked((uint)(ulong)(scale* scale + 7)));
}
}
Если я скомпилирую с .NET 4.7.2 я получу
859091763
7
Но если я делаю Roslyn или .NET Core, я получаю
859091763
0
Почему это происходит?
ulong
последнем случае приведение к игнорируется, поэтому оно происходит при преобразованииfloat
->int
.ulong
влияет на результат.Ответы:
Мои выводы были неверными. Смотрите обновление для более подробной информации.
Похоже, ошибка в первом компиляторе, который вы использовали. Ноль является правильным результатом в этом случае .Порядок операций, определяемый спецификацией C #, следующий:scale
наscale
, получаяa
a + 7
, уступаяb
b
кulong
, получаяc
c
чтобыuint
, получаяd
Первые две операции оставляют вас с плавающим значением
b = 4.2949673E+09f
. В стандартной арифметике с плавающей точкой это так4294967296
( вы можете проверить это здесь ). Это хорошо вписываетсяulong
, так чтоc = 4294967296
, но это ровно на один большеuint.MaxValue
, так что это0
, следовательно, круговоротd = 0
. Теперь сюрприз сюрприз, так как арифметика с плавающей точкой является фанком,4.2949673E+09f
и4.2949673E+09f + 7
точно такой же номер в IEEE 754. Таким образом ,scale * scale
даст вам то же значение аfloat
какscale * scale + 7
,a = b
так вторая операция не является в основном не оп.Компилятор Roslyn выполняет (некоторые) константные операции во время компиляции и оптимизирует все это выражение до
0
.Опять же, это правильный результат , икомпилятору разрешено выполнять любые оптимизации, которые приведут к тому же поведению, что и код без них.Я предполагаю , что компилятор .NET 4.7.2, который вы использовали, также пытается оптимизировать это, но имеет ошибку, которая заставляет его оценивать приведение в неправильном месте. Естественно, если вы сначала приведетеscale
к,uint
а затем выполните операцию, вы получите7
, потому чтоscale * scale
циклические переходы,0
а затем вы добавляете7
. Но это не согласуется с результатом, который вы получите при пошаговой оценке выражений во время выполнения . Опять же, коренная причина - это всего лишь предположение, если посмотреть на производимое поведение, но, учитывая все, что я изложил выше, я убежден, что это нарушение спецификации на стороне первого компилятора.ОБНОВИТЬ:
Я сделал глупость. Вот этот бит спецификации C #, который я не знал, существовал при написании вышеуказанного ответа:
C # гарантирует операции, чтобы обеспечить уровень точности, по крайней мере, на уровне IEEE 754, но не обязательно именно так . Это не ошибка, это особенность функции. Компилятор Рослин в своем праве оценивать выражение точно так , как IEEE 754 указывает, а другой компилятор в своем праве сделать вывод , что
2^32 + 7
это ,7
когда введен вuint
.Прошу прощения за мой вводящий в заблуждение первый ответ, но, по крайней мере, мы все кое-что узнали сегодня.
источник
scale
значением float, а затем оценит все остальное во время выполнения, результат будет таким же. Можете ли вы уточнить?Дело в том, что (как вы можете видеть на документах ), значения с плавающей точкой могут иметь основание только до 2 ^ 24 . Таким образом, когда вы присваиваете значение 2 ^ 32 ( 64 * 2014 * 164 * 1024 = 2 ^ 6 * 2 ^ 10 * 2 ^ 6 * 2 ^ 10 = 2 ^ 32 ), оно становится на самом деле 2 ^ 24 * 2 ^ 8 , что составляет 4294967000 . Добавление 7 будет только добавлением к части, усеченной преобразованием в ulong .
Если вы измените на удвоение , у которого есть основание 2 ^ 53 , это будет работать для того, что вы хотите.
Это может быть проблема времени выполнения, но в этом случае это проблема времени компиляции, потому что все значения являются константами и будут оцениваться компилятором.
источник
Прежде всего, вы используете непроверенный контекст, который является инструкцией для компилятора, вы, как разработчик, уверены, что результат не будет переполнен, и вы не хотите видеть ошибку компиляции. В вашем сценарии вы фактически намеренно переполняете тип и ожидаете согласованного поведения трех разных компиляторов, один из которых, вероятно, обратно совместим с историей, по сравнению с Roslyn и .NET Core, которые являются новыми.
Во-вторых, вы смешиваете неявные и явные преобразования. Я не уверен насчет компилятора Roslyn, но определенно компиляторы .NET Framework и .NET Core могут использовать разные оптимизации для этих операций.
Проблема здесь в том, что первая строка вашего кода использует только значения / типы с плавающей запятой, а вторая строка - это сочетание значений / типов с плавающей запятой и целочисленного значения / типа.
Если вы сразу сделаете целочисленный тип с плавающей точкой (7> 7,0), вы получите очень одинаковый результат для всех трех скомпилированных источников.
Таким образом, я бы сказал противоположно тому, что ответил V0ldek: «Ошибка (если это действительно ошибка) наиболее вероятна в компиляторах Roslyn и .NET Core».
Другая причина полагать, что результат первых непроверенных результатов вычислений одинаков для всех, и это значение выходит за пределы максимального значения
UInt32
типа.Минус один, так как мы начинаем с нуля, это значение, которое трудно вычесть самому. Если мое математическое понимание переполнения верно, мы начинаем со следующего числа после максимального значения.
ОБНОВИТЬ
Согласно джалшу
Его комментарий правильный. В случае, если мы используем float, вы все равно получаете 0 для Roslyn и .NET Core, но с другой стороны, используя двойные результаты в 7.
Я сделал несколько дополнительных тестов, и все стало еще более странным, но в конце все имеет смысл (по крайней мере, немного).
Я предполагаю, что компилятор .NET Framework 4.7.2 (выпущенный в середине 2018 года) действительно использует другие оптимизации, чем компиляторы .NET Core 3.1 и Roslyn 3.4 (выпущенные в конце 2019 года). Эти различные оптимизации / вычисления используются исключительно для постоянных значений, известных во время компиляции. Вот почему было необходимо использовать
unchecked
ключевое слово, так как компилятор уже знает, что происходит переполнение, но для оптимизации конечного IL использовались разные вычисления.Тот же исходный код и почти тот же IL, за исключением инструкции IL_000a. Один компилятор вычисляет 7, а другой 0.
Исходный код
.NET Framework (x64) IL
Рослин компилятор филиал (сентябрь 2019) IL
Он начинает работать правильно, когда вы добавляете неконстантные выражения (по умолчанию
unchecked
), как показано ниже.Который генерирует "точно" один и тот же IL обоими компиляторами.
.NET Framework (x64) IL
Рослин компилятор филиал (сентябрь 2019) IL
Итак, в конце я считаю, что причиной разного поведения является просто другая версия фреймворка и / или компилятора, которые используют разные оптимизации / вычисления для константных выражений, но в других случаях поведение очень одинаково.
источник