Uh oh!
There was an error while loading. Please reload this page.
[experiment] Fold String.Concat - #129659
Conversation
EgorBo
commented
Jun 20, 2026
@MihuBot -nuget |
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
It'll blow up on diffing CoreLib atm, seeing if #129658 is enough to make it work again |
EgorBo
commented
Jun 20, 2026
MihaZupan
commented
Jun 20, 2026
Oops, fixed. |
There was a problem hiding this comment.
Pull request overview
This PR experiments with enabling JIT-time folding of fixed-arity stringString.Concat calls into a single frozen string object when the inputs are compile-time readable, by introducing a new JIT↔EE interface API to allocate such frozen strings.
Changes:
- Marks
String.Concat(string?, ...)overloads as[Intrinsic]and adds a JIT importer expansion to constant-fold eligible calls. - Adds a new JIT↔EE interface method
tryCreateStringObjectand wires it through CoreCLR, SuperPMI record/replay, and NativeAOT/R2R wrappers. - Adjusts inline-observation scanning to avoid treating
String.Concat’s intrinsic marking as an inlining heuristic signal.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs | Marks fixed-arity Concat(string?,...) overloads as intrinsic. |
| src/coreclr/vm/jitinterface.cpp | Implements CEEInfo::tryCreateStringObject to allocate/fill a frozen string. |
| src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp | Plumbs tryCreateStringObject through SuperPMI replay interface. |
| src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp | Adds shim passthrough for the new JIT↔EE API. |
| src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp | Adds shim passthrough + call counting for the new API. |
| src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp | Records/replays tryCreateStringObject in the collector shim. |
| src/coreclr/tools/superpmi/superpmi-shared/methodcontext.h | Declares record/replay support + packet ID for the new API. |
| src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp | Implements record/replay logic for tryCreateStringObject. |
| src/coreclr/tools/superpmi/superpmi-shared/lwmlist.h | Adds a new LightWeightMap slot for TryCreateStringObject. |
| src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt | Adds the new API to the thunk generator input. |
| src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs | Adds managed callback plumbing for tryCreateStringObject. |
| src/coreclr/tools/aot/jitinterface/jitinterface_generated.h | Updates NativeAOT JIT interface wrapper for the new API. |
| src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs | Returns null for tryCreateStringObject during AOT compilation. |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs | Returns null for tryCreateStringObject during R2R compilation. |
| src/coreclr/jit/namedintrinsiclist.h | Adds NI_System_String_Concat. |
| src/coreclr/jit/importercalls.cpp | Implements impStringConcat folding and dispatches it from impIntrinsic. |
| src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp | Adds wrapper logging/dispatch for tryCreateStringObject. |
| src/coreclr/jit/ICorJitInfo_names_generated.h | Adds the API name for wrapper logging. |
| src/coreclr/jit/fgbasic.cpp | Prevents String.Concat intrinsic marking from affecting inliner heuristics. |
| src/coreclr/jit/compiler.h | Declares impStringConcat. |
| src/coreclr/inc/jiteeversionguid.h | Bumps the JIT↔EE interface GUID. |
| src/coreclr/inc/icorjitinfoimpl_generated.h | Adds the new interface method declaration (generated). |
| src/coreclr/inc/corinfo.h | Adds the new ICorStaticInfo::tryCreateStringObject API. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
MihaZupan
commented
Jun 20, 2026
@MihuBot -nuget -dependsOn https://github.com/MihaZupan/runtime/tree/revert-test-3 |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
EgorBo
commented
Jun 20, 2026
@MihuBot -nuget -dependsOn https://github.com/MihaZupan/runtime/tree/revert-test-3 |
MihaZupan
commented
Jun 20, 2026
@MihuBot -nuget |
EgorBo
commented
Jun 20, 2026
isRunRarely check swept quite a few hits. Anyway, I think it should be done differently and recognize FastAllocateStrings + memmoves in a late phase. |
af94797 to
83cbce6CompareUh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
83cbce6 to
c494901Comparec494901 to
c900896Compare| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| public static string Concat(string? str0, string? str1) |
| CONTRACTL { | ||
| THROWS; | ||
| GC_TRIGGERS; | ||
| MODE_PREEMPTIVE; | ||
| } CONTRACTL_END; |
| bool isFrozen = false; | ||
| STRINGREF strObj = AllocateString((DWORD)length, /* preferFrozenHeap */ true, &isFrozen); | ||
| virtual CORINFO_OBJECT_HANDLE tryCreateStringObject( | ||
| uint16_t* str, | ||
| int length | ||
| ) = 0; |
| // The input is a transient JIT-side buffer, so we key the record on a content hash of the | ||
| // requested string (length + characters) rather than on the pointer value. | ||
| static DWORDLONG HashStringObjectRequest(uint16_t* str, int length) | ||
| { | ||
| DWORDLONG hash = 0xCBF29CE484222325ULL ^ (DWORDLONG)(DWORD)length; // FNV-1a offset basis |
| // Ask the VM to materialize a frozen string object with the computed contents. | ||
| CORINFO_OBJECT_HANDLE strObj = info.compCompHnd->tryCreateStringObject(buffer, (int)strLength); | ||
| if (strObj == NO_OBJECT_HANDLE) | ||
| { | ||
| JITDUMP("...VM could not create a frozen string object - bail out.\n"); |
| // Success - remove the now-redundant initialization statements. Their only observable effect is | ||
| // writing into the string we are about to replace with a constant, and all the written data is | ||
| // constant, so they are safe to drop. | ||
| while (initStmts.Height() > 0) | ||
| { |
c900896 to
81196bbCompareAdd a JIT-EE API `tryCreateStringObject` and assertion-prop optimizations that fold compile-time-constant strings into a single frozen string object: * String.Concat(string, ...) fixed-arity overloads are marked [Intrinsic] and recognized as NI_System_String_Concat. The JIT keeps them as calls (impMarkInlineCandidate skips this intrinsic) instead of inlining their bodies, and folds the call directly when all arguments are constant strings. * The same frozen-string materialization also folds a FastAllocateString followed by a sequence of constant memmoves (as produced by String.Substring). The VM only creates the string on the frozen object heap and never for collectible/unloadable contexts (and never returns a non-frozen instance); NativeAOT/crossgen return null. NI_System_String_Concat is also excluded from the inliner's CALLEE_INTRINSIC heuristic so marking Concat [Intrinsic] does not perturb inlining decisions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
81196bb to
1a557d2Compare| virtual CORINFO_OBJECT_HANDLE tryCreateStringObject( | ||
| uint16_t* str, | ||
| int length | ||
| ) = 0; |
| totalLength += strLen; | ||
| } | ||
| GenTree* con = optCreateConstStringNode(buffer, totalLength, call); |
| // String.Concat is marked [Intrinsic] only so that it can be constant-folded during assertion | ||
| // propagation (NI_System_String_Concat). Keep it as a call rather than inlining its body so the | ||
| // fold has something to recognize, and to avoid the size bloat of inlining Concat everywhere. | ||
| if (call->IsSpecialIntrinsic(this, NI_System_String_Concat)) | ||
| { | ||
| return; | ||
| } |
| CORINFO_OBJECT_HANDLE strObj = info.compCompHnd->tryCreateStringObject(buffer, length); | ||
| if (strObj == NO_OBJECT_HANDLE) | ||
| { | ||
| JITDUMP("...VM could not create a frozen string object - bail out.\n"); | ||
| return nullptr; | ||
| } | ||
| GenTree* con = gtNewIconEmbObjHndNode(strObj); | ||
| con->gtVNPair.SetBoth(vnStore->VNForHandle((ssize_t)strObj, GTF_ICON_OBJ_HDL)); |
Folds string.Concat(...) for jit-time known constant strings (including static readonly fields holding non-FOH strings).
just to see diffs in MihuBot -nuget if it's any popular in 3rd parties..
Diffs are promising