Почему небезопасное чтение 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: отладка несоответствия и выпуск
Вы должны знать, что метод перегрузки, выбранный компилятором в этом примере,
WriteLine(int)
. Итак, если вы позвонитеWriteLine((uint)byteValue)
илиWriteLine(byteValue.ToString())
, вы получите результат255
.Сегодня компилятор предпочитает 32-битные целые типы со знаком и будет кодировать
sbyteValue: -1
не000000ff
из-за эффективности.Побочный эффект оптимизации в режиме выпуска.
// 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]
Чтобы добиться ожидаемого результата
- Используйте детерминированный метод.
Console.WriteLine((uint)byteValue); // Console.WriteLine(uint)
Console.WriteLine(byteValue.ToString()); // byte.ToString()
- Перевести в
uint*
первый
static unsafe byte ReadAsByteValue(sbyte sbyteValue)
=> (byte)*(uint*)(&sbyteValue);
static unsafe int GetIntValue<T>(T value) where T : unmanaged
=> (byte)*(uint*)(&value);