Skip to content

Commit 973ceee

Browse files
JIT: Intrinsify ClearWithoutReferences and Fill (#98700)
--------- Co-authored-by: SingleAccretion <62474226+SingleAccretion@users.noreply.github.com>
1 parent 99b7601 commit 973ceee

File tree

8 files changed

+211
-4
lines changed

8 files changed

+211
-4
lines changed

src/coreclr/jit/fgbasic.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,8 @@ void Compiler::fgFindJumpTargets(const BYTE* codeAddr, IL_OFFSET codeSize, Fixed
13291329
break;
13301330
}
13311331

1332+
case NI_System_SpanHelpers_ClearWithoutReferences:
1333+
case NI_System_SpanHelpers_Fill:
13321334
case NI_System_SpanHelpers_SequenceEqual:
13331335
case NI_System_Buffer_Memmove:
13341336
{

src/coreclr/jit/fgprofile.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -2544,6 +2544,9 @@ PhaseStatus Compiler::fgPrepareToInstrumentMethod()
25442544
case NI_System_MemoryExtensions_Equals:
25452545
case NI_System_MemoryExtensions_SequenceEqual:
25462546
case NI_System_MemoryExtensions_StartsWith:
2547+
case NI_System_SpanHelpers_Fill:
2548+
case NI_System_SpanHelpers_SequenceEqual:
2549+
case NI_System_SpanHelpers_ClearWithoutReferences:
25472550

25482551
// Same here, these are only folded when JIT knows the exact types
25492552
case NI_System_Type_IsAssignableFrom:

src/coreclr/jit/importercalls.cpp

+19
Original file line numberDiff line numberDiff line change
@@ -3982,6 +3982,7 @@ GenTree* Compiler::impIntrinsic(GenTree* newobjThis,
39823982

39833983
case NI_System_Text_UTF8Encoding_UTF8EncodingSealed_ReadUtf8:
39843984
case NI_System_SpanHelpers_SequenceEqual:
3985+
case NI_System_SpanHelpers_ClearWithoutReferences:
39853986
case NI_System_Buffer_Memmove:
39863987
{
39873988
if (sig->sigInst.methInstCount == 0)
@@ -3993,6 +3994,16 @@ GenTree* Compiler::impIntrinsic(GenTree* newobjThis,
39933994
break;
39943995
}
39953996

3997+
case NI_System_SpanHelpers_Fill:
3998+
{
3999+
if (sig->sigInst.methInstCount == 1)
4000+
{
4001+
// We'll try to unroll this in lower for constant input.
4002+
isSpecial = true;
4003+
}
4004+
break;
4005+
}
4006+
39964007
case NI_System_BitConverter_DoubleToInt64Bits:
39974008
{
39984009
GenTree* op1 = impStackTop().val;
@@ -9021,6 +9032,14 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method)
90219032
{
90229033
result = NI_System_SpanHelpers_SequenceEqual;
90239034
}
9035+
else if (strcmp(methodName, "Fill") == 0)
9036+
{
9037+
result = NI_System_SpanHelpers_Fill;
9038+
}
9039+
else if (strcmp(methodName, "ClearWithoutReferences") == 0)
9040+
{
9041+
result = NI_System_SpanHelpers_ClearWithoutReferences;
9042+
}
90249043
}
90259044
else if (strcmp(className, "String") == 0)
90269045
{

src/coreclr/jit/lower.cpp

+182-4
Original file line numberDiff line numberDiff line change
@@ -1842,6 +1842,157 @@ GenTree* Lowering::AddrGen(void* addr)
18421842
return AddrGen((ssize_t)addr);
18431843
}
18441844

1845+
// LowerCallMemset: Replaces the following memset-like special intrinsics:
1846+
//
1847+
// SpanHelpers.Fill<T>(ref dstRef, CNS_SIZE, CNS_VALUE)
1848+
// CORINFO_HELP_MEMSET(ref dstRef, CNS_VALUE, CNS_SIZE)
1849+
// SpanHelpers.ClearWithoutReferences(ref dstRef, CNS_SIZE)
1850+
//
1851+
// with a GT_STORE_BLK node:
1852+
//
1853+
// * STORE_BLK struct<CNS_SIZE> (init) (Unroll)
1854+
// +--* LCL_VAR byref dstRef
1855+
// \--* CNS_INT int 0
1856+
//
1857+
// Arguments:
1858+
// tree - GenTreeCall node to replace with STORE_BLK
1859+
// next - [out] Next node to lower if this function returns true
1860+
//
1861+
// Return Value:
1862+
// false if no changes were made
1863+
//
1864+
bool Lowering::LowerCallMemset(GenTreeCall* call, GenTree** next)
1865+
{
1866+
assert(call->IsSpecialIntrinsic(comp, NI_System_SpanHelpers_Fill) ||
1867+
call->IsSpecialIntrinsic(comp, NI_System_SpanHelpers_ClearWithoutReferences) ||
1868+
call->IsHelperCall(comp, CORINFO_HELP_MEMSET));
1869+
1870+
JITDUMP("Considering Memset-like call [%06d] for unrolling.. ", comp->dspTreeID(call))
1871+
1872+
if (comp->info.compHasNextCallRetAddr)
1873+
{
1874+
JITDUMP("compHasNextCallRetAddr=true so we won't be able to remove the call - bail out.\n");
1875+
return false;
1876+
}
1877+
1878+
GenTree* dstRefArg = call->gtArgs.GetUserArgByIndex(0)->GetNode();
1879+
GenTree* lengthArg;
1880+
GenTree* valueArg;
1881+
1882+
// Fill<T>'s length is not in bytes, so we need to scale it depending on the signature
1883+
unsigned lengthScale;
1884+
1885+
if (call->IsSpecialIntrinsic(comp, NI_System_SpanHelpers_Fill))
1886+
{
1887+
// void SpanHelpers::Fill<T>(ref T refData, nuint numElements, T value)
1888+
//
1889+
assert(call->gtArgs.CountUserArgs() == 3);
1890+
lengthArg = call->gtArgs.GetUserArgByIndex(1)->GetNode();
1891+
CallArg* valueCallArg = call->gtArgs.GetUserArgByIndex(2);
1892+
valueArg = valueCallArg->GetNode();
1893+
1894+
// Get that <T> from the signature
1895+
lengthScale = genTypeSize(valueCallArg->GetSignatureType());
1896+
// NOTE: structs and TYP_REF will be ignored by the "Value is not a constant" check
1897+
// Some of those cases can be enabled in future, e.g. s
1898+
}
1899+
else if (call->IsHelperCall(comp, CORINFO_HELP_MEMSET))
1900+
{
1901+
// void CORINFO_HELP_MEMSET(ref T refData, byte value, nuint numElements)
1902+
//
1903+
assert(call->gtArgs.CountUserArgs() == 3);
1904+
lengthArg = call->gtArgs.GetUserArgByIndex(2)->GetNode();
1905+
valueArg = call->gtArgs.GetUserArgByIndex(1)->GetNode();
1906+
lengthScale = 1; // it's always in bytes
1907+
}
1908+
else
1909+
{
1910+
// void SpanHelpers::ClearWithoutReferences(ref byte b, nuint byteLength)
1911+
//
1912+
assert(call->IsSpecialIntrinsic(comp, NI_System_SpanHelpers_ClearWithoutReferences));
1913+
assert(call->gtArgs.CountUserArgs() == 2);
1914+
1915+
// Simple zeroing
1916+
lengthArg = call->gtArgs.GetUserArgByIndex(1)->GetNode();
1917+
valueArg = comp->gtNewZeroConNode(TYP_INT);
1918+
lengthScale = 1; // it's always in bytes
1919+
}
1920+
1921+
if (!lengthArg->IsIntegralConst())
1922+
{
1923+
JITDUMP("Length is not a constant - bail out.\n");
1924+
return false;
1925+
}
1926+
1927+
if (!valueArg->IsCnsIntOrI() || !valueArg->TypeIs(TYP_INT))
1928+
{
1929+
JITDUMP("Value is not a constant - bail out.\n");
1930+
return false;
1931+
}
1932+
1933+
// If value is not zero, we can only unroll for single-byte values
1934+
if (!valueArg->IsIntegralConst(0) && (lengthScale != 1))
1935+
{
1936+
JITDUMP("Value is not unroll-friendly - bail out.\n");
1937+
return false;
1938+
}
1939+
1940+
// Convert lenCns to bytes
1941+
ssize_t lenCns = lengthArg->AsIntCon()->IconValue();
1942+
if (CheckedOps::MulOverflows((target_ssize_t)lenCns, (target_ssize_t)lengthScale, CheckedOps::Signed))
1943+
{
1944+
// lenCns overflows
1945+
JITDUMP("lenCns * lengthScale overflows - bail out.\n")
1946+
return false;
1947+
}
1948+
lenCns *= (ssize_t)lengthScale;
1949+
1950+
// TODO-CQ: drop the whole thing in case of lenCns = 0
1951+
if ((lenCns <= 0) || (lenCns > (ssize_t)comp->getUnrollThreshold(Compiler::UnrollKind::Memset)))
1952+
{
1953+
JITDUMP("Size is either 0 or too big to unroll - bail out.\n")
1954+
return false;
1955+
}
1956+
1957+
JITDUMP("Accepted for unrolling!\nOld tree:\n");
1958+
DISPTREERANGE(BlockRange(), call);
1959+
1960+
if (!valueArg->IsIntegralConst(0))
1961+
{
1962+
// Non-zero (byte) value, wrap value with GT_INIT_VAL
1963+
GenTree* initVal = valueArg;
1964+
valueArg = comp->gtNewOperNode(GT_INIT_VAL, TYP_INT, initVal);
1965+
BlockRange().InsertAfter(initVal, valueArg);
1966+
}
1967+
1968+
GenTreeBlk* storeBlk =
1969+
comp->gtNewStoreBlkNode(comp->typGetBlkLayout((unsigned)lenCns), dstRefArg, valueArg, GTF_IND_UNALIGNED);
1970+
storeBlk->gtBlkOpKind = GenTreeBlk::BlkOpKindUnroll;
1971+
1972+
// Insert/Remove trees into LIR
1973+
BlockRange().InsertBefore(call, storeBlk);
1974+
if (call->IsSpecialIntrinsic(comp, NI_System_SpanHelpers_ClearWithoutReferences))
1975+
{
1976+
// Value didn't exist in LIR previously
1977+
BlockRange().InsertBefore(storeBlk, valueArg);
1978+
}
1979+
1980+
// Remove the call and mark everything as unused ...
1981+
BlockRange().Remove(call, true);
1982+
// ... except the args we're going to re-use
1983+
dstRefArg->ClearUnusedValue();
1984+
valueArg->ClearUnusedValue();
1985+
if (valueArg->OperIs(GT_INIT_VAL))
1986+
{
1987+
valueArg->gtGetOp1()->ClearUnusedValue();
1988+
}
1989+
1990+
JITDUMP("\nNew tree:\n");
1991+
DISPTREERANGE(BlockRange(), storeBlk);
1992+
*next = storeBlk;
1993+
return true;
1994+
}
1995+
18451996
//------------------------------------------------------------------------
18461997
// LowerCallMemmove: Replace Buffer.Memmove(DST, SRC, CNS_SIZE) with a GT_STORE_BLK:
18471998
// Do the same for CORINFO_HELP_MEMCPY(DST, SRC, CNS_SIZE)
@@ -2221,11 +2372,32 @@ GenTree* Lowering::LowerCall(GenTree* node)
22212372
GenTree* nextNode = nullptr;
22222373
if (call->gtCallMoreFlags & GTF_CALL_M_SPECIAL_INTRINSIC)
22232374
{
2224-
NamedIntrinsic ni = comp->lookupNamedIntrinsic(call->gtCallMethHnd);
2225-
if (((ni == NI_System_Buffer_Memmove) && LowerCallMemmove(call, &nextNode)) ||
2226-
((ni == NI_System_SpanHelpers_SequenceEqual) && LowerCallMemcmp(call, &nextNode)))
2375+
switch (comp->lookupNamedIntrinsic(call->gtCallMethHnd))
22272376
{
2228-
return nextNode;
2377+
case NI_System_Buffer_Memmove:
2378+
if (LowerCallMemmove(call, &nextNode))
2379+
{
2380+
return nextNode;
2381+
}
2382+
break;
2383+
2384+
case NI_System_SpanHelpers_SequenceEqual:
2385+
if (LowerCallMemcmp(call, &nextNode))
2386+
{
2387+
return nextNode;
2388+
}
2389+
break;
2390+
2391+
case NI_System_SpanHelpers_Fill:
2392+
case NI_System_SpanHelpers_ClearWithoutReferences:
2393+
if (LowerCallMemset(call, &nextNode))
2394+
{
2395+
return nextNode;
2396+
}
2397+
break;
2398+
2399+
default:
2400+
break;
22292401
}
22302402
}
22312403

@@ -2234,6 +2406,12 @@ GenTree* Lowering::LowerCall(GenTree* node)
22342406
{
22352407
return nextNode;
22362408
}
2409+
2410+
// Try to lower CORINFO_HELP_MEMSET to unrollable STORE_BLK
2411+
if (call->IsHelperCall(comp, CORINFO_HELP_MEMSET) && LowerCallMemset(call, &nextNode))
2412+
{
2413+
return nextNode;
2414+
}
22372415
#endif
22382416

22392417
call->ClearOtherRegs();

src/coreclr/jit/lower.h

+1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ class Lowering final : public Phase
140140
GenTree* LowerCall(GenTree* call);
141141
bool LowerCallMemmove(GenTreeCall* call, GenTree** next);
142142
bool LowerCallMemcmp(GenTreeCall* call, GenTree** next);
143+
bool LowerCallMemset(GenTreeCall* call, GenTree** next);
143144
void LowerCFGCall(GenTreeCall* call);
144145
void MoveCFGCallArgs(GenTreeCall* call);
145146
void MoveCFGCallArg(GenTreeCall* call, GenTree* node);

src/coreclr/jit/namedintrinsiclist.h

+2
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ enum NamedIntrinsic : unsigned short
118118
NI_System_String_EndsWith,
119119
NI_System_Span_get_Item,
120120
NI_System_Span_get_Length,
121+
NI_System_SpanHelpers_ClearWithoutReferences,
122+
NI_System_SpanHelpers_Fill,
121123
NI_System_SpanHelpers_SequenceEqual,
122124
NI_System_ReadOnlySpan_get_Item,
123125
NI_System_ReadOnlySpan_get_Length,

src/libraries/System.Private.CoreLib/src/System/SpanHelpers.T.cs

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ namespace System
1616
{
1717
internal static partial class SpanHelpers // .T
1818
{
19+
[Intrinsic] // Unrolled for small sizes
1920
public static unsafe void Fill<T>(ref T refData, nuint numElements, T value)
2021
{
2122
// Early checks to see if it's even possible to vectorize - JIT will turn these checks into consts.

src/libraries/System.Private.CoreLib/src/System/SpanHelpers.cs

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace System
1212
{
1313
internal static partial class SpanHelpers
1414
{
15+
[Intrinsic] // Unrolled for small sizes
1516
public static unsafe void ClearWithoutReferences(ref byte b, nuint byteLength)
1617
{
1718
if (byteLength == 0)

0 commit comments

Comments
 (0)