Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions src/coreclr/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -3888,6 +3888,8 @@ class Compiler
void impImportLeave(BasicBlock* block);
void impResetLeaveBlock(BasicBlock* block, unsigned jmpAddr);
GenTree* impTypeIsAssignable(GenTree* typeTo, GenTree* typeFrom);
GenTree* impUnrollSpanComparisonWithStrCon(GenTree* span, GenTreeStrCon* cnsStr, bool ignoreCase, bool startsWith);

GenTree* impIntrinsic(GenTree* newobjThis,
CORINFO_CLASS_HANDLE clsHnd,
CORINFO_METHOD_HANDLE method,
Expand Down
16 changes: 16 additions & 0 deletions src/coreclr/jit/compiler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,22 @@ inline unsigned genTypeSize(T type)
return genTypeSizes[TypeGet(type)];
}

//------------------------------------------------------------------------------
// genTrimUnsignedValue: Trim size_t value to the size of the given type.
//
// Arguments:
// type - the type to fit the value into.
// value - the value.
//
// Return Value:
// The value trimmed to the size of the given type.

inline size_t genTrimUnsignedValue(var_types type, size_t value)
{
assert(genTypeSize(type) <= sizeof(size_t));
return (((size_t)-1) >> ((sizeof(size_t) - genTypeSize(type)) * 8)) & value;
}

/*****************************************************************************
*
* Return the "stack slot count" of the given type.
Expand Down
265 changes: 265 additions & 0 deletions src/coreclr/jit/importer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4318,6 +4318,97 @@ GenTree* Compiler::impIntrinsic(GenTree* newobjThis,
break;
}

case NI_System_MemoryExtensions_Equals:
case NI_System_MemoryExtensions_SequenceEqual:
case NI_System_MemoryExtensions_StartsWith:
{
assert((sig->numArgs == 2) || (sig->numArgs == 3));

// We're looking for:
//
// bool x1 = arg0.StartsWith(String.op_Implicit("cstr"));
// bool x2 = arg0.SequenceEqual(arg0, String.op_Implicit("cstr"));
//
// In order to optimize it into:
//
// bool x1 = arg0.Length >= 4 && *(arg0._pointer) == ToHexConst("cstr");
// bool x2 = arg0.Length == 4 && *(arg0._pointer) == ToHexConst("cstr");
//
GenTree* arg0 = impStackTop(sig->numArgs - 1).val;
GenTree* arg1 = impStackTop(sig->numArgs - 2).val;
bool ignoreCase = false;

if ((sig->numArgs == 3) && impStackTop(0).val->IsCnsIntOrI())
{
assert((ni == NI_System_MemoryExtensions_Equals) || (ni == NI_System_MemoryExtensions_StartsWith));

// See StringComparison.cs
const int Ordinal = 4;
const int OrdinalIgnoreCase = 5;

// Fetch mode from the last argument.
int mode = (int)impStackTop(0).val->AsIntCon()->IconValue();

if (mode == OrdinalIgnoreCase)
{
ignoreCase = true;
}
else if (mode == Ordinal)
{
ignoreCase = false;
}
else
{
// This mode is not supported.
assert(mode <= OrdinalIgnoreCase);
return nullptr;
}
}
else if (sig->numArgs == 3)
{
// Comparison mode is not a constant.
return nullptr;
}

if (arg1->OperIs(GT_RET_EXPR))
{
GenTreeCall* strToSpanCall = arg1->AsRetExpr()->gtInlineCandidate->AsCall();
if (!(strToSpanCall->gtFlags & CORINFO_FLG_JIT_INTRINSIC))
{
// strToSpanCall must be either:
// ReadOnlySpan<char> String.op_Implicit(String) or
// ReadOnlySpan<char> MemoryExtensions.AsSpan(String)
break;
}

NamedIntrinsic strToSpanNi = lookupNamedIntrinsic(strToSpanCall->gtCallMethHnd);
if ((strToSpanNi != NI_System_String_op_Implicit) &&
(strToSpanNi != NI_System_MemoryExtensions_AsSpan))
{
break;
}
if (!strToSpanCall->gtCallArgs->GetNode()->OperIs(GT_CNS_STR))
{
break;
}

GenTreeStrCon* strCon = strToSpanCall->gtCallArgs->GetNode()->AsStrCon();
bool startsWith = (ni == NI_System_MemoryExtensions_StartsWith);
GenTree* newNode = impUnrollSpanComparisonWithStrCon(arg0, strCon, ignoreCase, startsWith);
if (newNode != nullptr)
{
retNode = newNode;
strToSpanCall->ReplaceWith(gtNewNothingNode(), this);
for (unsigned i = 0; i < sig->numArgs; i++)
{
impPopStack();
}
break;
}
}
break;
}

case NI_System_Threading_Thread_get_ManagedThreadId:
{
if (opts.OptimizationEnabled() && impStackTop().val->OperIs(GT_RET_EXPR))
Expand Down Expand Up @@ -4573,6 +4664,154 @@ GenTree* Compiler::impTypeIsAssignable(GenTree* typeTo, GenTree* typeFrom)
return nullptr;
}

GenTree* Compiler::impUnrollSpanComparisonWithStrCon(GenTree* span,
GenTreeStrCon* cnsStr,
bool ignoreCase,
bool startsWith)
{
#ifdef TARGET_64BIT
// Grab the actual string literal from VM
int strLen = -1;
LPCWSTR str = info.compCompHnd->getStringLiteral(cnsStr->gtScpHnd, cnsStr->gtSconCPX, &strLen);
var_types cmpType;

if (strLen == 0)
{
if (startsWith)
{
// Any span starts with "", return true
return gtNewIconNode(1);
}
else
{
// TODO: Emit `span.Length == 0`
return nullptr;
}
}
else if (strLen == 1)
{
cmpType = TYP_SHORT;
}
else if (strLen == 2)
{
cmpType = TYP_INT;
}
else if (strLen == 4)
{
cmpType = TYP_LONG;
}
else
{
// TODO: Support other sizes and emit SIMD for larger strings
return nullptr;
}

if (str == nullptr)
{
// getStringLiteral couldn't manage to get the literal (e.g. in dynamic context)
return nullptr;
}

bool allAreLetters = true;
UINT64 strAsUlong = 0;

for (int i = 0; i < strLen; i++)
{
UINT64 strChar = str[i];
if (strChar > 127)
{
// str is not ASCII - bail out.
return nullptr;
}

bool isLetter = ((strChar >= 'A') && (strChar <= 'Z')) || ((strChar >= 'a') && (strChar <= 'z'));
if (!isLetter)
{
allAreLetters = false;
}
strAsUlong |= (strChar << 16UL * i);
}

if (ignoreCase)
{
if (!allAreLetters)
{
// TODO: Implement logic from UInt64OrdinalIgnoreCaseAscii
//
// bool x = (((IND ^ strAsUlong)<<2) &
// (((strAsUlong+0x0005000500050005ul)|0x00A000A000A000A0ul)+0x001A001A001A001Aul)|0xFF7FFF7FFF7FFF7Ful)==0;
//
return nullptr;
}
strAsUlong |= 0x0020002000200020ULL;
}

// We're going to emit the following tree:
//
// \--* QMARK int
// +--* GE/EQ int
// | +--* FIELD int Span<char>._length
// | \--* CNS_INT int %strLen%
// \--* COLON int
// +--* CNS_INT int 0 (false)
// \--* EQ int
// +--* IND long
// | \--* FIELD byref Span<char>._pointer
// \--* CNS_INT long %strAsUlong%
//

CORINFO_CLASS_HANDLE spanCls = gtGetStructHandle(span);
CORINFO_FIELD_HANDLE pointerHnd = info.compCompHnd->getFieldInClass(spanCls, 0);
CORINFO_FIELD_HANDLE lengthHnd = info.compCompHnd->getFieldInClass(spanCls, 1);
const unsigned pointerOffset = info.compCompHnd->getFieldOffset(pointerHnd);
const unsigned lengthOffset = info.compCompHnd->getFieldOffset(lengthHnd);

GenTree* spanRef = span;
if (span->TypeIs(TYP_STRUCT))
{
spanRef = gtNewOperNode(GT_ADDR, TYP_BYREF, span);
}
assert(spanRef->TypeIs(TYP_BYREF));

// We're going to use spanRef twice so need to clone it
GenTree* spanRefClone = nullptr;
spanRef =
impCloneExpr(spanRef, &spanRefClone, NO_CLASS_HANDLE, (unsigned)CHECK_SPILL_ALL, nullptr DEBUGARG("spanRef"));

GenTree* spanData = gtNewFieldRef(TYP_BYREF, pointerHnd, spanRefClone, pointerOffset);
GenTree* spanDataIndir = gtNewIndir(cmpType, spanData);
GenTreeIntCon* constStrAsIntCon = gtNewIconNode(genTrimUnsignedValue(cmpType, strAsUlong), cmpType);

if (ignoreCase)
{
ssize_t lowerBitMask = genTrimUnsignedValue(cmpType, 0x0020002000200020ULL);

// Set "is lower" bits in all chars
spanDataIndir = gtNewOperNode(GT_OR, cmpType, spanDataIndir, gtNewIconNode(lowerBitMask, cmpType));
}

// TODO: for length == 3 (not supported yet) we need to do two indir cmp ops:
//
// bool x = (*(Int64*)span._pointer ^ 0xXXX) | (*((Int32*)span._pointer + 2) ^ 0xYYY) != 0
//
GenTree* indirCmp = gtNewOperNode(GT_EQ, TYP_INT, spanDataIndir, constStrAsIntCon);
GenTree* spanLenField = gtNewFieldRef(TYP_INT, lengthHnd, spanRef, lengthOffset);
GenTreeColon* colon = new (this, GT_COLON) GenTreeColon(TYP_INT, indirCmp, gtNewIconNode(0));

GenTreeQmark* qmark =
gtNewQmarkNode(TYP_INT, gtNewOperNode(startsWith ? GT_GE : GT_EQ, TYP_INT, spanLenField, gtNewIconNode(strLen)),
colon);

// Spill qmark into a temp.
unsigned tmp = lvaGrabTemp(true DEBUGARG("spilling STARTSWITH root qmark"));
impAssignTempGen(tmp, qmark, (unsigned)CHECK_SPILL_NONE);
return gtNewLclvNode(tmp, TYP_INT);
#else // TARGET_64BIT
// TODO: Enable for 32 at least for length [0..2]
return nullptr;
#endif
}

GenTree* Compiler::impMathIntrinsic(CORINFO_METHOD_HANDLE method,
CORINFO_SIG_INFO* sig,
var_types callType,
Expand Down Expand Up @@ -4831,6 +5070,32 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method)
result = NI_System_Type_IsAssignableTo;
}
}
else if (strcmp(className, "MemoryExtensions") == 0)
{
if (strcmp(methodName, "StartsWith") == 0)
{
result = NI_System_MemoryExtensions_StartsWith;
}
else if (strcmp(methodName, "SequenceEqual") == 0)
{
result = NI_System_MemoryExtensions_SequenceEqual;
}
else if (strcmp(methodName, "Equals") == 0)
{
result = NI_System_MemoryExtensions_Equals;
}
else if (strcmp(methodName, "AsSpan") == 0)
{
result = NI_System_MemoryExtensions_AsSpan;
}
}
else if (strcmp(className, "String") == 0)
{
if (strcmp(methodName, "op_Implicit") == 0)
{
result = NI_System_String_op_Implicit;
}
}
}
else if (strcmp(namespaceName, "System.Threading") == 0)
{
Expand Down
5 changes: 5 additions & 0 deletions src/coreclr/jit/namedintrinsiclist.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ enum NamedIntrinsic : unsigned short
NI_System_Type_IsAssignableFrom,
NI_System_Type_IsAssignableTo,
NI_System_Array_Clone,
NI_System_MemoryExtensions_AsSpan,
NI_System_MemoryExtensions_Equals,
NI_System_MemoryExtensions_StartsWith,
NI_System_MemoryExtensions_SequenceEqual,
NI_System_String_op_Implicit,

// These are used by HWIntrinsics but are defined more generally
// to allow dead code optimization and handle the recursion case
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public static bool Contains(this ReadOnlySpan<char> span, ReadOnlySpan<char> val
/// <param name="other">The value to compare with the source span.</param>
/// <param name="comparisonType">One of the enumeration values that determines how the <paramref name="span"/> and <paramref name="other"/> are compared.</param>
/// </summary>
[Intrinsic]
public static bool Equals(this ReadOnlySpan<char> span, ReadOnlySpan<char> other, StringComparison comparisonType)
{
string.CheckStringComparison(comparisonType);
Expand Down Expand Up @@ -331,6 +332,7 @@ ref MemoryMarshal.GetReference(value),
/// <param name="span">The source span.</param>
/// <param name="value">The sequence to compare to the beginning of the source span.</param>
/// <param name="comparisonType">One of the enumeration values that determines how the <paramref name="span"/> and <paramref name="value"/> are compared.</param>
[Intrinsic]
public static bool StartsWith(this ReadOnlySpan<char> span, ReadOnlySpan<char> value, StringComparison comparisonType)
{
string.CheckStringComparison(comparisonType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public static Span<T> AsSpan<T>(this T[]? array, Range range)
/// </summary>
/// <param name="text">The target string.</param>
/// <remarks>Returns default when <paramref name="text"/> is null.</remarks>
[Intrinsic]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ReadOnlySpan<char> AsSpan(this string? text)
{
Expand Down Expand Up @@ -991,6 +992,7 @@ ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(values)),
/// <summary>
/// Determines whether two sequences are equal by comparing the elements using IEquatable{T}.Equals(T).
/// </summary>
[Intrinsic]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool SequenceEqual<T>(this ReadOnlySpan<T> span, ReadOnlySpan<T> other) where T : IEquatable<T>
{
Expand Down Expand Up @@ -1038,6 +1040,7 @@ ref Unsafe.As<T, char>(ref MemoryMarshal.GetReference(other)),
/// <summary>
/// Determines whether the specified sequence appears at the start of the span.
/// </summary>
[Intrinsic]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool StartsWith<T>(this Span<T> span, ReadOnlySpan<T> value) where T : IEquatable<T>
{
Expand All @@ -1058,6 +1061,7 @@ ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(value)),
/// <summary>
/// Determines whether the specified sequence appears at the start of the span.
/// </summary>
[Intrinsic]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool StartsWith<T>(this ReadOnlySpan<T> span, ReadOnlySpan<T> value) where T : IEquatable<T>
{
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Private.CoreLib/src/System/String.cs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ public static string Create<TState>(int length, TState state, SpanAction<char, T
return result;
}

[Intrinsic]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator ReadOnlySpan<char>(string? value) =>
value != null ? new ReadOnlySpan<char>(ref value.GetRawStringData(), value.Length) : default;
Expand Down
Loading