Поведінка переповнення 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

Чому це відбувається?


В ulongостанньому випадку акторський склад ігнорується, тому він відбувається в конверсії float-> int.
madreflection

Мене більше дивує зміна поведінки, що здається досить великою різницею. Я б не очікував, що "0" буде достовірною відповіддю, як із тим ланцюжком закидів tbh.
Лукас

Зрозуміло. Кілька речей у специфікації були зафіксовані у компіляторі, коли вони будували Рослін, так що це могло бути частиною цього. Перевірте вихід 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 виконує (деякі) операції const під час компіляції та оптимізує весь цей вираз до 0. Знову ж таки, це правильний результат , і компілятору дозволено проводити будь-які оптимізації, які призводять до такої ж поведінки, що і код без них.

Я гадаю , що компілятор .NET 4.7.2, який ви використовували, також намагається оптимізувати цю проблему, але має помилку, яка змушує її оцінювати склад у неправильному місці. Природно, якщо ви спочатку кинете scaleна а, uintа потім виконаєте операцію, ви отримаєте 7, тому що scale * scaleтуди і назад 0ви додаєте 7. Але це суперечить результату, який ви отримали, оцінюючи вирази крок за кроком під час виконання . Знову ж таки, першопричиною є лише здогадка, коли дивляться на вироблену поведінку, але враховуючи все, що я сказала вище, я переконаний, що це специфічне порушення на стороні першого компілятора.

ОНОВЛЕННЯ:

Я зробив гуф. Є цей біт специфікації C #, про який я не знав, що існує при написанні вищевказаної відповіді:

Операції з плаваючою комою можуть виконуватися з більшою точністю, ніж тип результату операції. Наприклад, деякі апаратні архітектури підтримують тип "плаваючої точки" з розширеним або "довгим подвійним" з більшим діапазоном і точністю, ніж подвійний тип, і неявно виконують усі операції з плаваючою комою, використовуючи цей тип більш високої точності. Тільки за надмірних витрат на продуктивність такі архітектурні апаратури можуть бути зроблені для виконання операцій з плаваючою комою з меншою точністю, а замість того, щоб вимагати виконання, щоб втратити як продуктивність, так і точність, C # дозволяє використовувати тип високої точності для всіх операцій з плаваючою комою. . Крім того, що дає більш точні результати, це рідко має якісь вимірювані ефекти. Однак у виразах форми x * y / z,

C # гарантує операції для забезпечення рівня точності принаймні на рівні IEEE 754, але не обов'язково саме цього. Це не помилка, це особливість специфікації. Компілятор Roslyn має право оцінювати вираз точно так, як вказує IEEE 754, а інший компілятор має право на висновок, який 2^32 + 7є 7під час введення uint.

Вибачте за свою оманливу першу відповідь, але принаймні ми сьогодні все чомусь дізналися.


Тоді я думаю, у нас є помилка в поточному компіляторі .NET Framework (я просто спробував у VS 2019 просто переконатися) :) Я думаю, я спробую побачити, чи є десь ввійти помилку, хоча виправити щось подібне напевно, є багато небажаних побічних ефектів і, можливо, їх ігнорують ...
Лукаш

Я не думаю, що це передчасне відтворення до int, що могло б викликати набагато чіткіші проблеми у багатьох випадках, я думаю, справа в тому, що в операції const він не оцінює значення та передає його до останнього, тобто полягає в тому, що замість того, щоб зберігати проміжні значення в плавках, це просто пропускати це і замінювати його в кожному виразі самим виразом
яльш

@jalsh Я не думаю, що я розумію твою здогадку. Якби компілятор просто замінив кожне scaleзначенням float, а потім оцінив усе інше під час виконання, результат був би таким же. Чи можете ви докладно?
V0ldek

@ V0ldek, то downvote була помилка, я редагував свій відповідь , щоб я міг видалити його :)
jalsh

я здогадуюсь, що він насправді не зберігав проміжні значення у плавках, він просто замінив f на вираз, який оцінює f, не кидаючи його на плавати
jalsh

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 буде лише додаванням до частини, урізаної шляхом перетворення в улонг .

Якщо ви перейдете на подвійний , який має базу 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

Мінус один є, коли ми починаємо з нуля, це значення, яке важко відняти. Якщо моє математичне розуміння переповнення правильне, ми починаємо з наступного числа після максимального значення.

ОНОВЛЕННЯ

За коментарем jalash

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_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

Він починає правильним шляхом, коли ви додаєте непостійні вирази (за замовчуванням є 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
    }
}

Що генерує "точно" той самий ІЛ обох компіляторів.

.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

Отже, врешті-решт, я вважаю, що причиною різної поведінки є лише інша версія фреймворку та / або компілятора, які використовують різні оптимізації / обчислення для постійних виразів, але в інших випадках поведінка дуже однакова.


7.0 - це подвійний, а не поплавок, спробуйте 7.0f, він все одно дасть вам 0
яллів

Так, це має бути тип з плаваючою комою, а не плаваючий. Дякуємо за виправлення
dropoutcoder

Це змінює всю перспективу питання, якщо мати справу з подвійною точністю, яку ви отримуєте, набагато вище, а результат, пояснений у відповіді В0лдека, різко змінюється, ви можете скоріше просто змінити шкалу, щоб подвоїтись і перевірити ще раз, результати були б однакові. ..
jalash

Зрештою, це складніше питання.
dropoutcoder

1
@jalsh Так, але є прапор компілятора, який скрізь перетворює перевірений контекст. Можливо, ви хочете, щоб усе перевірили на безпеку, за винятком певного гарячого шляху, який потребує всіх процесорних циклів, які він може отримати.
V0ldek
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.