Skip to content

Commit

Permalink
Adding System.Runtime.CompilerServices.Unsafe.BitCast (#82917)
Browse files Browse the repository at this point in the history
* Adding System.Runtime.CompilerServices.Unsafe.BitCast

* Adding some basic intrinsic recognition for Unsafe.BitCast

* Use ClassLayout::AreCompatible as part of Unsafe.BitCast

* Fixup BitConverter to use Unsafe.BitCast

* Fixup Enum to use Unsafe.BitCast

* Ensure BitCast resolves the right generic type for toTypeHnd

* Use Unsafe.BitCast in places using the `*(TTo*)&tfrom` pattern

* Don't use BitCast in places the generic constraints disallows it

* Missing semicolon

* Don't use Unsafe.BitCast where it introduces additional generic instantiations

* Don't regress the files that are used for both netstandard and netcoreapp

* Responding to PR feedback

* Fix a typo in the bitcast tests
  • Loading branch information
tannergooding authored Mar 8, 2023
1 parent f5fd94b commit 3f3db95
Show file tree
Hide file tree
Showing 14 changed files with 267 additions and 32 deletions.
1 change: 1 addition & 0 deletions src/coreclr/jit/fgbasic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,7 @@ void Compiler::fgFindJumpTargets(const BYTE* codeAddr, IL_OFFSET codeSize, Fixed
#endif // FEATURE_HW_INTRINSICS
case NI_SRCS_UNSAFE_As:
case NI_SRCS_UNSAFE_AsRef:
case NI_SRCS_UNSAFE_BitCast:
case NI_SRCS_UNSAFE_SkipInit:
{
// TODO-CQ: These are no-ops in that they never produce any IR
Expand Down
143 changes: 143 additions & 0 deletions src/coreclr/jit/importercalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4027,6 +4027,145 @@ GenTree* Compiler::impSRCSUnsafeIntrinsic(NamedIntrinsic intrinsic,
return impPopStack().val;
}

case NI_SRCS_UNSAFE_BitCast:
{
assert(sig->sigInst.methInstCount == 2);

CORINFO_CLASS_HANDLE fromTypeHnd = sig->sigInst.methInst[0];
CORINFO_CLASS_HANDLE toTypeHnd = sig->sigInst.methInst[1];

if (fromTypeHnd == toTypeHnd)
{
// Handle the easy case of matching type handles, such as `int` to `int`
return impPopStack().val;
}

unsigned fromSize = info.compCompHnd->getClassSize(fromTypeHnd);
unsigned toSize = info.compCompHnd->getClassSize(toTypeHnd);

// Runtime requires all types to be at least 1-byte
assert((fromSize != 0) && (toSize != 0));

if (fromSize != toSize)
{
// Fallback to the software implementation to throw when sizes don't match
return nullptr;
}

CorInfoType fromJitType = info.compCompHnd->asCorInfoType(fromTypeHnd);
var_types fromType = JITtype2varType(fromJitType);

CorInfoType toJitType = info.compCompHnd->asCorInfoType(toTypeHnd);
var_types toType = JITtype2varType(toJitType);

bool involvesStructType = false;

if (fromType == TYP_STRUCT)
{
involvesStructType = true;

if (toType == TYP_STRUCT)
{
ClassLayout* fromLayout = typGetObjLayout(fromTypeHnd);
ClassLayout* toLayout = typGetObjLayout(toTypeHnd);

if (ClassLayout::AreCompatible(fromLayout, toLayout))
{
// Handle compatible struct layouts where we can simply return op1
return impPopStack().val;
}
}
}
else if (toType == TYP_STRUCT)
{
involvesStructType = true;
}

if (involvesStructType)
{
// TODO-CQ: Handle this by getting the address of `op1` and then dereferencing
// that as TTo, much as `Unsafe.As<TFrom, TTo>(ref op1)` would work.
return nullptr;
}

if (varTypeIsFloating(fromType))
{
// Handle bitcasting from floating to same sized integral, such as `float` to `int`
assert(varTypeIsIntegral(toType));

#if !TARGET_64BIT
if ((fromType == TYP_DOUBLE) && !impStackTop().val->IsCnsFltOrDbl())
{
// TODO-Cleanup: We should support this on 32-bit but it requires decomposition work
return nullptr;
}
#endif // !TARGET_64BIT

GenTree* op1 = impPopStack().val;

if (op1->IsCnsFltOrDbl())
{
if (fromType == TYP_DOUBLE)
{
double f64Cns = static_cast<double>(op1->AsDblCon()->DconValue());
return gtNewLconNode(static_cast<int64_t>(BitOperations::DoubleToUInt64Bits(f64Cns)));
}
else
{
assert(fromType == TYP_FLOAT);
float f32Cns = static_cast<float>(op1->AsDblCon()->DconValue());
return gtNewIconNode(static_cast<int32_t>(BitOperations::SingleToUInt32Bits(f32Cns)));
}
}
else
{
toType = varTypeToSigned(toType);
op1 = impImplicitR4orR8Cast(op1, fromType);
return gtNewBitCastNode(toType, op1);
}
break;
}

if (varTypeIsFloating(toType))
{
// Handle bitcasting from integral to same sized floating, such as `int` to `float`
assert(varTypeIsIntegral(fromType));

#if !TARGET_64BIT
if ((toType == TYP_DOUBLE) && !impStackTop().val->IsIntegralConst())
{
// TODO-Cleanup: We should support this on 32-bit but it requires decomposition work
return nullptr;
}
#endif // !TARGET_64BIT

GenTree* op1 = impPopStack().val;

if (op1->IsIntegralConst())
{
if (toType == TYP_DOUBLE)
{
uint64_t u64Cns = static_cast<uint64_t>(op1->AsIntConCommon()->LngValue());
return gtNewDconNode(BitOperations::UInt64BitsToDouble(u64Cns), TYP_DOUBLE);
}
else
{
assert(toType == TYP_FLOAT);

uint32_t u32Cns = static_cast<uint32_t>(op1->AsIntConCommon()->IconValue());
return gtNewDconNode(BitOperations::UInt32BitsToSingle(u32Cns), TYP_FLOAT);
}
}
else
{
return gtNewBitCastNode(toType, op1);
}
}

// Handle bitcasting for same sized integrals, such as `int` to `uint`
return impPopStack().val;
}

case NI_SRCS_UNSAFE_ByteOffset:
{
assert(sig->sigInst.methInstCount == 1);
Expand Down Expand Up @@ -8227,6 +8366,10 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method)
{
result = NI_SRCS_UNSAFE_AsRef;
}
else if (strcmp(methodName, "BitCast") == 0)
{
result = NI_SRCS_UNSAFE_BitCast;
}
else if (strcmp(methodName, "ByteOffset") == 0)
{
result = NI_SRCS_UNSAFE_ByteOffset;
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/jit/namedintrinsiclist.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ enum NamedIntrinsic : unsigned short
NI_SRCS_UNSAFE_As,
NI_SRCS_UNSAFE_AsPointer,
NI_SRCS_UNSAFE_AsRef,
NI_SRCS_UNSAFE_BitCast,
NI_SRCS_UNSAFE_ByteOffset,
NI_SRCS_UNSAFE_Copy,
NI_SRCS_UNSAFE_CopyBlock,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ internal IntPtr ReadIntPtr(int offset)
internal unsafe float ReadSingle(int offset)
{
int value = ReadInt32(offset);
return *(float*)&value;
return BitConverter.Int32BitsToSingle(value);
}

protected override bool ReleaseHandle()
Expand Down Expand Up @@ -636,7 +636,7 @@ internal void WriteIntPtr(int offset, IntPtr value)

internal unsafe void WriteSingle(int offset, float value)
{
WriteInt32(offset, *(int*)&value);
WriteInt32(offset, BitConverter.SingleToInt32Bits(value));
}

internal void ZeroMemory()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ internal IntPtr ReadIntPtr(int offset)
internal unsafe float ReadSingle(int offset)
{
int value = ReadInt32(offset);
return *(float*)&value;
return BitConverter.Int32BitsToSingle(value);
}

protected override bool ReleaseHandle()
Expand Down Expand Up @@ -615,7 +615,7 @@ internal void WriteIntPtr(int offset, IntPtr value)

internal unsafe void WriteSingle(int offset, float value)
{
WriteInt32(offset, *(int*)&value);
WriteInt32(offset, BitConverter.SingleToInt32Bits(value));
}

internal Guid ReadGuid(int offset)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,10 @@ private static bool IsParsedValueEqual<T>(T expected, T actual)
double expectedDouble = (double)(object)expected;
double actualDouble = (double)(object)actual;

unsafe
{
if (*((ulong*)&expectedDouble) != *((ulong*)&actualDouble))
return false;
if (BitConverter.DoubleToUInt64Bits(expectedDouble) != BitConverter.DoubleToUInt64Bits(actualDouble))
return false;

return true;
}
return true;
}

// Parsed floating points are constructed, not computed. Thus, we can do the exact compare.
Expand All @@ -121,13 +118,10 @@ private static bool IsParsedValueEqual<T>(T expected, T actual)
float expectedSingle = (float)(object)expected;
float actualSingle = (float)(object)actual;

unsafe
{
if (*((uint*)&expectedSingle) != *((uint*)&actualSingle))
return false;
if (BitConverter.SingleToUInt32Bits(expectedSingle) != BitConverter.SingleToUInt32Bits(actualSingle))
return false;

return true;
}
return true;
}

return expected.Equals(actual);
Expand Down
16 changes: 8 additions & 8 deletions src/libraries/System.Private.CoreLib/src/System/BitConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -764,31 +764,31 @@ public static bool ToBoolean(ReadOnlySpan<byte> value)
/// <param name="value">The number to convert.</param>
/// <returns>A 64-bit signed integer whose bits are identical to <paramref name="value"/>.</returns>
[Intrinsic]
public static unsafe long DoubleToInt64Bits(double value) => *((long*)&value);
public static unsafe long DoubleToInt64Bits(double value) => Unsafe.BitCast<double, long>(value);

/// <summary>
/// Converts the specified 64-bit signed integer to a double-precision floating point number.
/// </summary>
/// <param name="value">The number to convert.</param>
/// <returns>A double-precision floating point number whose bits are identical to <paramref name="value"/>.</returns>
[Intrinsic]
public static unsafe double Int64BitsToDouble(long value) => *((double*)&value);
public static unsafe double Int64BitsToDouble(long value) => Unsafe.BitCast<long, double>(value);

/// <summary>
/// Converts the specified single-precision floating point number to a 32-bit signed integer.
/// </summary>
/// <param name="value">The number to convert.</param>
/// <returns>A 32-bit signed integer whose bits are identical to <paramref name="value"/>.</returns>
[Intrinsic]
public static unsafe int SingleToInt32Bits(float value) => *((int*)&value);
public static unsafe int SingleToInt32Bits(float value) => Unsafe.BitCast<float, int>(value);

/// <summary>
/// Converts the specified 32-bit signed integer to a single-precision floating point number.
/// </summary>
/// <param name="value">The number to convert.</param>
/// <returns>A single-precision floating point number whose bits are identical to <paramref name="value"/>.</returns>
[Intrinsic]
public static unsafe float Int32BitsToSingle(int value) => *((float*)&value);
public static unsafe float Int32BitsToSingle(int value) => Unsafe.BitCast<int, float>(value);

/// <summary>
/// Converts the specified half-precision floating point number to a 16-bit signed integer.
Expand All @@ -813,7 +813,7 @@ public static bool ToBoolean(ReadOnlySpan<byte> value)
/// <returns>A 64-bit unsigned integer whose bits are identical to <paramref name="value"/>.</returns>
[CLSCompliant(false)]
[Intrinsic]
public static unsafe ulong DoubleToUInt64Bits(double value) => *((ulong*)&value);
public static unsafe ulong DoubleToUInt64Bits(double value) => Unsafe.BitCast<double, ulong>(value);

/// <summary>
/// Converts the specified 64-bit unsigned integer to a double-precision floating point number.
Expand All @@ -822,7 +822,7 @@ public static bool ToBoolean(ReadOnlySpan<byte> value)
/// <returns>A double-precision floating point number whose bits are identical to <paramref name="value"/>.</returns>
[CLSCompliant(false)]
[Intrinsic]
public static unsafe double UInt64BitsToDouble(ulong value) => *((double*)&value);
public static unsafe double UInt64BitsToDouble(ulong value) => Unsafe.BitCast<ulong, double>(value);

/// <summary>
/// Converts the specified single-precision floating point number to a 32-bit unsigned integer.
Expand All @@ -831,7 +831,7 @@ public static bool ToBoolean(ReadOnlySpan<byte> value)
/// <returns>A 32-bit unsigned integer whose bits are identical to <paramref name="value"/>.</returns>
[CLSCompliant(false)]
[Intrinsic]
public static unsafe uint SingleToUInt32Bits(float value) => *((uint*)&value);
public static unsafe uint SingleToUInt32Bits(float value) => Unsafe.BitCast<float, uint>(value);

/// <summary>
/// Converts the specified 32-bit unsigned integer to a single-precision floating point number.
Expand All @@ -840,7 +840,7 @@ public static bool ToBoolean(ReadOnlySpan<byte> value)
/// <returns>A single-precision floating point number whose bits are identical to <paramref name="value"/>.</returns>
[CLSCompliant(false)]
[Intrinsic]
public static unsafe float UInt32BitsToSingle(uint value) => *((float*)&value);
public static unsafe float UInt32BitsToSingle(uint value) => Unsafe.BitCast<uint, float>(value);

/// <summary>
/// Converts the specified half-precision floating point number to a 16-bit unsigned integer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ private static unsafe uint GetExponent(float f)
// ULONG sign:1;
// } SNGSTRUCT;

return (byte)(*(uint*)&f >> 23);
return (byte)(BitConverter.SingleToUInt32Bits(f) >> 23);
}

private static unsafe uint GetExponent(double d)
Expand All @@ -171,7 +171,7 @@ private static unsafe uint GetExponent(double d)
// DWORDLONG signexp:12;
// } DBLSTRUCT;

return (uint)(*(ulong*)&d >> 52) & 0x7FFu;
return (uint)(BitConverter.DoubleToUInt64Bits(d) >> 52) & 0x7FFu;
}

private static ulong UInt32x32To64(uint a, uint b)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,24 @@ public static bool AreSame<T>([AllowNull] ref T left, [AllowNull] ref T right)
// ret
}

/// <summary>
/// Reinterprets the given value of type <typeparamref name="TFrom" /> as a value of type <typeparamref name="TTo" />.
/// </summary>
/// <exception cref="NotSupportedException">The size of <typeparamref name="TFrom" /> and <typeparamref name="TTo" /> are not the same.</exception>
[Intrinsic]
[NonVersionable]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TTo BitCast<TFrom, TTo>(TFrom source)
where TFrom : struct
where TTo : struct
{
if (sizeof(TFrom) != sizeof(TTo))
{
ThrowHelper.ThrowNotSupportedException();
}
return As<TFrom, TTo>(ref source);
}

/// <summary>
/// Copies a value of type T to the given location.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ public static void Write(ref byte location, byte value) =>
public static double Read(ref double location)
{
long result = Read(ref Unsafe.As<double, long>(ref location));
return *(double*)&result;
return BitConverter.Int64BitsToDouble(result);
}

[Intrinsic]
[NonVersionable]
public static void Write(ref double location, double value) =>
Write(ref Unsafe.As<double, long>(ref location), *(long*)&value);
Write(ref Unsafe.As<double, long>(ref location), BitConverter.DoubleToInt64Bits(value));
#endregion

#region Int16
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -710,15 +710,13 @@ public static int ToCharsR(long value, byte[] chars, int offset)
private static unsafe bool IsNegativeZero(float value)
{
// Simple equals function will report that -0 is equal to +0, so compare bits instead
float negativeZero = -0e0F;
return (*(int*)&value == *(int*)&negativeZero);
return BitConverter.SingleToUInt32Bits(value) == 0x8000_0000U;
}

private static unsafe bool IsNegativeZero(double value)
{
// Simple equals function will report that -0 is equal to +0, so compare bits instead
double negativeZero = -0e0;
return (*(long*)&value == *(long*)&negativeZero);
return BitConverter.DoubleToUInt64Bits(value) == 0x8000_0000_0000_0000UL;
}

private static int ToInfinity(bool isNegative, byte[] buffer, int offset)
Expand Down
Loading

0 comments on commit 3f3db95

Please sign in to comment.