C # переполнение поведения для непроверенной uint

10

Я тестировал этот код на 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

Почему это происходит?

Lukas
источник
В ulongпоследнем случае приведение к игнорируется, поэтому оно происходит при преобразовании float-> int.
Madreflection
Меня больше удивляет изменение поведения, которое кажется довольно большой разницей. Я бы не ожидал, что «0» будет правильным ответом с этой цепочкой приведений.
Лукас
Понятный. Несколько вещей в спецификации были исправлены в компиляторе при сборке Roslyn, так что это может быть частью этого. Проверьте вывод JIT в этой версии на SharpLab. Это показывает, как приведение ulongвлияет на результат.
Madreflection
Удивительно, когда ваш пример возвращается к dotnetfiddle, последний WriteLine выводит 0 в Roslyn 3.4 и 7 на .NET Core 3.1
Лукас
Я также подтвердил на моем рабочем столе. Код JIT даже не выглядит близко, я получаю разные результаты между .NET Core и .NET Framework. Trippy
Лукас

Ответы:

1

Мои выводы были неверными. Смотрите обновление для более подробной информации.

Похоже, ошибка в первом компиляторе, который вы использовали. Ноль является правильным результатом в этом случае . Порядок операций, определяемый спецификацией C #, следующий:

  1. умножить scaleнаscale , получаяa
  2. выполнять a + 7 , уступаяb
  3. приведение bкulong, получаяc
  4. литой , 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 #, который я не знал, существовал при написании вышеуказанного ответа:

Операции с плавающей запятой могут выполняться с большей точностью, чем тип результата операции. Например, некоторые аппаратные архитектуры поддерживают «расширенный» или «длинный двойной» тип с плавающей точкой с большей дальностью и точностью, чем тип double, и неявно выполняют все операции с плавающей точкой, используя этот тип с более высокой точностью. Только при чрезмерных затратах на производительность такие аппаратные архитектуры могут быть выполнены для выполнения операций с плавающей запятой с меньшей точностью, и вместо того, чтобы требовать реализации для потери как производительности, так и точности, C # позволяет использовать тип с более высокой точностью для всех операций с плавающей запятой , Помимо предоставления более точных результатов, это редко дает ощутимые результаты. Однако в выражениях вида x * y / z

C # гарантирует операции, чтобы обеспечить уровень точности, по крайней мере, на уровне IEEE 754, но не обязательно именно так . Это не ошибка, это особенность функции. Компилятор Рослин в своем праве оценивать выражение точно так , как IEEE 754 указывает, а другой компилятор в своем праве сделать вывод , что 2^32 + 7это , 7когда введен в uint.

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

V0ldek
источник
Тогда, я думаю, у нас есть ошибка в текущем компиляторе .NET Framework (я просто попробовал в VS 2019, просто чтобы быть уверенным) :) Я думаю, я попытаюсь посмотреть, есть ли где-нибудь, чтобы записать ошибку, хотя исправление чего-то подобного могло бы вероятно, имеет много нежелательных побочных эффектов и, вероятно, будет проигнорировано ...
Лукас
Я не думаю, что он приводит к преждевременному приведению к int, что могло бы вызвать гораздо более ясные проблемы во многих случаях. Я предполагаю, что дело в том, что в операции const оно не оценивает значение и не приводит его до самого последнего значения, что означает в том, что вместо того, чтобы хранить промежуточные значения в числах с плавающей точкой, он просто пропускает это и заменяет его в каждом выражении на само выражение
jalsh
@jalsh Не думаю, что понимаю твои догадки. Если компилятор просто заменит каждый scaleзначением float, а затем оценит все остальное во время выполнения, результат будет таким же. Можете ли вы уточнить?
V0ldek
@ V0ldek, downvote был ошибкой, я отредактировал твой ответ, чтобы я мог удалить его :)
jalsh
Я предполагаю, что на самом деле он не хранит промежуточные значения в числах с плавающей запятой, он просто заменил f на выражение, которое вычисляет f без
преобразования
0

Дело в том, что (как вы можете видеть на документах ), значения с плавающей точкой могут иметь основание только до 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 , это будет работать для того, что вы хотите.

Это может быть проблема времени выполнения, но в этом случае это проблема времени компиляции, потому что все значения являются константами и будут оцениваться компилятором.

Пауло Моргадо
источник
-2

Прежде всего, вы используете непроверенный контекст, который является инструкцией для компилятора, вы, как разработчик, уверены, что результат не будет переполнен, и вы не хотите видеть ошибку компиляции. В вашем сценарии вы фактически намеренно переполняете тип и ожидаете согласованного поведения трех разных компиляторов, один из которых, вероятно, обратно совместим с историей, по сравнению с Roslyn и .NET Core, которые являются новыми.

Во-вторых, вы смешиваете неявные и явные преобразования. Я не уверен насчет компилятора Roslyn, но определенно компиляторы .NET Framework и .NET Core могут использовать разные оптимизации для этих операций.

Проблема здесь в том, что первая строка вашего кода использует только значения / типы с плавающей запятой, а вторая строка - это сочетание значений / типов с плавающей запятой и целочисленного значения / типа.

Если вы сразу сделаете целочисленный тип с плавающей точкой (7> 7,0), вы получите очень одинаковый результат для всех трех скомпилированных источников.

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))); // 859091763
        Console.WriteLine(unchecked((uint)(ulong)(scale * scale + 7.0))); // 7
    }
}

Таким образом, я бы сказал противоположно тому, что ответил V0ldek: «Ошибка (если это действительно ошибка) наиболее вероятна в компиляторах Roslyn и .NET Core».

Другая причина полагать, что результат первых непроверенных результатов вычислений одинаков для всех, и это значение выходит за пределы максимального значения UInt32типа.

Console.WriteLine(unchecked((uint)(ulong)(1.2 * scale * scale + 1.5 * scale) - UInt32.MaxValue - 1)); // 859091763

Минус один, так как мы начинаем с нуля, это значение, которое трудно вычесть самому. Если мое математическое понимание переполнения верно, мы начинаем со следующего числа после максимального значения.

ОБНОВИТЬ

Согласно джалшу

7.0 - это двойное число, а не число с плавающей точкой, попробуйте 7.0f, оно все равно даст вам 0

Его комментарий правильный. В случае, если мы используем 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.

Исходный код

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)(scale * scale + 7.0)));
    }
}

.NET Framework (x64) IL

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi beforefieldinit Program
    extends [mscorlib]System.Object
{
    // Fields
    .field private static literal float32 scale = float32(65536)

    // Methods
    .method public hidebysig static 
        void Main () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 17 (0x11)
        .maxstack 8

        IL_0000: ldc.i4 859091763
        IL_0005: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_000a: ldc.i4.7
        IL_000b: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_0010: ret
    } // end of method Program::Main

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2062
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: ret
    } // end of method Program::.ctor

} // end of class Program

Рослин компилятор филиал (сентябрь 2019) IL

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi beforefieldinit Program
    extends [System.Private.CoreLib]System.Object
{
    // Fields
    .field private static literal float32 scale = float32(65536)

    // Methods
    .method public hidebysig static 
        void Main () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 17 (0x11)
        .maxstack 8

        IL_0000: ldc.i4 859091763
        IL_0005: call void [System.Console]System.Console::WriteLine(uint32)
        IL_000a: ldc.i4.0
        IL_000b: call void [System.Console]System.Console::WriteLine(uint32)
        IL_0010: ret
    } // end of method Program::Main

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2062
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Private.CoreLib]System.Object::.ctor()
        IL_0006: ret
    } // end of method Program::.ctor

} // end of class Program

Он начинает работать правильно, когда вы добавляете неконстантные выражения (по умолчанию unchecked), как показано ниже.

using System;

public class Program
{
    static Random random = new Random();

    public static void Main()
    {
        var scale = 64 * random.Next(1024, 1025);       
        uint f = (uint)(ulong)(scale * scale + 7f);
        uint d = (uint)(ulong)(scale * scale + 7d);
        uint i = (uint)(ulong)(scale * scale + 7);

        Console.WriteLine((uint)(ulong)(1.2 * scale * scale + 1.5 * scale)); // 859091763
        Console.WriteLine((uint)(ulong)(scale * scale + 7f)); // 7
        Console.WriteLine(f); // 7
        Console.WriteLine((uint)(ulong)(scale * scale + 7d)); // 7
        Console.WriteLine(d); // 7
        Console.WriteLine((uint)(ulong)(scale * scale + 7)); // 7
        Console.WriteLine(i); // 7
    }
}

Который генерирует "точно" один и тот же IL обоими компиляторами.

.NET Framework (x64) IL

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi beforefieldinit Program
    extends [mscorlib]System.Object
{
    // Fields
    .field private static class [mscorlib]System.Random random

    // Methods
    .method public hidebysig static 
        void Main () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 164 (0xa4)
        .maxstack 4
        .locals init (
            [0] int32,
            [1] uint32,
            [2] uint32
        )

        IL_0000: ldc.i4.s 64
        IL_0002: ldsfld class [mscorlib]System.Random Program::random
        IL_0007: ldc.i4 1024
        IL_000c: ldc.i4 1025
        IL_0011: callvirt instance int32 [mscorlib]System.Random::Next(int32, int32)
        IL_0016: mul
        IL_0017: stloc.0
        IL_0018: ldloc.0
        IL_0019: ldloc.0
        IL_001a: mul
        IL_001b: conv.r4
        IL_001c: ldc.r4 7
        IL_0021: add
        IL_0022: conv.u8
        IL_0023: conv.u4
        IL_0024: ldloc.0
        IL_0025: ldloc.0
        IL_0026: mul
        IL_0027: conv.r8
        IL_0028: ldc.r8 7
        IL_0031: add
        IL_0032: conv.u8
        IL_0033: conv.u4
        IL_0034: stloc.1
        IL_0035: ldloc.0
        IL_0036: ldloc.0
        IL_0037: mul
        IL_0038: ldc.i4.7
        IL_0039: add
        IL_003a: conv.i8
        IL_003b: conv.u4
        IL_003c: stloc.2
        IL_003d: ldc.r8 1.2
        IL_0046: ldloc.0
        IL_0047: conv.r8
        IL_0048: mul
        IL_0049: ldloc.0
        IL_004a: conv.r8
        IL_004b: mul
        IL_004c: ldc.r8 1.5
        IL_0055: ldloc.0
        IL_0056: conv.r8
        IL_0057: mul
        IL_0058: add
        IL_0059: conv.u8
        IL_005a: conv.u4
        IL_005b: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_0060: ldloc.0
        IL_0061: ldloc.0
        IL_0062: mul
        IL_0063: conv.r4
        IL_0064: ldc.r4 7
        IL_0069: add
        IL_006a: conv.u8
        IL_006b: conv.u4
        IL_006c: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_0071: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_0076: ldloc.0
        IL_0077: ldloc.0
        IL_0078: mul
        IL_0079: conv.r8
        IL_007a: ldc.r8 7
        IL_0083: add
        IL_0084: conv.u8
        IL_0085: conv.u4
        IL_0086: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_008b: ldloc.1
        IL_008c: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_0091: ldloc.0
        IL_0092: ldloc.0
        IL_0093: mul
        IL_0094: ldc.i4.7
        IL_0095: add
        IL_0096: conv.i8
        IL_0097: conv.u4
        IL_0098: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_009d: ldloc.2
        IL_009e: call void [mscorlib]System.Console::WriteLine(uint32)
        IL_00a3: ret
    } // end of method Program::Main

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2100
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: ret
    } // end of method Program::.ctor

    .method private hidebysig specialname rtspecialname static 
        void .cctor () cil managed 
    {
        // Method begins at RVA 0x2108
        // Code size 11 (0xb)
        .maxstack 8

        IL_0000: newobj instance void [mscorlib]System.Random::.ctor()
        IL_0005: stsfld class [mscorlib]System.Random Program::random
        IL_000a: ret
    } // end of method Program::.cctor

} // end of class Program

Рослин компилятор филиал (сентябрь 2019) IL

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi beforefieldinit Program
    extends [System.Private.CoreLib]System.Object
{
    // Fields
    .field private static class [System.Private.CoreLib]System.Random random

    // Methods
    .method public hidebysig static 
        void Main () cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 164 (0xa4)
        .maxstack 4
        .locals init (
            [0] int32,
            [1] uint32,
            [2] uint32
        )

        IL_0000: ldc.i4.s 64
        IL_0002: ldsfld class [System.Private.CoreLib]System.Random Program::random
        IL_0007: ldc.i4 1024
        IL_000c: ldc.i4 1025
        IL_0011: callvirt instance int32 [System.Private.CoreLib]System.Random::Next(int32, int32)
        IL_0016: mul
        IL_0017: stloc.0
        IL_0018: ldloc.0
        IL_0019: ldloc.0
        IL_001a: mul
        IL_001b: conv.r4
        IL_001c: ldc.r4 7
        IL_0021: add
        IL_0022: conv.u8
        IL_0023: conv.u4
        IL_0024: ldloc.0
        IL_0025: ldloc.0
        IL_0026: mul
        IL_0027: conv.r8
        IL_0028: ldc.r8 7
        IL_0031: add
        IL_0032: conv.u8
        IL_0033: conv.u4
        IL_0034: stloc.1
        IL_0035: ldloc.0
        IL_0036: ldloc.0
        IL_0037: mul
        IL_0038: ldc.i4.7
        IL_0039: add
        IL_003a: conv.i8
        IL_003b: conv.u4
        IL_003c: stloc.2
        IL_003d: ldc.r8 1.2
        IL_0046: ldloc.0
        IL_0047: conv.r8
        IL_0048: mul
        IL_0049: ldloc.0
        IL_004a: conv.r8
        IL_004b: mul
        IL_004c: ldc.r8 1.5
        IL_0055: ldloc.0
        IL_0056: conv.r8
        IL_0057: mul
        IL_0058: add
        IL_0059: conv.u8
        IL_005a: conv.u4
        IL_005b: call void [System.Console]System.Console::WriteLine(uint32)
        IL_0060: ldloc.0
        IL_0061: ldloc.0
        IL_0062: mul
        IL_0063: conv.r4
        IL_0064: ldc.r4 7
        IL_0069: add
        IL_006a: conv.u8
        IL_006b: conv.u4
        IL_006c: call void [System.Console]System.Console::WriteLine(uint32)
        IL_0071: call void [System.Console]System.Console::WriteLine(uint32)
        IL_0076: ldloc.0
        IL_0077: ldloc.0
        IL_0078: mul
        IL_0079: conv.r8
        IL_007a: ldc.r8 7
        IL_0083: add
        IL_0084: conv.u8
        IL_0085: conv.u4
        IL_0086: call void [System.Console]System.Console::WriteLine(uint32)
        IL_008b: ldloc.1
        IL_008c: call void [System.Console]System.Console::WriteLine(uint32)
        IL_0091: ldloc.0
        IL_0092: ldloc.0
        IL_0093: mul
        IL_0094: ldc.i4.7
        IL_0095: add
        IL_0096: conv.i8
        IL_0097: conv.u4
        IL_0098: call void [System.Console]System.Console::WriteLine(uint32)
        IL_009d: ldloc.2
        IL_009e: call void [System.Console]System.Console::WriteLine(uint32)
        IL_00a3: ret
    } // end of method Program::Main

    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed 
    {
        // Method begins at RVA 0x2100
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: call instance void [System.Private.CoreLib]System.Object::.ctor()
        IL_0006: ret
    } // end of method Program::.ctor

    .method private hidebysig specialname rtspecialname static 
        void .cctor () cil managed 
    {
        // Method begins at RVA 0x2108
        // Code size 11 (0xb)
        .maxstack 8

        IL_0000: newobj instance void [System.Private.CoreLib]System.Random::.ctor()
        IL_0005: stsfld class [System.Private.CoreLib]System.Random Program::random
        IL_000a: ret
    } // end of method Program::.cctor

} // end of class Program

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

dropoutcoder
источник
7.0 - это двойное число, а не число с плавающей точкой, попробуйте 7.0f, оно все равно даст вам 0
jalsh
Да, это должен быть тип с плавающей точкой, а не с плавающей точкой. Спасибо за исправление.
выпадающий код
Это меняет всю перспективу проблемы, когда имея дело с удвоением, точность, которую вы получаете, значительно выше, а результат, объясненный в ответе V0ldek, резко меняется, вы можете просто изменить масштаб на двойную и проверить еще раз, результаты будут такими же. ..
Джалш
В конце концов, это более сложный вопрос.
выпадающий код
1
@jalsh Да, но есть флаг компилятора, который поворачивает проверенный контекст везде. Возможно, вы захотите проверить все на безопасность, кроме определенного горячего пути, который требует всех циклов ЦП, которые он может получить.
V0ldek