Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding System.Runtime.CompilerServices.Unsafe.BitCast #82917

Merged
merged 13 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/coreclr/jit/fgbasic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1484,6 +1484,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
141 changes: 141 additions & 0 deletions src/coreclr/jit/importercalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4002,6 +4002,143 @@ 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);

if (fromSize != toSize)
tannergooding marked this conversation as resolved.
Show resolved Hide resolved
{
// 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this whole condition be simplified to

if ((fromType == TYP_STRUCT) ^ ((fromType == TYP_STRUCT))
    return null;
if ((fromType == TYP_STRUCT) && ((fromType == TYP_STRUCT))
    ...

?
(nit)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that code is much less obvious and harder to read/understand. It's the kind of thing that you have to stop and think about boolean tables to remember what it does.

The current code is a bit more verbose, but I think that almost anyone can understand what's happening.

{
involvesStructType = true;

if (toType == TYP_STRUCT)
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
{
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))
tannergooding marked this conversation as resolved.
Show resolved Hide resolved
{
// 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())
Copy link
Member

@EgorBo EgorBo Mar 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move these foldings to gtFoldExprConst ? (or I'd expect them to be already there) so we can just emit bitcast here and have much simpler logic - it is a bit hard to read. Although, you don't even need to call gtFoldExprConst here, it will likely be called by someone else in importer, e.g. if it's part of a condition for a branch

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can, but I think it can/should be a separate PR given it has to touch a bit of unrelated other code that exists.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notably, we can't "easily" do this since it would regress handling on 32-bit for double/int64. Once we add the relevant decomposition support then it won't be so problematic.

{
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);
}
break;
tannergooding marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 @@ -8202,6 +8339,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 reference as a reference to a value of type <typeparamref name="TTo"/>.
tannergooding marked this conversation as resolved.
Show resolved Hide resolved
/// </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
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this check IsReferenceOrContainsReferences on TFrom/TTo and throw?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply checking that IsReferenceOrContainsReferences is the same between TFrom and TTo will only catch a subset of cases. It won't catch the more complex cases where they match, but the layouts are then considered "incompatible".

The implementation will "do the right thing" as the software fallback just uses As<TFrom, TTo>(ref value) and the intrinsic logic checks for compatible layouts. It's just that them being actually incompatible may result in UB, much as it would for As or many of the other Unsafe APIs.

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