C# поведение переполнения для непроверенной uint - PullRequest
10 голосов
/ 20 января 2020

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

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

Ответы [ 3 ]

1 голос
/ 20 января 2020

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

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

  1. умножение scale на scale , уступив a
  2. выполнить a + 7, уступив b
  3. приведение b к ulong, уступив c
  4. приведение c к uint, получая d

Первые две операции оставляют вас с плавающим значением b = 4.2949673E+09f. При стандартной арифметике с плавающей точкой c это 4294967296 ( вы можете проверить здесь ). Это вписывается в ulong просто отлично, так что c = 4294967296, но это ровно на единицу больше, чем uint.MaxValue, поэтому он возвращается к 0, следовательно, d = 0. Теперь, неожиданный сюрприз, так как арифметика с плавающей точкой c является фанки, 4.2949673E+09f и 4.2949673E+09f + 7 - это то же самое число в IEEE 754. Так что scale * scale даст вам то же значение float, что и scale * scale + 7, a = b, поэтому вторые операции в основном не используются.

Компилятор Roslyn выполняет (некоторые) операции const во время компиляции и оптимизирует все это выражение до 0. Опять же, - это правильный результат , и компилятору разрешено выполнять любые оптимизации, которые приведут к тому же поведению, что и код без них.

My догадываюсь , что используемый вами компилятор. NET 4.7.2 также пытается оптимизировать это, но имеет ошибку, из-за которой он оценивает приведение в неправильном месте. Естественно, если вы сначала приведете scale к uint, а затем выполните операцию, вы получите 7, потому что scale * scale обходит 0, а затем вы добавляете 7. Но , что не согласуется с результатом, который вы получите при пошаговой оценке выражений во время выполнения . Опять же, причина root - всего лишь предположение, если посмотреть на производимое поведение, но, учитывая все, что я изложил выше, я убежден, что это нарушение spe c на стороне первого компилятора.

ОБНОВЛЕНИЕ:

Я сделал глупость. этот бит спецификации C# , о котором я не знал, существовал при написании вышеуказанного ответа:

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

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

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

0 голосов
/ 20 января 2020

Суть в том, что (как вы можете видеть на документах ), значения 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 .

Если вы измените на double , у которого есть основание 2 ^ 53 , это будет работать для того, что вы хотите.

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

0 голосов
/ 20 января 2020

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

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

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

Если вы сразу же сделаете целочисленный тип с плавающей запятой (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» compilers ".

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

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

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

ОБНОВЛЕНИЕ

Согласно комментарию jal sh

7.0 - double, а не float, попробуйте 7.0f, it все равно дам вам 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

Ветвь компилятора Roslyn (сентябрь 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

Правильно начинается go при добавлении непостоянных выражений (по умолчанию 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

Ветка компилятора Roslyn (сентябрь 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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...