Почему небезопасное чтение sbyte -> byte несовместимо в режиме Release: *(byte*)(&sbyteValue)?

При написании конвертации generic enum в int происходили странные вещи с небезопасным чтением типа sbyte в byte.

Следующие примеры были протестированы с .Net 6.0 на машине AMD x64.

Пример 1: отладка несоответствия и выпуск

Следующий код генерирует разные выходные данные в режиме отладки и в режиме выпуска:

      class Program
{
    static void Main()
    {
        byte byteValue = ReadAsByteValue(sbyteValue: -1);

        Console.WriteLine(byteValue);

        // OUTPUT DEBUG:   255
        // OUTPUT RELEASE: -1
    }

    static unsafe byte ReadAsByteValue(sbyte sbyteValue)
    {
        return *(byte*)(&sbyteValue);
    }
}

Поскольку тип не имеет значения -1, я предполагаю, что в режиме Release компилятор возвращаетsbyteвместоbyte.

Пример 2A: Несоответствие в режиме Release

      class Program
{
    static void Main()
    {
        var value1 = GetIntValueEncapsulated((sbyte)-1, true);
        var value2 = GetIntValue((sbyte)-1);

        Console.WriteLine($"{value1} vs. {value2}");

        foreach (var value in Array.Empty<sbyte>())
        {
            GetIntValueEncapsulated(value, true);
        }

        // OUTPUT RELEASE: -1 vs. 255
    }

    static int GetIntValueEncapsulated<T>(T value, bool trueFalse)
        where T : unmanaged
    {
        if (trueFalse)
        {
            return GetIntValue(value);
        }
        else
        {
            throw new NotImplementedException($"Not implemented for size: {Unsafe.SizeOf<T>()}");
        }
    }

    static unsafe int GetIntValue<T>(T value)
        where T : unmanaged
    {
        return *(byte*)(&value);
    }
}

Пример 2B: Комментирование пустого foreachменяет результаты

      var value1 = GetIntValueEncapsulated((sbyte)-1, true);
var value2 = GetIntValue((sbyte)-1);

Console.WriteLine($"{value1} vs. {value2}");

//foreach (var value in Array.Empty<sbyte>())
//{
//    GetIntValueEncapsulated(value, true);
//}

// OUTPUT RELEASE: -1 vs. -1

Пример 2C: Нефункциональное изменение в строке исключения изменяет результаты

Начиная с примера 2A и заменяя строку:

      throw new NotImplementedException($"Not implemented for size: {Unsafe.SizeOf<T>()}"); 

с линией:

      throw new NotImplementedException($"Not implemented for size: " + Unsafe.SizeOf<T>()); 

дает вывод:

      // OUTPUT RELEASE: 255 vs. 255

Вопросы

  • Какова точная причина этих различий?
  • Как заставить компилятор в режиме Release вести себя как положено? (т.е. как в режиме отладки)

1 ответ

Пример 1: отладка несоответствия и выпуск

  1. Вы должны знать, что метод перегрузки, выбранный компилятором в этом примере,WriteLine(int). Итак, если вы позвонитеWriteLine((uint)byteValue)илиWriteLine(byteValue.ToString()), вы получите результат255.

  2. Сегодня компилятор предпочитает 32-битные целые типы со знаком и будет кодироватьsbyteValue: -1не000000ffиз-за эффективности.

  3. Побочный эффект оптимизации в режиме выпуска.

      // Release
ldc.i4.m1
call uint8 C::ReadAsByteValue(int8)
call void [System.Console]System.Console::WriteLine(int32)

// Debug
ldc.i4.m1
call uint8 C::ReadAsByteValue(int8)
stloc.0
ldloc.0
call void [System.Console]System.Console::WriteLine(int32)

Вы можете видеть, что в режиме отладки он использует локальную переменную для передачи байта. Документыstlocговорит.

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

Поскольку в режиме выпуска нет посредника, нет усечения,WriteLineМетод будет использовать возвращаемое значение в регистре как есть. Эффект распространяется и наshort+ushortпо той же причине.


Пример 2A: Несоответствие в режиме Release

Согласно приведенному выше объяснению, значения, возвращенные изGetIntValueEncapsulatedилиGetIntValueв реестрах всегдаffffffff.

Извините, я не эксперт JIT, поэтому не могу рассказать подробности реализации. Я знаю, что это вызвано встраиванием метода. ПрименятьNoInliningметоду выход равен -1.

      [MethodImpl(MethodImplOptions.NoInlining)]
static unsafe int GetIntValue<T>(T value)

Следующий код можно использовать для имитации эффекта принудительного встраивания.

      sbyte a = -1;
var value2 = *(byte*)(&a);

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

      movzx       edi,byte ptr [rsp+4Ch]

Чтобы добиться ожидаемого результата

  1. Используйте детерминированный метод.
      Console.WriteLine((uint)byteValue); // Console.WriteLine(uint)
Console.WriteLine(byteValue.ToString()); // byte.ToString()
  1. Перевести вuint*первый
      static unsafe byte ReadAsByteValue(sbyte sbyteValue)
    => (byte)*(uint*)(&sbyteValue);
static unsafe int GetIntValue<T>(T value) where T : unmanaged
    => (byte)*(uint*)(&value);
Другие вопросы по тегам