Skip to content

[API Proposal]: Unsafe.BitCast for efficient type reinterpretation #81334

@MichalPetryka

Description

@MichalPetryka

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:

  1. Should the API have any generic constraints? Restricting it to unmanaged or struct types would make it safer to use but potentially less useful in generic contexts (for example with Enum constraint, since that by itself doesn't restrict it to such types).
  2. 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), the value written 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 with Unsafe.As and could be used for extending stuff like Vectors.
  3. Would BitCast or Bitcast be 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-System.Runtime.CompilerServicesgood first issueIssue should be easy to implement, good for first-time contributorshelp wanted[up-for-grabs] Good issue for external contributors

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions