Skip to content

Commit 086be91

Browse files
EgorBoCopilot
andcommitted
[Experiment] intrinsify Span.Slice(int) / ReadOnlySpan.Slice(int)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f595fa3 commit 086be91

7 files changed

Lines changed: 194 additions & 6 deletions

File tree

src/coreclr/jit/fgbasic.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1594,8 +1594,13 @@ void Compiler::fgFindJumpTargets(const BYTE* codeAddr, IL_OFFSET codeSize, Fixed
15941594
else if (ni != NI_Illegal)
15951595
{
15961596
// Otherwise note "intrinsic" (most likely will be lowered as single instructions)
1597-
// except Math where only a few intrinsics won't end up as normal calls
1598-
if (!IsMathIntrinsic(ni) || IsTargetIntrinsic(ni))
1597+
// except Math where only a few intrinsics won't end up as normal calls.
1598+
// Span.Slice(int) / ReadOnlySpan.Slice(int) are tagged [Intrinsic] only so the
1599+
// JIT can replace their managed bodies with a GT_BOUNDS_CHECK + byref form;
1600+
// they are not lowered to a single instruction, so excluding them here keeps
1601+
// the caller's inlining profitability identical to the pre-[Intrinsic] world.
1602+
if ((!IsMathIntrinsic(ni) || IsTargetIntrinsic(ni)) && (ni != NI_System_Span_Slice) &&
1603+
(ni != NI_System_ReadOnlySpan_Slice))
15991604
{
16001605
compInlineResult->Note(InlineObservation::CALLEE_INTRINSIC);
16011606
}

src/coreclr/jit/importercalls.cpp

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3447,8 +3447,10 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd,
34473447
case NI_System_String_get_Length:
34483448
case NI_System_Span_get_Item:
34493449
case NI_System_Span_get_Length:
3450+
case NI_System_Span_Slice:
34503451
case NI_System_ReadOnlySpan_get_Item:
34513452
case NI_System_ReadOnlySpan_get_Length:
3453+
case NI_System_ReadOnlySpan_Slice:
34523454
case NI_System_BitConverter_DoubleToInt64Bits:
34533455
case NI_System_BitConverter_Int32BitsToSingle:
34543456
case NI_System_BitConverter_Int64BitsToDouble:
@@ -3885,6 +3887,132 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd,
38853887
break;
38863888
}
38873889

3890+
case NI_System_Span_Slice:
3891+
case NI_System_ReadOnlySpan_Slice:
3892+
{
3893+
optMethodFlags |= OMF_HAS_ARRAYREF;
3894+
3895+
// Have start, stack pointer-to Span<T> s on the stack. Expand to:
3896+
//
3897+
// For Span<T>
3898+
// BoundsCheck(start, s->_length + 1) // ArgumentOutOfRange on failure
3899+
// tmp._reference = s->_reference + (nuint)(uint)start * sizeof(T)
3900+
// tmp._length = s->_length - start
3901+
// tmp
3902+
//
3903+
// For ReadOnlySpan<T> -- same expansion, the only difference is the result type.
3904+
//
3905+
// Signature should show one class type parameter, which
3906+
// we need to examine.
3907+
assert(sig->sigInst.classInstCount == 1);
3908+
assert(sig->numArgs == 1);
3909+
CORINFO_CLASS_HANDLE spanElemHnd = sig->sigInst.classInst[0];
3910+
const unsigned elemSize = info.compCompHnd->getClassSize(spanElemHnd);
3911+
assert(elemSize > 0);
3912+
3913+
const bool isReadOnly = (ni == NI_System_ReadOnlySpan_Slice);
3914+
3915+
JITDUMP("\nimpIntrinsic: Expanding %sSpan<T>.Slice(int), T=%s, sizeof(T)=%u\n",
3916+
isReadOnly ? "ReadOnly" : "", eeGetClassName(spanElemHnd), elemSize);
3917+
3918+
GenTree* start = impPopStack().val;
3919+
GenTree* ptrToSpan = impPopStack().val;
3920+
GenTree* startClone = nullptr;
3921+
GenTree* ptrToSpanClone = nullptr;
3922+
assert(genActualType(start) == TYP_INT);
3923+
assert(ptrToSpan->TypeIs(TYP_BYREF, TYP_I_IMPL));
3924+
3925+
#if defined(DEBUG)
3926+
if (verbose)
3927+
{
3928+
printf("with ptr-to-span\n");
3929+
gtDispTree(ptrToSpan);
3930+
printf("and start\n");
3931+
gtDispTree(start);
3932+
}
3933+
#endif // defined(DEBUG)
3934+
3935+
// We need 'start' three times (bounds check, byref offset, new length),
3936+
// so spill it once and clone the resulting LclVar reads as needed.
3937+
start = impCloneExpr(start, &startClone, CHECK_SPILL_ALL, nullptr DEBUGARG("Span.Slice start"));
3938+
GenTree* startClone2 = gtCloneExpr(startClone);
3939+
3940+
if (impIsAddressInLocal(ptrToSpan))
3941+
{
3942+
ptrToSpanClone = gtCloneExpr(ptrToSpan);
3943+
}
3944+
else
3945+
{
3946+
ptrToSpan = impCloneExpr(ptrToSpan, &ptrToSpanClone, CHECK_SPILL_ALL,
3947+
nullptr DEBUGARG("Span.Slice ptrToSpan"));
3948+
}
3949+
3950+
// Read input length.
3951+
CORINFO_FIELD_HANDLE lengthHnd = info.compCompHnd->getFieldInClass(clsHnd, 1);
3952+
const unsigned lengthOffset = info.compCompHnd->getFieldOffset(lengthHnd);
3953+
3954+
GenTreeFieldAddr* lengthFieldAddr = gtNewFieldAddrNode(lengthHnd, ptrToSpan, lengthOffset);
3955+
GenTree* length = gtNewIndir(TYP_INT, lengthFieldAddr);
3956+
lengthFieldAddr->SetIsSpanLength(true);
3957+
3958+
// Length is needed twice: once for the bounds check (as length + 1)
3959+
// and once for the new length (length - start).
3960+
GenTree* lengthClone = nullptr;
3961+
length = impCloneExpr(length, &lengthClone, CHECK_SPILL_ALL, nullptr DEBUGARG("Span.Slice length"));
3962+
3963+
// Bounds check: throw ArgumentOutOfRangeException if (uint)start > (uint)length,
3964+
// which is equivalent to (uint)start >= (uint)(length + 1).
3965+
//
3966+
// Use TYP_INT (not widened to TYP_LONG) so that downstream range-check
3967+
// elimination (rangecheck.cpp) can actually reason about the bound -- it bails on
3968+
// TYP_LONG. The (length + 1) addition is in TYP_INT and would wrap to int.MinValue
3969+
// when _length == int.MaxValue; codegen still does the comparison unsigned and
3970+
// produces the correct result, but assertion-prop / range-check passes lose some
3971+
// information at that boundary (a Span<byte> with int.MaxValue elements is the
3972+
// only case that could be affected).
3973+
GenTree* boundInt = gtNewOperNode(GT_ADD, TYP_INT, length, gtNewIconNode(1));
3974+
GenTree* boundsCheck =
3975+
new (this, GT_BOUNDS_CHECK) GenTreeBoundsChk(start, boundInt, SCK_ARG_RNG_EXCPN);
3976+
3977+
// Read input reference.
3978+
CORINFO_FIELD_HANDLE ptrHnd = info.compCompHnd->getFieldInClass(clsHnd, 0);
3979+
const unsigned ptrOffset = info.compCompHnd->getFieldOffset(ptrHnd);
3980+
GenTreeFieldAddr* dataFieldAddr = gtNewFieldAddrNode(ptrHnd, ptrToSpanClone, ptrOffset);
3981+
GenTree* oldRef = gtNewIndir(TYP_BYREF, dataFieldAddr);
3982+
3983+
// newRef = oldRef + (nuint)(uint)start * sizeof(T)
3984+
GenTree* offset = impImplicitIorI4Cast(startClone, TYP_I_IMPL, /* zeroExtend */ true);
3985+
if (elemSize != 1)
3986+
{
3987+
GenTree* sizeofNode = gtNewIconNode(static_cast<ssize_t>(elemSize), TYP_I_IMPL);
3988+
offset = gtNewOperNode(GT_MUL, TYP_I_IMPL, offset, sizeofNode);
3989+
}
3990+
GenTree* newRef = gtNewOperNode(GT_ADD, TYP_BYREF, oldRef, offset);
3991+
3992+
// newLength = length - start
3993+
GenTree* newLength = gtNewOperNode(GT_SUB, TYP_INT, lengthClone, startClone2);
3994+
3995+
// Allocate a temp local for the result Span and initialize its fields.
3996+
CORINFO_CLASS_HANDLE retSpanHnd = sig->retTypeClass;
3997+
unsigned spanTempNum = lvaGrabTemp(true DEBUGARG("Span<T> for Slice"));
3998+
lvaSetStruct(spanTempNum, retSpanHnd, false);
3999+
4000+
GenTree* lengthFieldStore =
4001+
gtNewStoreLclFldNode(spanTempNum, TYP_INT, OFFSETOF__CORINFO_Span__length, newLength);
4002+
GenTree* dataFieldStore =
4003+
gtNewStoreLclFldNode(spanTempNum, TYP_BYREF, OFFSETOF__CORINFO_Span__reference, newRef);
4004+
4005+
// Statements are appended in order, and GT_BOUNDS_CHECK carries GTF_EXCEPT, so the
4006+
// check executes before any of the temp-span field stores. Match the field-store
4007+
// order used by impCreateSpanIntrinsic (length, then reference).
4008+
impAppendTree(boundsCheck, CHECK_SPILL_NONE, impCurStmtDI);
4009+
impAppendTree(lengthFieldStore, CHECK_SPILL_NONE, impCurStmtDI);
4010+
impAppendTree(dataFieldStore, CHECK_SPILL_NONE, impCurStmtDI);
4011+
4012+
retNode = impCreateLocalNode(spanTempNum DEBUGARG(0));
4013+
break;
4014+
}
4015+
38884016
case NI_System_Span_get_Length:
38894017
case NI_System_ReadOnlySpan_get_Length:
38904018
{
@@ -10672,6 +10800,18 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method)
1067210800
{
1067310801
result = NI_System_ReadOnlySpan_get_Length;
1067410802
}
10803+
else if (strcmp(methodName, "Slice") == 0)
10804+
{
10805+
// Only the single-argument Slice(int) overload is intrinsic; the
10806+
// two-argument Slice(int, int) overload shares the same method name
10807+
// but is not [Intrinsic] and must not be matched here.
10808+
CORINFO_SIG_INFO sliceSig;
10809+
info.compCompHnd->getMethodSig(method, &sliceSig);
10810+
if (sliceSig.numArgs == 1)
10811+
{
10812+
result = NI_System_ReadOnlySpan_Slice;
10813+
}
10814+
}
1067510815
}
1067610816
else if (strcmp(className, "RuntimeType") == 0)
1067710817
{
@@ -10710,6 +10850,18 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method)
1071010850
{
1071110851
result = NI_System_Span_get_Length;
1071210852
}
10853+
else if (strcmp(methodName, "Slice") == 0)
10854+
{
10855+
// Only the single-argument Slice(int) overload is intrinsic; the
10856+
// two-argument Slice(int, int) overload shares the same method name
10857+
// but is not [Intrinsic] and must not be matched here.
10858+
CORINFO_SIG_INFO sliceSig;
10859+
info.compCompHnd->getMethodSig(method, &sliceSig);
10860+
if (sliceSig.numArgs == 1)
10861+
{
10862+
result = NI_System_Span_Slice;
10863+
}
10864+
}
1071310865
}
1071410866
else if (strcmp(className, "SpanHelpers") == 0)
1071510867
{

src/coreclr/jit/namedintrinsiclist.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,13 @@ enum NamedIntrinsic : unsigned short
147147
NI_System_String_EndsWith,
148148
NI_System_Span_get_Item,
149149
NI_System_Span_get_Length,
150+
NI_System_Span_Slice,
150151
NI_System_SpanHelpers_ClearWithoutReferences,
151152
NI_System_SpanHelpers_Fill,
152153
NI_System_SpanHelpers_SequenceEqual,
153154
NI_System_ReadOnlySpan_get_Item,
154155
NI_System_ReadOnlySpan_get_Length,
156+
NI_System_ReadOnlySpan_Slice,
155157

156158
NI_System_MemoryExtensions_AsSpan,
157159
NI_System_MemoryExtensions_Equals,

src/coreclr/jit/rangecheck.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,6 +1232,25 @@ void RangeCheck::MergeEdgeAssertionsWorker(Compiler* comp
12321232
{
12331233
cmpOper = Compiler::AssertionDsc::ToCompareOper(curAssertion.GetKind(), &isUnsigned);
12341234
limit = Limit(Limit::keBinOpArray, boundVN, boundCns);
1235+
1236+
// If the assertion's bound VN differs from preferredBoundVN but they are
1237+
// related by a small constant, normalize the limit to use preferredBoundVN.
1238+
// This commonly arises with the Span.Slice(int) intrinsic, which emits
1239+
// BOUNDS_CHECK(start, length + 1) -- so preferredBoundVN is "length + 1"
1240+
// while loop assertions decompose against "length" itself.
1241+
// Without normalization, TightenLimit's "prefer preferredBound" heuristic
1242+
// would pick a looser non-asserted limit over this tighter assertion.
1243+
ValueNum addOpVN;
1244+
int addOpCns;
1245+
if ((boundVN != preferredBoundVN) && (preferredBoundVN != ValueNumStore::NoVN) &&
1246+
(boundCns > INT32_MIN) &&
1247+
comp->vnStore->IsVNBinFuncWithConst(preferredBoundVN, VNF_ADD, &addOpVN, &addOpCns) &&
1248+
(addOpVN == boundVN) && (addOpCns == 1))
1249+
{
1250+
// preferredBoundVN = boundVN + 1, so "boundVN + boundCns" is equivalent to
1251+
// "preferredBoundVN + (boundCns - 1)".
1252+
limit = Limit(Limit::keBinOpArray, preferredBoundVN, boundCns - 1);
1253+
}
12351254
}
12361255
else if (normalLclVNMatchesOp2)
12371256
{

src/coreclr/jit/valuenum.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13348,6 +13348,18 @@ void Compiler::fgValueNumberTree(GenTree* tree)
1334813348
if ((lengthVN != ValueNumStore::NoVN) && !vnStore->IsVNConstant(lengthVN))
1334913349
{
1335013350
vnStore->SetVNIsCheckedBound(lengthVN);
13351+
13352+
// If the bound is "X + 1" (e.g. emitted by the Span.Slice(int) intrinsic to encode
13353+
// "throw if start > X" as BOUNDS_CHECK(start, X + 1)), also register X as a checked
13354+
// bound so the assertion creator decomposes "(i + k) <= X - m" against X rather than
13355+
// falling back to an opaque RelopVN that range-check elimination cannot see through.
13356+
ValueNum innerVN;
13357+
int addCns;
13358+
if (vnStore->IsVNBinFuncWithConst(lengthVN, VNF_ADD, &innerVN, &addCns) &&
13359+
(addCns == 1) && !vnStore->IsVNConstant(innerVN))
13360+
{
13361+
vnStore->SetVNIsCheckedBound(innerVN);
13362+
}
1335113363
}
1335213364
}
1335313365
break;

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,6 @@ public ReadOnlySpan(ref readonly T reference)
125125
[MethodImpl(MethodImplOptions.AggressiveInlining)]
126126
internal ReadOnlySpan(ref T reference, int length)
127127
{
128-
Debug.Assert(length >= 0);
129-
130128
_reference = ref reference;
131129
_length = length;
132130
}
@@ -369,6 +367,7 @@ public override string ToString()
369367
/// Thrown when the specified <paramref name="start"/> index is not in range (&lt;0 or &gt;Length).
370368
/// </exception>
371369
[MethodImpl(MethodImplOptions.AggressiveInlining)]
370+
[Intrinsic]
372371
public ReadOnlySpan<T> Slice(int start)
373372
{
374373
if ((uint)start > (uint)_length)

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,6 @@ public Span(ref T reference)
130130
[MethodImpl(MethodImplOptions.AggressiveInlining)]
131131
internal Span(ref T reference, int length)
132132
{
133-
Debug.Assert(length >= 0);
134-
135133
_reference = ref reference;
136134
_length = length;
137135
}
@@ -393,6 +391,7 @@ public override string ToString()
393391
/// Thrown when the specified <paramref name="start"/> index is not in range (&lt;0 or &gt;Length).
394392
/// </exception>
395393
[MethodImpl(MethodImplOptions.AggressiveInlining)]
394+
[Intrinsic]
396395
public Span<T> Slice(int start)
397396
{
398397
if ((uint)start > (uint)_length)

0 commit comments

Comments
 (0)