-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Unroll SequenceEqual(ref byte, ref byte, nuint) in JIT #83945
Changes from 13 commits
1b74d09
7354f8b
81d6291
1840d4d
97b0c66
229f239
7bd602f
dba9524
cbb38ee
7ed7546
d767076
0a6f9ea
382866b
aeb8f23
01fd512
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -1865,6 +1865,185 @@ GenTree* Lowering::LowerCallMemmove(GenTreeCall* call) | |||||||||||||||||||
return nullptr; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
//------------------------------------------------------------------------ | ||||||||||||||||||||
// LowerCallMemcmp: Replace SpanHelpers.SequenceEqual)(left, right, CNS_SIZE) | ||||||||||||||||||||
// with a series of merged comparisons (via GT_IND nodes) | ||||||||||||||||||||
// | ||||||||||||||||||||
// Arguments: | ||||||||||||||||||||
// tree - GenTreeCall node to unroll as memcmp | ||||||||||||||||||||
// | ||||||||||||||||||||
// Return Value: | ||||||||||||||||||||
// nullptr if no changes were made | ||||||||||||||||||||
// | ||||||||||||||||||||
GenTree* Lowering::LowerCallMemcmp(GenTreeCall* call) | ||||||||||||||||||||
{ | ||||||||||||||||||||
JITDUMP("Considering Memcmp [%06d] for unrolling.. ", comp->dspTreeID(call)) | ||||||||||||||||||||
assert(comp->lookupNamedIntrinsic(call->gtCallMethHnd) == NI_System_SpanHelpers_SequenceEqual); | ||||||||||||||||||||
assert(call->gtArgs.CountUserArgs() == 3); | ||||||||||||||||||||
assert(TARGET_POINTER_SIZE == 8); | ||||||||||||||||||||
|
||||||||||||||||||||
if (!comp->opts.OptimizationEnabled()) | ||||||||||||||||||||
{ | ||||||||||||||||||||
JITDUMP("Optimizations aren't allowed - bail out.\n") | ||||||||||||||||||||
return nullptr; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
if (comp->info.compHasNextCallRetAddr) | ||||||||||||||||||||
{ | ||||||||||||||||||||
JITDUMP("compHasNextCallRetAddr=true so we won't be able to remove the call - bail out.\n") | ||||||||||||||||||||
return nullptr; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
GenTree* lengthArg = call->gtArgs.GetUserArgByIndex(2)->GetNode(); | ||||||||||||||||||||
if (lengthArg->IsIntegralConst()) | ||||||||||||||||||||
{ | ||||||||||||||||||||
ssize_t cnsSize = lengthArg->AsIntCon()->IconValue(); | ||||||||||||||||||||
JITDUMP("Size=%ld.. ", (LONG)cnsSize); | ||||||||||||||||||||
// TODO-CQ: drop the whole thing in case of 0 | ||||||||||||||||||||
if (cnsSize > 0) | ||||||||||||||||||||
{ | ||||||||||||||||||||
GenTree* lArg = call->gtArgs.GetUserArgByIndex(0)->GetNode(); | ||||||||||||||||||||
GenTree* rArg = call->gtArgs.GetUserArgByIndex(1)->GetNode(); | ||||||||||||||||||||
// TODO: Add SIMD path for [16..128] via GT_HWINTRINSIC nodes | ||||||||||||||||||||
if (cnsSize <= 16) | ||||||||||||||||||||
{ | ||||||||||||||||||||
unsigned loadWidth = 1 << BitOperations::Log2((unsigned)cnsSize); | ||||||||||||||||||||
var_types loadType; | ||||||||||||||||||||
if (loadWidth == 1) | ||||||||||||||||||||
{ | ||||||||||||||||||||
loadType = TYP_UBYTE; | ||||||||||||||||||||
} | ||||||||||||||||||||
else if (loadWidth == 2) | ||||||||||||||||||||
{ | ||||||||||||||||||||
loadType = TYP_USHORT; | ||||||||||||||||||||
} | ||||||||||||||||||||
else if (loadWidth == 4) | ||||||||||||||||||||
{ | ||||||||||||||||||||
loadType = TYP_INT; | ||||||||||||||||||||
} | ||||||||||||||||||||
else if ((loadWidth == 8) || (loadWidth == 16)) | ||||||||||||||||||||
{ | ||||||||||||||||||||
loadWidth = 8; | ||||||||||||||||||||
loadType = TYP_LONG; | ||||||||||||||||||||
} | ||||||||||||||||||||
else | ||||||||||||||||||||
{ | ||||||||||||||||||||
unreached(); | ||||||||||||||||||||
} | ||||||||||||||||||||
var_types actualLoadType = genActualType(loadType); | ||||||||||||||||||||
|
||||||||||||||||||||
GenTree* result = nullptr; | ||||||||||||||||||||
|
||||||||||||||||||||
// loadWidth == cnsSize means a single load is enough for both args | ||||||||||||||||||||
if ((loadWidth == (unsigned)cnsSize) && (loadWidth <= 8)) | ||||||||||||||||||||
{ | ||||||||||||||||||||
// We're going to emit something like the following: | ||||||||||||||||||||
// | ||||||||||||||||||||
// bool result = *(int*)leftArg == *(int*)rightArg | ||||||||||||||||||||
// | ||||||||||||||||||||
// ^ in the given example we unroll for length=4 | ||||||||||||||||||||
// | ||||||||||||||||||||
GenTree* lIndir = comp->gtNewIndir(loadType, lArg); | ||||||||||||||||||||
GenTree* rIndir = comp->gtNewIndir(loadType, rArg); | ||||||||||||||||||||
result = comp->gtNewOperNode(GT_EQ, TYP_INT, lIndir, rIndir); | ||||||||||||||||||||
|
||||||||||||||||||||
BlockRange().InsertAfter(lArg, lIndir); | ||||||||||||||||||||
BlockRange().InsertAfter(rArg, rIndir); | ||||||||||||||||||||
BlockRange().InsertBefore(call, result); | ||||||||||||||||||||
} | ||||||||||||||||||||
else | ||||||||||||||||||||
{ | ||||||||||||||||||||
// First, make both args multi-use: | ||||||||||||||||||||
LIR::Use lArgUse; | ||||||||||||||||||||
LIR::Use rArgUse; | ||||||||||||||||||||
bool lFoundUse = BlockRange().TryGetUse(lArg, &lArgUse); | ||||||||||||||||||||
bool rFoundUse = BlockRange().TryGetUse(rArg, &rArgUse); | ||||||||||||||||||||
assert(lFoundUse && rFoundUse); | ||||||||||||||||||||
Comment on lines
+1957
to
+1961
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a bit wasteful to go looking for the uses of this given that we know the arg they come from. E.g. you could do
Suggested change
I don't have a super strong opinion on it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank, will check in a follow up once SPMI is collected - want to see if it's worth the effort to improve this expansion. jit-diff utils found around 30 methods only |
||||||||||||||||||||
GenTree* lArgClone = comp->gtNewLclLNode(lArgUse.ReplaceWithLclVar(comp), lArg->TypeGet()); | ||||||||||||||||||||
GenTree* rArgClone = comp->gtNewLclLNode(rArgUse.ReplaceWithLclVar(comp), rArg->TypeGet()); | ||||||||||||||||||||
EgorBo marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
BlockRange().InsertBefore(call, lArgClone, rArgClone); | ||||||||||||||||||||
|
||||||||||||||||||||
// We're going to emit something like the following: | ||||||||||||||||||||
// | ||||||||||||||||||||
// bool result = ((*(int*)leftArg ^ *(int*)rightArg) | | ||||||||||||||||||||
// (*(int*)(leftArg + 1) ^ *((int*)(rightArg + 1)))) == 0; | ||||||||||||||||||||
// | ||||||||||||||||||||
// ^ in the given example we unroll for length=5 | ||||||||||||||||||||
// | ||||||||||||||||||||
// In IR: | ||||||||||||||||||||
// | ||||||||||||||||||||
// * EQ int | ||||||||||||||||||||
// +--* OR int | ||||||||||||||||||||
// | +--* XOR int | ||||||||||||||||||||
// | | +--* IND int | ||||||||||||||||||||
// | | | \--* LCL_VAR byref V1 | ||||||||||||||||||||
// | | \--* IND int | ||||||||||||||||||||
// | | \--* LCL_VAR byref V2 | ||||||||||||||||||||
// | \--* XOR int | ||||||||||||||||||||
// | +--* IND int | ||||||||||||||||||||
// | | \--* ADD byref | ||||||||||||||||||||
// | | +--* LCL_VAR byref V1 | ||||||||||||||||||||
// | | \--* CNS_INT int 1 | ||||||||||||||||||||
// | \--* IND int | ||||||||||||||||||||
// | \--* ADD byref | ||||||||||||||||||||
// | +--* LCL_VAR byref V2 | ||||||||||||||||||||
// | \--* CNS_INT int 1 | ||||||||||||||||||||
// \--* CNS_INT int 0 | ||||||||||||||||||||
// | ||||||||||||||||||||
GenTree* l1Indir = comp->gtNewIndir(loadType, lArgUse.Def()); | ||||||||||||||||||||
GenTree* r1Indir = comp->gtNewIndir(loadType, rArgUse.Def()); | ||||||||||||||||||||
GenTree* lXor = comp->gtNewOperNode(GT_XOR, actualLoadType, l1Indir, r1Indir); | ||||||||||||||||||||
GenTree* l2Offs = comp->gtNewIconNode(cnsSize - loadWidth); | ||||||||||||||||||||
EgorBo marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
GenTree* l2AddOffs = comp->gtNewOperNode(GT_ADD, lArg->TypeGet(), lArgClone, l2Offs); | ||||||||||||||||||||
jakobbotsch marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
GenTree* l2Indir = comp->gtNewIndir(loadType, l2AddOffs); | ||||||||||||||||||||
GenTree* r2Offs = comp->gtCloneExpr(l2Offs); // offset is the same | ||||||||||||||||||||
GenTree* r2AddOffs = comp->gtNewOperNode(GT_ADD, rArg->TypeGet(), rArgClone, r2Offs); | ||||||||||||||||||||
jakobbotsch marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
GenTree* r2Indir = comp->gtNewIndir(loadType, r2AddOffs); | ||||||||||||||||||||
GenTree* rXor = comp->gtNewOperNode(GT_XOR, actualLoadType, l2Indir, r2Indir); | ||||||||||||||||||||
GenTree* resultOr = comp->gtNewOperNode(GT_OR, actualLoadType, lXor, rXor); | ||||||||||||||||||||
GenTree* zeroCns = comp->gtNewIconNode(0, actualLoadType); | ||||||||||||||||||||
result = comp->gtNewOperNode(GT_EQ, TYP_INT, resultOr, zeroCns); | ||||||||||||||||||||
|
||||||||||||||||||||
BlockRange().InsertAfter(rArgClone, l1Indir, r1Indir, l2Offs, l2AddOffs); | ||||||||||||||||||||
BlockRange().InsertAfter(l2AddOffs, l2Indir, r2Offs, r2AddOffs, r2Indir); | ||||||||||||||||||||
BlockRange().InsertAfter(r2Indir, lXor, rXor, resultOr, zeroCns); | ||||||||||||||||||||
BlockRange().InsertAfter(zeroCns, result); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
JITDUMP("\nUnrolled to:\n"); | ||||||||||||||||||||
DISPTREE(result); | ||||||||||||||||||||
|
||||||||||||||||||||
LIR::Use use; | ||||||||||||||||||||
if (BlockRange().TryGetUse(call, &use)) | ||||||||||||||||||||
{ | ||||||||||||||||||||
use.ReplaceWith(result); | ||||||||||||||||||||
} | ||||||||||||||||||||
BlockRange().Remove(lengthArg); | ||||||||||||||||||||
BlockRange().Remove(call); | ||||||||||||||||||||
|
||||||||||||||||||||
// Remove all non-user args (e.g. r2r cell) | ||||||||||||||||||||
for (CallArg& arg : call->gtArgs.Args()) | ||||||||||||||||||||
{ | ||||||||||||||||||||
if (!arg.IsUserArg()) | ||||||||||||||||||||
{ | ||||||||||||||||||||
arg.GetNode()->SetUnusedValue(); | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
return lArg; | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
else | ||||||||||||||||||||
{ | ||||||||||||||||||||
JITDUMP("Size is either 0 or too big to unroll.\n") | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
else | ||||||||||||||||||||
{ | ||||||||||||||||||||
JITDUMP("size is not a constant.\n") | ||||||||||||||||||||
} | ||||||||||||||||||||
return nullptr; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// do lowering steps for a call | ||||||||||||||||||||
// this includes: | ||||||||||||||||||||
// - adding the placement nodes (either stack or register variety) for arguments | ||||||||||||||||||||
|
@@ -1883,19 +2062,26 @@ GenTree* Lowering::LowerCall(GenTree* node) | |||||||||||||||||||
// All runtime lookups are expected to be expanded in fgExpandRuntimeLookups | ||||||||||||||||||||
assert(!call->IsExpRuntimeLookup()); | ||||||||||||||||||||
|
||||||||||||||||||||
#if defined(TARGET_AMD64) || defined(TARGET_ARM64) | ||||||||||||||||||||
if (call->gtCallMoreFlags & GTF_CALL_M_SPECIAL_INTRINSIC) | ||||||||||||||||||||
{ | ||||||||||||||||||||
#if defined(TARGET_AMD64) || defined(TARGET_ARM64) | ||||||||||||||||||||
if (comp->lookupNamedIntrinsic(call->gtCallMethHnd) == NI_System_Buffer_Memmove) | ||||||||||||||||||||
GenTree* newNode = nullptr; | ||||||||||||||||||||
NamedIntrinsic ni = comp->lookupNamedIntrinsic(call->gtCallMethHnd); | ||||||||||||||||||||
if (ni == NI_System_Buffer_Memmove) | ||||||||||||||||||||
{ | ||||||||||||||||||||
GenTree* newNode = LowerCallMemmove(call); | ||||||||||||||||||||
if (newNode != nullptr) | ||||||||||||||||||||
{ | ||||||||||||||||||||
return newNode->gtNext; | ||||||||||||||||||||
} | ||||||||||||||||||||
newNode = LowerCallMemmove(call); | ||||||||||||||||||||
} | ||||||||||||||||||||
else if (ni == NI_System_SpanHelpers_SequenceEqual) | ||||||||||||||||||||
{ | ||||||||||||||||||||
newNode = LowerCallMemcmp(call); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
if (newNode != nullptr) | ||||||||||||||||||||
{ | ||||||||||||||||||||
return newNode->gtNext; | ||||||||||||||||||||
} | ||||||||||||||||||||
#endif | ||||||||||||||||||||
} | ||||||||||||||||||||
#endif | ||||||||||||||||||||
|
||||||||||||||||||||
call->ClearOtherRegs(); | ||||||||||||||||||||
LowerArgsForCall(call); | ||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is completely unrelated to the fix or changes being done, sorry... but has anyone ever tried reversing the
methodName
andclassName
tests for a performance hack in the JIT itself?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@IDisposable
lookupNamedIntrinsics
never show up in our JIT traces so we don't bother. This code is only executed for methods with[Intrinsic]
attribute so for 99% of methods it doesn't kick in.We could use here a Trie/binary search if it was a real problem