While writing conversion of generic enum to int strange things happened around unsafe read of sbyte type to byte.
The folloging examples were tested with .Net 6.0 on AMD x64 machine.
Example 1: Inconsistency Debug vs. Release
The following code generates different output in Debug and in Release mode:
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);
}
}
Since type byte does not have value -1, I suppose that in Release mode the compiler returns sbyte instead of byte.
Example 2A: Inconsistency in Release mode
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);
}
}
Example 2B: Commenting out empty foreach changes results
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
Example 2C: Non-functional change on the Exception line changes results
Starting with Example 2A and replacing line:
throw new NotImplementedException($"Not implemented for size: {Unsafe.SizeOf<T>()}");
with line:
throw new NotImplementedException($"Not implemented for size: " + Unsafe.SizeOf<T>());
gives output:
// OUTPUT RELEASE: 255 vs. 255
Questions
- What is the exact cause of these differences?
- How to force compiler in the Release mode to behave as expected? (i.e. as in the Debug mode)
Example 1: Inconsistency Debug vs. Release
You should know that the overload method chosen by the compiler in this example is
WriteLine(int). So if you callWriteLine((uint)byteValue)orWriteLine(byteValue.ToString()), you'll get the result255.The compiler prefers 32-bit signed integer types today and will encode
sbyteValue: -1toffffffffnot000000ffbecause of the efficiency.The side-effect of optimzation in release mode.
You can see in debug mode, it uses a local variable to transmit the byte. The docs of
stlocsays.Since there is no middleman in release mode, no truncation,
WriteLinemethod will use the return valueffffffffin the register as is. The effect also applies toshort+ushortwith the same reason.Example 2A: Inconsistency in Release mode
According to the explaination above the values returned from
GetIntValueEncapsulatedorGetIntValuein the registers are alwaysffffffff.Sorry I'm not a JIT expert, so I can't tell the implemention detail. What I know is this is caused by method inlining. Apply
NoInliningto the method, the output is -1.The following code can be used to simulate the effect of forced inlining.
When the method is inline the compiler uses the following instrument to set the value of value2 which force the type to byte.
To enforce the expected result
uint*first