-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Background and motivation
Reinterpretation of types - especially value ones - is a common occurrence in high performance code.
Currently in C#, it is usually realized using the Unsafe.As API or with the *(TTo*)&value pattern. The JIT however has some issues with optimizing such code, due to the value being marked as address taken. Introducing a new API with a more descriptive name might be easier and cheaper than making the JIT recognize such pattern.
The name BitCast was inspired by the C++ std::bit_cast and LLVM bitcast.
Open questions:
- Should the API have any generic constraints? Restricting it to
unmanagedorstructtypes would make it safer to use but potentially less useful in generic contexts (for example withEnumconstraint, since that by itself doesn't restrict it to such types). - What should the behaviour be for non matching size types? As I see it, there are two reasonable options:
a) make the API throw in such case - that's what the C++ and LLVM bitcasts do, not really useful but could be regarded as the expected behaviour.
b) make the API function as if a buffer of the larger size was created (and zero initialized), thevaluewritten into it and then the target type read from its beginning. This behaviour would allow the API to be utilized in the current code that reads a smaller type from the beginning of a larger one withUnsafe.Asand could be used for extending stuff like Vectors. - Would
BitCastorBitcastbe the preferred casing?
With different sizes being permitted the code would behave like this:
unsafe static TTo BitCast<TFrom, TTo>(TFrom value)
{
if (sizeof(TFrom) >= sizeof(TTo))
return *(TTo*)&value;
TTo v = default;
*(TFrom*)&v= value;
return v;
}With only same sizes being accepted it'd work like this:
unsafe static TTo BitCast<TFrom, TTo>(TFrom value)
{
if (sizeof(TFrom) != sizeof(TTo))
throw new NotSupportedException();
return *(TTo*)&value;
}API Proposal
namespace System.Runtime.CompilerServices;
public static class Unsafe
{
public static TTo BitCast<TFrom, TTo>(TFrom value);
}API Usage
| if (sizeof(T) == 1) | |
| { | |
| vector = new Vector<byte>(Unsafe.As<T, byte>(ref tmp)); | |
| } | |
| else if (sizeof(T) == 2) | |
| { | |
| vector = (Vector<byte>)(new Vector<ushort>(Unsafe.As<T, ushort>(ref tmp))); | |
| } | |
| else if (sizeof(T) == 4) | |
| { | |
| // special-case float since it's already passed in a SIMD reg | |
| vector = (typeof(T) == typeof(float)) | |
| ? (Vector<byte>)(new Vector<float>((float)(object)tmp!)) | |
| : (Vector<byte>)(new Vector<uint>(Unsafe.As<T, uint>(ref tmp))); | |
| } | |
| else if (sizeof(T) == 8) | |
| { | |
| // special-case double since it's already passed in a SIMD reg | |
| vector = (typeof(T) == typeof(double)) | |
| ? (Vector<byte>)(new Vector<double>((double)(object)tmp!)) | |
| : (Vector<byte>)(new Vector<ulong>(Unsafe.As<T, ulong>(ref tmp))); | |
| } |
// instead of non generic BitConverter APIs:
// Console.WriteLine(BitConverter.UInt32BitsToSingle(12345));
Console.WriteLine(Unsafe.BitCast<uint, float>(12345));
// prints 1.7299E-41
// bitcasting to a same sized type
Console.WriteLine(Unsafe.BitCast<Vector4, Vector128<float>>(new Vector4(1f, 2f, 3f, 4f)));
// prints <1, 2, 3, 4>
// bitcasting to a smaller type
Console.WriteLine(Unsafe.BitCast<Vector4, Vector2>(new Vector4(1f, 2f, 3f, 4f)));
// prints <1, 2>
// bitcasting to a bigger type
Console.WriteLine(Unsafe.BitCast<Vector2, Vector4>(new Vector2(1f, 2f)));
// prints <1, 2, 0, 0>Alternative Designs
Using Unsafe.As for same sized or smaller types, writing to a stack allocated, zero initialized, buffer of the destination type size and reading from it.
Risks
Unsafely reinterpreting managed types if there are no generic constraints can lead to GC holes, unexpected behaviour on big endian platforms for non matching sizes can cause code to become platform specific.