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
26 changes: 26 additions & 0 deletions src/coreclr/inc/corinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -3210,6 +3210,32 @@ class ICorStaticInfo
// Returns the primitive type for passing/returning a Wasm struct by value,
// or CORINFO_WASM_TYPE_VOID if passing/returning must be by reference.
virtual CorInfoWasmType getWasmLowering(CORINFO_CLASS_HANDLE structHnd) = 0;

//------------------------------------------------------------------------------
// tryAppendStrings: try to concatenate the given frozen-string handles into a
// single new frozen string and return its handle. This is a best-effort
// helper used by the JIT to constant-fold String.Concat calls whose
// arguments are known constant strings.
//
// Arguments:
// strings - array of CORINFO_OBJECT_HANDLEs that must reference frozen
// System.String objects. May contain handles to the empty
// string. Must not be null when count > 0.
// count - number of entries in 'strings'. Must be > 0.
//
// Return Value:
// A handle to a frozen System.String object containing the ordered
// concatenation of the input strings, or nullptr if the runtime could
// not produce one (e.g. the result wouldn't fit on the frozen heap, or
// the inputs are not really strings). The returned handle is suitable
// for use with gtNewIconEmbObjHndNode (i.e. IAT_VALUE-style direct
// object reference). The returned handle has the same lifetime as a
// handle returned by constructStringLiteral.
//
virtual CORINFO_OBJECT_HANDLE tryAppendStrings (
CORINFO_OBJECT_HANDLE* strings, /* IN */
int count /* IN */
) = 0;
};

/*****************************************************************************
Expand Down
4 changes: 4 additions & 0 deletions src/coreclr/inc/icorjitinfoimpl_generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,10 @@ bool getStringChar(
CORINFO_CLASS_HANDLE getObjectType(
CORINFO_OBJECT_HANDLE objPtr) override;

CORINFO_OBJECT_HANDLE tryAppendStrings(
CORINFO_OBJECT_HANDLE* strings,
int count) override;

bool getReadyToRunHelper(
CORINFO_RESOLVED_TOKEN* pResolvedToken,
CorInfoHelpFunc id,
Expand Down
10 changes: 5 additions & 5 deletions src/coreclr/inc/jiteeversionguid.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@

#include <minipal/guid.h>

constexpr GUID JITEEVersionIdentifier = { /* 1516acb8-ac41-4dcb-9840-f39ee25ffa73 */
0x1516acb8,
0xac41,
0x4dcb,
{0x98, 0x40, 0xf3, 0x9e, 0xe2, 0x5f, 0xfa, 0x73}
constexpr GUID JITEEVersionIdentifier = { /* 875cb5c1-fef8-4a79-acfb-b4f531c632a4 */
0x875cb5c1,
0xfef8,
0x4a79,
{0xac, 0xfb, 0xb4, 0xf5, 0x31, 0xc6, 0x32, 0xa4}
};

#endif // JIT_EE_VERSIONING_GUID_H
1 change: 1 addition & 0 deletions src/coreclr/jit/ICorJitInfo_names_generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ DEF_CLR_API(getRuntimeTypePointer)
DEF_CLR_API(isObjectImmutable)
DEF_CLR_API(getStringChar)
DEF_CLR_API(getObjectType)
DEF_CLR_API(tryAppendStrings)
DEF_CLR_API(getReadyToRunHelper)
DEF_CLR_API(getReadyToRunDelegateCtorHelper)
DEF_CLR_API(initClass)
Expand Down
10 changes: 10 additions & 0 deletions src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,16 @@ CORINFO_CLASS_HANDLE WrapICorJitInfo::getObjectType(
return temp;
}

CORINFO_OBJECT_HANDLE WrapICorJitInfo::tryAppendStrings(
CORINFO_OBJECT_HANDLE* strings,
int count)
{
API_ENTER(tryAppendStrings);
CORINFO_OBJECT_HANDLE temp = wrapHnd->tryAppendStrings(strings, count);
API_LEAVE(tryAppendStrings);
return temp;
}

bool WrapICorJitInfo::getReadyToRunHelper(
CORINFO_RESOLVED_TOKEN* pResolvedToken,
CorInfoHelpFunc id,
Expand Down
59 changes: 59 additions & 0 deletions src/coreclr/jit/assertionprop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2656,6 +2656,60 @@ GenTree* Compiler::optVNBasedFoldExpr_Call_Memmove(GenTreeCall* call)
return result;
}

//------------------------------------------------------------------------------
// optVNBasedFoldExpr_Call_StringConcat: Folds NI_System_String_Concat into a
// single frozen-string handle when every argument's value number resolves
// to a constant frozen-string handle.
//
// Arguments:
// block - The block containing the call.
// call - The String.Concat call to fold.
//
// Return Value:
// A new tree representing the concatenated frozen string, or nullptr if the
// call cannot be folded.
//
GenTree* Compiler::optVNBasedFoldExpr_Call_StringConcat(BasicBlock* block, GenTreeCall* call)
{
// Don't waste frozen-heap budget on cold code.
if ((block != nullptr) && block->isRunRarely())
{
return nullptr;
}

unsigned const argCount = call->gtArgs.CountUserArgs();
if ((argCount < 2) || (argCount > 4))
{
return nullptr;
}

CORINFO_OBJECT_HANDLE handles[4] = {nullptr, nullptr, nullptr, nullptr};
for (unsigned i = 0; i < argCount; i++)
{
GenTree* argNode = call->gtArgs.GetUserArgByIndex(i)->GetNode();
ValueNum vn = vnStore->VNConservativeNormalValue(argNode->gtVNPair);
if (!vnStore->IsVNObjHandle(vn))
{
return nullptr;
}

handles[i] = vnStore->ConstantObjHandle(vn);
if (handles[i] == nullptr)
{
return nullptr;
}
}

CORINFO_OBJECT_HANDLE folded = info.compCompHnd->tryAppendStrings(handles, (int)argCount);
if (folded == nullptr)
{
return nullptr;
}

JITDUMP("VN-fold: String.Concat of %u constant frozen strings -> single frozen string handle\n", argCount);
return gtNewIconEmbObjHndNode(folded);
}

//------------------------------------------------------------------------------
// optVNBasedFoldExpr_Call: Folds given call using VN to a simpler tree.
//
Expand Down Expand Up @@ -2738,6 +2792,11 @@ GenTree* Compiler::optVNBasedFoldExpr_Call(BasicBlock* block, GenTree* parent, G
return optVNBasedFoldExpr_Call_Memcmp(call);
}

if (call->IsSpecialIntrinsic(this, NI_System_String_Concat))
{
return optVNBasedFoldExpr_Call_StringConcat(block, call);
}

return nullptr;
}

Expand Down
1 change: 1 addition & 0 deletions src/coreclr/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -8776,6 +8776,7 @@ class Compiler
GenTree* optVNBasedFoldExpr_Call_Memmove(GenTreeCall* call);
GenTree* optVNBasedFoldExpr_Call_Memset(GenTreeCall* call);
GenTree* optVNBasedFoldExpr_Call_Memcmp(GenTreeCall* call);
GenTree* optVNBasedFoldExpr_Call_StringConcat(BasicBlock* block, GenTreeCall* call);

AssertionIndex GetAssertionCount()
{
Expand Down
6 changes: 4 additions & 2 deletions src/coreclr/jit/fgbasic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1594,8 +1594,10 @@ void Compiler::fgFindJumpTargets(const BYTE* codeAddr, IL_OFFSET codeSize, Fixed
else if (ni != NI_Illegal)
{
// Otherwise note "intrinsic" (most likely will be lowered as single instructions)
// except Math where only a few intrinsics won't end up as normal calls
if (!IsMathIntrinsic(ni) || IsTargetIntrinsic(ni))
// except Math where only a few intrinsics won't end up as normal calls.
// String.Concat is also excluded: when its arguments aren't constants the
// call survives as a non-trivial method call and shouldn't bias the inliner.
if ((!IsMathIntrinsic(ni) || IsTargetIntrinsic(ni)) && (ni != NI_System_String_Concat))
{
compInlineResult->Note(InlineObservation::CALLEE_INTRINSIC);
}
Expand Down
62 changes: 62 additions & 0 deletions src/coreclr/jit/gentree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15162,6 +15162,68 @@ GenTree* Compiler::gtFoldExprCall(GenTreeCall* call)
break;
}

case NI_System_String_Concat:
{
// Try to fold String.Concat(<cns>, <cns> [, <cns> [, <cns>]]) into a
// single frozen-string handle. We only see this case when the
// importer marked the call as a special intrinsic, which requires
// every argument to be a GT_CNS_STR.
Comment on lines +15167 to +15170
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

The comment says the importer marking the call as a special intrinsic "requires every argument to be a GT_CNS_STR", but impIntrinsic marks the string-only overloads as special regardless of whether the args are constant; the folding logic below is what requires GT_CNS_STR. Consider adjusting the comment to avoid implying stronger invariants than actually enforced.

Copilot uses AI. Check for mistakes.
if ((compCurBB != nullptr) && compCurBB->isRunRarely())
{
// Don't waste frozen-heap budget on cold code.
break;
}

unsigned const argCount = call->gtArgs.CountUserArgs();
assert((argCount >= 2) && (argCount <= 4));

CORINFO_OBJECT_HANDLE handles[4] = {nullptr, nullptr, nullptr, nullptr};
bool allCns = true;
for (unsigned i = 0; i < argCount; i++)
{
GenTree* argNode = call->gtArgs.GetUserArgByIndex(i)->GetNode();
if (!argNode->OperIs(GT_CNS_STR))
{
allCns = false;
break;
}

GenTreeStrCon* strCon = argNode->AsStrCon();
LPVOID pValue = nullptr;
InfoAccessType iat;
if (strCon->IsStringEmptyField())
{
iat = info.compCompHnd->emptyStringLiteral(&pValue);
}
else
{
iat = info.compCompHnd->constructStringLiteral(strCon->gtScpHnd, strCon->gtSconCPX, &pValue);
}

if (iat != IAT_VALUE)
{
// The string is not directly addressable as a frozen handle
// (e.g. it's a lazily-allocated literal). Bail.
allCns = false;
break;
}

handles[i] = (CORINFO_OBJECT_HANDLE)pValue;
}

if (allCns)
{
CORINFO_OBJECT_HANDLE folded = info.compCompHnd->tryAppendStrings(handles, (int)argCount);
if (folded != nullptr)
{
JITDUMP("Folded String.Concat of %u constant strings into a single frozen string handle\n",
argCount);
return gtNewIconEmbObjHndNode(folded);
}
Comment thread
EgorBo marked this conversation as resolved.
}
break;
}

default:
break;
}
Expand Down
86 changes: 86 additions & 0 deletions src/coreclr/jit/importercalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3599,6 +3599,60 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd,
break;
}

case NI_System_String_Concat:
{
// Recognize the static String.Concat(string, string [, string [, string]])
// overloads. We mark the call as "special" so morph and VN can later
// attempt to constant-fold it into a single frozen string handle.
if (((methodFlags & CORINFO_FLG_STATIC) == 0) || (sig->numArgs < 2) || (sig->numArgs > 4) ||
(sig->retType != CORINFO_TYPE_CLASS))
{
break;
}

// All arguments must be System.String, matching the String.Concat
// string-only overloads. Bail otherwise (e.g. params object[]).
bool allStringArgs = true;
CORINFO_ARG_LIST_HANDLE sigArg = sig->args;
for (unsigned i = 0; i < sig->numArgs; i++)
{
CORINFO_CLASS_HANDLE argClass = NO_CLASS_HANDLE;
CorInfoType argType = strip(info.compCompHnd->getArgType(sig, sigArg, &argClass));
if (argType != CORINFO_TYPE_CLASS)
{
allStringArgs = false;
break;
}
// For CORINFO_TYPE_CLASS args, getArgType may return a null
// class handle; query it explicitly and require it to be the
// owning System.String class.
if (argClass == NO_CLASS_HANDLE)
{
argClass = info.compCompHnd->getArgClass(sig, sigArg);
}
if (argClass != clsHnd)
{
allStringArgs = false;
break;
}
sigArg = info.compCompHnd->getArgNext(sigArg);
}

if (!allStringArgs)
{
break;
}

// Mark all string-only Concat overloads as special so both
// morph (gtFoldExprCall) and VN-based folding
// (optVNBasedFoldExpr_Call_StringConcat) can later try to fold
// them. The actual inline-suppression decision in
// impMarkInlineCandidateHelper additionally requires every
// argument to currently be a GT_CNS_STR.
isSpecial = true;
break;
}

case NI_System_String_get_Chars:
{
GenTree* op2 = impPopStack().val;
Expand Down Expand Up @@ -8253,6 +8307,34 @@ void Compiler::impMarkInlineCandidateHelper(GenTreeCall* call,
return;
}

// Don't inline String.Concat when all arguments are constant strings.
// We let the call survive so morph and VN-based folding can replace it
// with a single frozen string handle. Inlining the call here would defeat
// that fold. We deliberately allow inlining when at least one argument is
// not a constant string — VN-based folding handles a few extra cases, but
// most non-constant calls are still better off inlined.
if (call->IsSpecialIntrinsic() && (lookupNamedIntrinsic(call->gtCallMethHnd) == NI_System_String_Concat))
{
bool allCns = true;
for (CallArg& arg : call->gtArgs.Args())
{
if (arg.GetWellKnownArg() != WellKnownArg::None)
{
continue;
}
if (!arg.GetNode()->OperIs(GT_CNS_STR))
{
allCns = false;
break;
}
}
if (allCns)
{
inlineResult->NoteFatal(InlineObservation::CALLEE_IS_FOLDABLE_INTRINSIC);
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

inlineResult->NoteFatal(InlineObservation::CALLEE_IS_FOLDABLE_INTRINSIC) uses a CALLEE-targeted observation. In the inlining policy, CALLEE-targeted fatals become a NEVER decision, which can cause System.String.Concat to be treated as non-inlineable for all callsites after the first time this triggers. That contradicts the intent described above (allow inlining when any arg is non-constant). Consider making this a CALLSITE-targeted observation (or otherwise recording a callsite-only failure) so only this specific callsite is rejected.

Suggested change
inlineResult->NoteFatal(InlineObservation::CALLEE_IS_FOLDABLE_INTRINSIC);
inlineResult->NoteFatal(InlineObservation::CALLSITE_IS_FOLDABLE_INTRINSIC);

Copilot uses AI. Check for mistakes.
return;
}
}

// Inlining candidate determination needs to honor only IL tail prefix.
// Inlining takes precedence over implicit tail call optimization (if the call is not directly recursive).
if (call->IsTailPrefixedCall())
Expand Down Expand Up @@ -10727,6 +10809,10 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method)
{
result = NI_System_String_Equals;
}
else if (strcmp(methodName, "Concat") == 0)
{
result = NI_System_String_Concat;
}
else if (strcmp(methodName, "get_Chars") == 0)
{
result = NI_System_String_get_Chars;
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/jit/inline.def
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ INLINE_OBSERVATION(IS_JIT_NOINLINE, bool, "noinline per JitNoinline"
INLINE_OBSERVATION(IS_NOINLINE, bool, "noinline per IL/cached result", FATAL, CALLEE)
INLINE_OBSERVATION(IS_SYNCHRONIZED, bool, "is synchronized", FATAL, CALLEE)
INLINE_OBSERVATION(IS_VM_NOINLINE, bool, "noinline per VM", FATAL, CALLEE)
INLINE_OBSERVATION(IS_FOLDABLE_INTRINSIC, bool, "foldable intrinsic - keep as call", FATAL, CALLEE)
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

IS_FOLDABLE_INTRINSIC is declared with target CALLEE, which means any NoteFatal using it will set the inlining decision to NEVER (callee-wide) rather than failing just the current callsite. Given the goal is to avoid inlining only when the arguments are all constant strings, this should likely be a CALLSITE observation (and the use site updated accordingly).

Suggested change
INLINE_OBSERVATION(IS_FOLDABLE_INTRINSIC, bool, "foldable intrinsic - keep as call", FATAL, CALLEE)
INLINE_OBSERVATION(IS_FOLDABLE_INTRINSIC, bool, "foldable intrinsic - keep as call", FATAL, CALLSITE)

Copilot uses AI. Check for mistakes.
INLINE_OBSERVATION(LACKS_RETURN, bool, "no return opcode", FATAL, CALLEE)
INLINE_OBSERVATION(LDFLD_NEEDS_HELPER, bool, "ldfld needs helper", FATAL, CALLEE)
INLINE_OBSERVATION(LOCALLOC_TOO_LARGE, bool, "localloc size too large", FATAL, CALLEE)
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 @@ -138,6 +138,7 @@ enum NamedIntrinsic : unsigned short

NI_System_Runtime_InteropService_MemoryMarshal_GetArrayDataReference,

NI_System_String_Concat,
NI_System_String_Equals,
NI_System_String_get_Chars,
NI_System_String_get_Length,
Expand Down
Loading
Loading