From 325fd7d48b619e4a577faa7a2a41588dcbcc6130 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Thu, 27 Feb 2025 10:48:01 -0800 Subject: [PATCH 01/18] JIT: enable inlining methods with EH Enable inlining of method with EH. Inlinee EH clauses are integrated into the root method EH table at the appropriate point (mid-table if the call site is in an EH region; at table end otherwise). Contributes to #108900. --- src/coreclr/jit/compiler.cpp | 17 +++ src/coreclr/jit/compiler.h | 3 + src/coreclr/jit/fgbasic.cpp | 4 +- src/coreclr/jit/fginline.cpp | 181 +++++++++++++++++++++++++++++- src/coreclr/jit/importer.cpp | 24 ++-- src/coreclr/jit/jitconfigvalues.h | 3 + src/coreclr/vm/jitinterface.cpp | 9 +- 7 files changed, 223 insertions(+), 18 deletions(-) diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index 61c98f2b557731..609e8174a9f981 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -2574,6 +2574,23 @@ void Compiler::compInitOptions(JitFlags* jitFlags) #endif // DEBUG } + bool enableInliningMethodsWithEH = false; + +#ifdef DEBUG + enableInliningMethodsWithEH = JitConfig.JitInlineMethodsWithEH() > 0; + static ConfigMethodRange JitInlineMethodsWithEHRange; + JitInlineMethodsWithEHRange.EnsureInit(JitConfig.JitInlineMethodsWithEHRange()); + const unsigned hash = impInlineRoot()->info.compMethodHash(); + const bool inRange = JitInlineMethodsWithEHRange.Contains(hash); + enableInliningMethodsWithEH &= inRange; + if (enableInliningMethodsWithEH) + { + JITDUMP("#### enabling EH inlining in %s (0x%08x)\n", info.compFullName, hash); + } +#endif + + opts.compInlineMethodsWithEH = enableInliningMethodsWithEH; + if (compIsForInlining()) { return; diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 6763f1f2d5231f..142e2bd41a4351 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -10470,6 +10470,9 @@ class Compiler // Collect 64 bit counts for PGO data. bool compCollect64BitCounts; + // Allow inlining of methods with EH. + bool compInlineMethodsWithEH; + } opts; static bool s_pAltJitExcludeAssembliesListInitialized; diff --git a/src/coreclr/jit/fgbasic.cpp b/src/coreclr/jit/fgbasic.cpp index e593b92c6b7da6..ec22a2a99c26ea 100644 --- a/src/coreclr/jit/fgbasic.cpp +++ b/src/coreclr/jit/fgbasic.cpp @@ -3429,7 +3429,7 @@ void Compiler::fgFindBasicBlocks() if (info.compXcptnsCount > 0) { - noway_assert(!compIsForInlining()); + assert(!compIsForInlining() || opts.compInlineMethodsWithEH); /* Check and mark all the exception handlers */ @@ -3577,8 +3577,6 @@ void Compiler::fgFindBasicBlocks() lvaInlineeReturnSpillTempFreshlyCreated = true; } } - - return; } /* Mark all blocks within 'try' blocks as such */ diff --git a/src/coreclr/jit/fginline.cpp b/src/coreclr/jit/fginline.cpp index 5b9052b28c6699..6db7cf35b8cbf3 100644 --- a/src/coreclr/jit/fginline.cpp +++ b/src/coreclr/jit/fginline.cpp @@ -1568,13 +1568,182 @@ void Compiler::fgInsertInlineeBlocks(InlineInfo* pInlineInfo) // bottomBlock->RemoveFlags(BBF_DONT_REMOVE); + // If the inlinee has EH, merge the EH tables, and figure out how much of + // a shift we need to make in the inlinee blocks EH indicies. + // + unsigned const inlineeRegionCount = InlineeCompiler->compHndBBtabCount; + const bool inlineeHasEH = inlineeRegionCount > 0; + unsigned inlineeIndexShift = 0; + + if (inlineeHasEH) + { + // If the call site also has EH, we need to insert the inlinee clauses + // so they are a child of the call site's innermost enclosing region. + // Figure out what this is. + // + bool inTryRegion = false; + unsigned const enclosingRegion = ehGetMostNestedRegionIndex(iciBlock, &inTryRegion); + + // We will insert the inlinee clauses in bulk before this index. + // + unsigned insertBeforeIndex = 0; + + if (enclosingRegion == 0) + { + // The call site is not in an EH region, so we can put the inlinee EH clauses + // at the end of root method's the EH table. + // + // For example, if the root method already has EH#0, and the inlinee has 2 regions + // + // enclosingRegion will be 0 + // inlineeIndexShift will be 1 + // insertBeforeIndex will be 1 + // + // inlinee eh0 -> eh1 + // inlinee eh1 -> eh2 + // + // root eh0 remains as is + // + inlineeIndexShift = compHndBBtabCount; + insertBeforeIndex = compHndBBtabCount; + } + else + { + // enclosingRegion is shifted up by one, eg EH#0 will report as 1.. + // + // + // The call site is in an EH region, so we can put the inlinee EH clauses + // just before the enclosing region + // + // For example, if the enclosing EH regions are try#2 and hnd#3, and the inlinee has 2 eh clauses + // + // enclosingRegion will be 3 + // inlineeIndexShift will be 2 + // insertBeforeIndex will be 2 + // + // inlinee eh0 -> eh2 + // inlinee eh1 -> eh3 + // + // root eh2 -> eh4; + // root eh3 -> eh5 + // + inlineeIndexShift = enclosingRegion - 1; + insertBeforeIndex = enclosingRegion - 1; + } + + JITDUMP( + "Inlinee has EH. In root method, inlinee's %u EH region indices will shift by %u and become EH#%02u ... EH#%02u (%p)\n", + inlineeRegionCount, inlineeIndexShift, insertBeforeIndex, insertBeforeIndex + inlineeRegionCount - 1, + &inlineeIndexShift); + + if (enclosingRegion != 0) + { + JITDUMP("Inlinee is nested within current %s EH#%02u (which will become EH#%02u)\n", + inTryRegion ? "try" : "hnd", enclosingRegion - 1, enclosingRegion - 1 + inlineeRegionCount); + } + else + { + JITDUMP("Inlinee is not nested inside any EH region\n"); + } + + // Grow the EH table. + // + // TODO: verify earlier that this won't fail... + // + EHblkDsc* const outermostEbd = + fgTryAddEHTableEntries(insertBeforeIndex, inlineeRegionCount, /* deferAdding */ false); + assert(outermostEbd != nullptr); + + // fgTryAddEHTableEntries has adjusted the indices of all root method blocks and EH clauses + // to accommodate the new entries. No other changes to those are needed. + // + // We just need to add in and fix up the new entries from the inlinee. + // + // Fetch the new enclosing try/handler table indicies. + // + const unsigned enclosingTryIndex = + iciBlock->hasTryIndex() ? iciBlock->getTryIndex() : EHblkDsc::NO_ENCLOSING_INDEX; + const unsigned enclosingHndIndex = + iciBlock->hasHndIndex() ? iciBlock->getHndIndex() : EHblkDsc::NO_ENCLOSING_INDEX; + + // Copy over the EH table entries from inlinee->root and adjust their enclosing indicies. + // + for (unsigned XTnum = 0; XTnum < inlineeRegionCount; XTnum++) + { + unsigned newXTnum = XTnum + inlineeIndexShift; + compHndBBtab[newXTnum] = InlineeCompiler->compHndBBtab[XTnum]; + EHblkDsc* const ebd = &compHndBBtab[newXTnum]; + + if (ebd->ebdEnclosingTryIndex != EHblkDsc::NO_ENCLOSING_INDEX) + { + ebd->ebdEnclosingTryIndex += (unsigned short)inlineeIndexShift; + } + else + { + ebd->ebdEnclosingTryIndex = (unsigned short)enclosingTryIndex; + } + + if (ebd->ebdEnclosingHndIndex != EHblkDsc::NO_ENCLOSING_INDEX) + { + ebd->ebdEnclosingHndIndex += (unsigned short)inlineeIndexShift; + } + else + { + ebd->ebdEnclosingHndIndex = (unsigned short)enclosingHndIndex; + } + } + + if (verbose) + { + fgDispHandlerTab(); + } + } + + // Fetch the new enclosing try/handler indicies for blocks. + // Note these are represented differently than the EH table indices. + // + const unsigned blockEnclosingTryIndex = iciBlock->hasTryIndex() ? iciBlock->getTryIndex() + 1 : 0; + const unsigned blockEnclosingHndIndex = iciBlock->hasHndIndex() ? iciBlock->getHndIndex() + 1 : 0; + // Set the try and handler index and fix the jump types of inlinee's blocks. // for (BasicBlock* const block : InlineeCompiler->Blocks()) { - noway_assert(!block->hasTryIndex()); - noway_assert(!block->hasHndIndex()); - block->copyEHRegion(iciBlock); + if (block->hasTryIndex()) + { + JITDUMP("Inlinee " FMT_BB " has old try index %u, shift %u, new try index %u\n", block->bbNum, + (unsigned)block->bbTryIndex, inlineeIndexShift, + (unsigned)(block->bbTryIndex + inlineeIndexShift)); + block->bbTryIndex += (unsigned short)inlineeIndexShift; + } + else + { + block->bbTryIndex = (unsigned short)blockEnclosingTryIndex; + } + + if (block->hasHndIndex()) + { + block->bbHndIndex += (unsigned short)inlineeIndexShift; + } + else + { + block->bbHndIndex = (unsigned short)blockEnclosingHndIndex; + } + + // Sanity checks + // + if (iciBlock->hasTryIndex()) + { + assert(block->hasTryIndex()); + assert(block->getTryIndex() <= iciBlock->getTryIndex()); + } + + if (iciBlock->hasHndIndex()) + { + assert(block->hasHndIndex()); + assert(block->getHndIndex() <= iciBlock->getHndIndex()); + } + block->CopyFlags(iciBlock, BBF_BACKWARD_JUMP | BBF_PROF_WEIGHT); // Update block nums appropriately @@ -1618,6 +1787,12 @@ void Compiler::fgInsertInlineeBlocks(InlineInfo* pInlineInfo) topBlock->SetNext(InlineeCompiler->fgFirstBB); InlineeCompiler->fgLastBB->SetNext(bottomBlock); + if (verbose) + { + fgDispBasicBlocks(); + fgDispHandlerTab(); + } + // // Add inlinee's block count to inliner's. // diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index 323951bf64ba91..3b95a84444211a 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -6974,9 +6974,9 @@ void Compiler::impImportBlockCode(BasicBlock* block) case CEE_ENDFINALLY: - if (compIsForInlining()) + if (compIsForInlining() && !opts.compInlineMethodsWithEH) { - assert(!"Shouldn't have exception handlers in the inliner!"); + assert(!"Shouldn't have exception handlers in the inlinee!"); compInlineResult->NoteFatal(InlineObservation::CALLEE_HAS_ENDFINALLY); return; } @@ -6998,9 +6998,9 @@ void Compiler::impImportBlockCode(BasicBlock* block) case CEE_ENDFILTER: - if (compIsForInlining()) + if (compIsForInlining() && !opts.compInlineMethodsWithEH) { - assert(!"Shouldn't have exception handlers in the inliner!"); + assert(!"Shouldn't have exception handlers in the inlinee!"); compInlineResult->NoteFatal(InlineObservation::CALLEE_HAS_ENDFILTER); return; } @@ -7572,7 +7572,7 @@ void Compiler::impImportBlockCode(BasicBlock* block) LEAVE: - if (compIsForInlining()) + if (compIsForInlining() && !opts.compInlineMethodsWithEH) { compInlineResult->NoteFatal(InlineObservation::CALLEE_HAS_LEAVE); return; @@ -11482,7 +11482,7 @@ inline void Compiler::impReimportMarkBlock(BasicBlock* block) void Compiler::impVerifyEHBlock(BasicBlock* block) { assert(block->hasTryIndex()); - assert(!compIsForInlining()); + assert(!compIsForInlining() || opts.compInlineMethodsWithEH); unsigned tryIndex = block->getTryIndex(); EHblkDsc* HBtab = ehGetDsc(tryIndex); @@ -12555,7 +12555,8 @@ void Compiler::impImport() // (notably those from BBJ_EHFINALLYRET blocks). Add them. // Only needed for the root method, since inlinees can't have EH. // - if (!compIsForInlining() && (info.compXcptnsCount > 0)) + // TODO --------------- if haseh... + if (info.compXcptnsCount > 0) { impFixPredLists(); JITDUMP("\nAfter impImport() added blocks for try,catch,finally"); @@ -12977,10 +12978,13 @@ void Compiler::impCanInlineIL(CORINFO_METHOD_HANDLE fncHandle, // We shouldn't have made up our minds yet... assert(!inlineResult->IsDecided()); - if (methInfo->EHcount) + if (methInfo->EHcount > 0) { - inlineResult->NoteFatal(InlineObservation::CALLEE_HAS_EH); - return; + if (!opts.compInlineMethodsWithEH) + { + inlineResult->NoteFatal(InlineObservation::CALLEE_HAS_EH); + return; + } } if ((methInfo->ILCode == nullptr) || (codeSize == 0)) diff --git a/src/coreclr/jit/jitconfigvalues.h b/src/coreclr/jit/jitconfigvalues.h index 5fe7439d2bc658..70601666c8ef4f 100644 --- a/src/coreclr/jit/jitconfigvalues.h +++ b/src/coreclr/jit/jitconfigvalues.h @@ -119,6 +119,9 @@ CONFIG_INTEGER(JitInlinePrintStats, "JitInlinePrintStats", 0) CONFIG_INTEGER(JitInlineSize, "JITInlineSize", DEFAULT_MAX_INLINE_SIZE) CONFIG_INTEGER(JitInlineDepth, "JITInlineDepth", DEFAULT_MAX_INLINE_DEPTH) CONFIG_INTEGER(JitForceInlineDepth, "JITForceInlineDepth", DEFAULT_MAX_FORCE_INLINE_DEPTH) +CONFIG_INTEGER(JitInlineMethodsWithEH, "JitInlineMethodsWithEH", 0) +CONFIG_STRING(JitInlineMethodsWithEHRange, "JitInlineMethodsWithEHRange") + CONFIG_INTEGER(JitLongAddress, "JitLongAddress", 0) // Force using the large pseudo instruction form for long address CONFIG_INTEGER(JitMaxUncheckedOffset, "JitMaxUncheckedOffset", 8) diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 87c0b89e593d04..c702066d55617e 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -12480,11 +12480,16 @@ void CEEJitInfo::getEHinfo( { GetMethod(ftn)->AsDynamicMethodDesc()->GetResolver()->GetEHInfo(EHnumber, clause); } - else + else if (ftn == CORINFO_METHOD_HANDLE(m_pMethodBeingCompiled)) { - _ASSERTE(ftn == CORINFO_METHOD_HANDLE(m_pMethodBeingCompiled)); // For now only support if the method being jitted getEHinfoHelper(ftn, EHnumber, clause, m_ILHeader); } + else + { + MethodDesc* method = GetMethod(ftn); + COR_ILMETHOD_DECODER header(method->GetILHeader(), method->GetMDImport(), NULL); + getEHinfoHelper(ftn, EHnumber, clause, &header); + } EE_TO_JIT_TRANSITION(); } From 1c7d547dcc4b6b2ef5e9bbb6916ca26944407341 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Thu, 27 Feb 2025 12:10:08 -0800 Subject: [PATCH 02/18] enable by default; fix release build issues --- src/coreclr/jit/compiler.cpp | 7 +------ src/coreclr/jit/fginline.cpp | 11 ----------- src/coreclr/jit/jitconfigvalues.h | 2 +- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index 609e8174a9f981..6d7ee0826c9ea3 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -2574,19 +2574,14 @@ void Compiler::compInitOptions(JitFlags* jitFlags) #endif // DEBUG } - bool enableInliningMethodsWithEH = false; + bool enableInliningMethodsWithEH = JitConfig.JitInlineMethodsWithEH() > 0; #ifdef DEBUG - enableInliningMethodsWithEH = JitConfig.JitInlineMethodsWithEH() > 0; static ConfigMethodRange JitInlineMethodsWithEHRange; JitInlineMethodsWithEHRange.EnsureInit(JitConfig.JitInlineMethodsWithEHRange()); const unsigned hash = impInlineRoot()->info.compMethodHash(); const bool inRange = JitInlineMethodsWithEHRange.Contains(hash); enableInliningMethodsWithEH &= inRange; - if (enableInliningMethodsWithEH) - { - JITDUMP("#### enabling EH inlining in %s (0x%08x)\n", info.compFullName, hash); - } #endif opts.compInlineMethodsWithEH = enableInliningMethodsWithEH; diff --git a/src/coreclr/jit/fginline.cpp b/src/coreclr/jit/fginline.cpp index 6db7cf35b8cbf3..a8ffe9577b9e92 100644 --- a/src/coreclr/jit/fginline.cpp +++ b/src/coreclr/jit/fginline.cpp @@ -1692,11 +1692,6 @@ void Compiler::fgInsertInlineeBlocks(InlineInfo* pInlineInfo) ebd->ebdEnclosingHndIndex = (unsigned short)enclosingHndIndex; } } - - if (verbose) - { - fgDispHandlerTab(); - } } // Fetch the new enclosing try/handler indicies for blocks. @@ -1787,12 +1782,6 @@ void Compiler::fgInsertInlineeBlocks(InlineInfo* pInlineInfo) topBlock->SetNext(InlineeCompiler->fgFirstBB); InlineeCompiler->fgLastBB->SetNext(bottomBlock); - if (verbose) - { - fgDispBasicBlocks(); - fgDispHandlerTab(); - } - // // Add inlinee's block count to inliner's. // diff --git a/src/coreclr/jit/jitconfigvalues.h b/src/coreclr/jit/jitconfigvalues.h index 70601666c8ef4f..e5651f6258be87 100644 --- a/src/coreclr/jit/jitconfigvalues.h +++ b/src/coreclr/jit/jitconfigvalues.h @@ -119,7 +119,7 @@ CONFIG_INTEGER(JitInlinePrintStats, "JitInlinePrintStats", 0) CONFIG_INTEGER(JitInlineSize, "JITInlineSize", DEFAULT_MAX_INLINE_SIZE) CONFIG_INTEGER(JitInlineDepth, "JITInlineDepth", DEFAULT_MAX_INLINE_DEPTH) CONFIG_INTEGER(JitForceInlineDepth, "JITForceInlineDepth", DEFAULT_MAX_FORCE_INLINE_DEPTH) -CONFIG_INTEGER(JitInlineMethodsWithEH, "JitInlineMethodsWithEH", 0) +RELEASE_CONFIG_INTEGER(JitInlineMethodsWithEH, "JitInlineMethodsWithEH", 1) CONFIG_STRING(JitInlineMethodsWithEHRange, "JitInlineMethodsWithEHRange") CONFIG_INTEGER(JitLongAddress, "JitLongAddress", 0) // Force using the large pseudo instruction form for long address From bb61117860349e17a962088720f68bd9e99c558f Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Thu, 27 Feb 2025 17:21:31 -0800 Subject: [PATCH 03/18] fix uses of compXcptnsCount; fix unmanaged callconv with IL case --- src/coreclr/jit/compiler.h | 2 +- src/coreclr/jit/compiler.hpp | 2 +- src/coreclr/jit/importercalls.cpp | 9 +++++++++ src/coreclr/jit/inline.def | 3 ++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 142e2bd41a4351..a342d97acd901b 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -2704,7 +2704,7 @@ class Compiler bool ehNeedsShadowSPslots() { - return (info.compXcptnsCount || opts.compDbgEnC); + return ((compHndBBtabCount > 0) || opts.compDbgEnC); } // 0 for methods with no EH diff --git a/src/coreclr/jit/compiler.hpp b/src/coreclr/jit/compiler.hpp index cca9e2e6755dbe..770ab66c1c3384 100644 --- a/src/coreclr/jit/compiler.hpp +++ b/src/coreclr/jit/compiler.hpp @@ -2515,7 +2515,7 @@ inline bool Compiler::lvaKeepAliveAndReportThis() { // TODO: Check if any of the exception clauses are // typed using a generic type. Else, we do not need to report this. - if (info.compXcptnsCount > 0) + if (compHndBBtabCount > 0) return true; if (opts.compDbgCode) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 43b022b99e67d4..413932546ae51e 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -7635,6 +7635,15 @@ void Compiler::impMarkInlineCandidateHelper(GenTreeCall* call, return; } + // The inliner gets confused when the unmanaged convention reverses arg order (like x86). + // Just suppress for all targets for now. + // + if (call->GetUnmanagedCallConv() != CorInfoCallConvExtension::Managed) + { + inlineResult->NoteFatal(InlineObservation::CALLEE_HAS_UNMANAGED_CALLCONV); + return; + } + /* I removed the check for BBJ_THROW. BBJ_THROW is usually marked as rarely run. This more or less * restricts the inliner to non-expanding inlines. I removed the check to allow for non-expanding * inlining in throw blocks. I should consider the same thing for catch and filter regions. */ diff --git a/src/coreclr/jit/inline.def b/src/coreclr/jit/inline.def index efacbd4deb27b6..2b045ad5d20009 100644 --- a/src/coreclr/jit/inline.def +++ b/src/coreclr/jit/inline.def @@ -28,6 +28,7 @@ INLINE_OBSERVATION(UNUSED_INITIAL, bool, "unused initial observatio INLINE_OBSERVATION(BAD_ARGUMENT_NUMBER, bool, "invalid argument number", FATAL, CALLEE) INLINE_OBSERVATION(BAD_LOCAL_NUMBER, bool, "invalid local number", FATAL, CALLEE) INLINE_OBSERVATION(COMPILATION_ERROR, bool, "compilation error", FATAL, CALLEE) +INLINE_OBSERVATION(EXPLICIT_TAIL_PREFIX, bool, "explicit tail prefix in callee", FATAL, CALLEE) INLINE_OBSERVATION(HAS_EH, bool, "has exception handling", FATAL, CALLEE) INLINE_OBSERVATION(HAS_ENDFILTER, bool, "has endfilter", FATAL, CALLEE) INLINE_OBSERVATION(HAS_ENDFINALLY, bool, "has endfinally", FATAL, CALLEE) @@ -36,6 +37,7 @@ INLINE_OBSERVATION(HAS_MANAGED_VARARGS, bool, "managed varargs", INLINE_OBSERVATION(HAS_NATIVE_VARARGS, bool, "native varargs", FATAL, CALLEE) INLINE_OBSERVATION(HAS_NO_BODY, bool, "has no body", FATAL, CALLEE) INLINE_OBSERVATION(HAS_NULL_FOR_LDELEM, bool, "has null pointer for ldelem", FATAL, CALLEE) +INLINE_OBSERVATION(HAS_UNMANAGED_CALLCONV, bool, "has unmanaged calling convention", FATAL, CALLEE) INLINE_OBSERVATION(IS_ARRAY_METHOD, bool, "is array method", FATAL, CALLEE) INLINE_OBSERVATION(IS_GENERIC_VIRTUAL, bool, "generic virtual", FATAL, CALLEE) INLINE_OBSERVATION(IS_JIT_NOINLINE, bool, "noinline per JitNoinline", FATAL, CALLEE) @@ -55,7 +57,6 @@ INLINE_OBSERVATION(STACK_CRAWL_MARK, bool, "uses stack crawl mark", INLINE_OBSERVATION(STFLD_NEEDS_HELPER, bool, "stfld needs helper", FATAL, CALLEE) INLINE_OBSERVATION(TOO_MANY_ARGUMENTS, bool, "too many arguments", FATAL, CALLEE) INLINE_OBSERVATION(TOO_MANY_LOCALS, bool, "too many locals", FATAL, CALLEE) -INLINE_OBSERVATION(EXPLICIT_TAIL_PREFIX, bool, "explicit tail prefix in callee", FATAL, CALLEE) // ------ Callee Performance ------- From e654746615703a85e68136c0d9d78f2bcbf1bb07 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Thu, 27 Feb 2025 18:37:01 -0800 Subject: [PATCH 04/18] avoid inlining methods with EH into filters --- src/coreclr/jit/importercalls.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 413932546ae51e..098056fe84d7c3 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -7763,6 +7763,14 @@ void Compiler::impMarkInlineCandidateHelper(GenTreeCall* call, return; } + // We cannot inline methods with EH into filter clauses, even if marked as aggressive inline + // + if ((inlineCandidateInfo->methInfo.EHcount > 0) && bbInFilterBBRange(compCurBB)) + { + inlineResult->NoteFatal(InlineObservation::CALLSITE_IS_WITHIN_FILTER); + return; + } + // The old value should be null OR this call should be a guarded devirtualization candidate. assert(call->IsGuardedDevirtualizationCandidate() || (call->GetSingleInlineCandidateInfo() == nullptr)); From ae2d432ce2d6745593bfe500f86658f344d08730 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Thu, 27 Feb 2025 17:21:31 -0800 Subject: [PATCH 05/18] JIT: more inlining with EH prep work Don't try an inline managed methods with unmanaged calling conventions. This mainly copes with x86 where unmanaged calling conventions use reversed arg order, but I've disabled it in general. No diffs as these methods seem to always include EH. Remove uses of `compXcptnsCount` as this goes stale whenever we clone or remove EH, or (eventually) inline methods with EH. Instead, rely on `compHndBBtabCount`. Defer allocating x86's shadow SP var and area until later in jitting, so this reflects any changes in EH table structure. In particular we often are able to eliminate EH in part or all together and this saves a low-offset allocation and so leads to some nice code size savings on x86. Also on x86 remove the runtime-dependent catch class case from the computation for keeping this alive, as we now transform such into runtime lookups in filters (that may well keep this alive). --- src/coreclr/jit/compiler.cpp | 77 ++++++++++++++++++++++++++++++++++++ src/coreclr/jit/compiler.h | 3 ++ src/coreclr/jit/compiler.hpp | 32 ++++++--------- src/coreclr/jit/jiteh.cpp | 3 ++ src/coreclr/jit/lclvars.cpp | 29 -------------- 5 files changed, 94 insertions(+), 50 deletions(-) diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index 6d7ee0826c9ea3..c47a726ee637c8 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -5013,6 +5013,8 @@ void Compiler::compCompile(void** methodCodePtr, uint32_t* methodCodeSize, JitFl stackLevelSetter.Run(); m_pLowering->FinalizeOutgoingArgSpace(); + FinalizeEH(); + // We can not add any new tracked variables after this point. lvaTrackedFixed = true; @@ -5168,6 +5170,81 @@ void Compiler::compCompile(void** methodCodePtr, uint32_t* methodCodeSize, JitFl #endif // FUNC_INFO_LOGGING } +//---------------------------------------------------------------------------------------------- +// FinalizeEH: Finalize EH information +// +void Compiler::FinalizeEH() +{ +#if defined(FEATURE_EH_WINDOWS_X86) + + // Grab space for exception handling info on the frame + // + if (!UsesFunclets() && ehNeedsShadowSPslots()) + { + // Recompute the handler nesting levels, as they may have changed. + // + unsigned oldHandlerNestingLevel = ehMaxHndNestingCount; + ehMaxHndNestingCount = 0; + + if (compHndBBtabCount > 0) + { + for (int XTnum = compHndBBtabCount - 1; XTnum >= 0; XTnum--) + { + EHblkDsc* const HBtab = &compHndBBtab[XTnum]; + unsigned const enclosingHndIndex = HBtab->ebdEnclosingHndIndex; + + if (enclosingHndIndex != EHblkDsc::NO_ENCLOSING_INDEX) + { + EHblkDsc* const enclosingHBtab = &compHndBBtab[enclosingHndIndex]; + unsigned const newNestingLevel = enclosingHBtab->ebdHandlerNestingLevel + 1; + HBtab->ebdHandlerNestingLevel = (unsigned short)newNestingLevel; + + if (newNestingLevel > ehMaxHndNestingCount) + { + ehMaxHndNestingCount = newNestingLevel; + } + } + else + { + HBtab->ebdHandlerNestingLevel = 0; + } + } + } + + if (oldHandlerNestingLevel != ehMaxHndNestingCount) + { + JITDUMP("Finalize EH: max handler nesting level now %u (was %u)\n", oldHandlerNestingLevel, + ehMaxHndNestingCount); + } + + // The first slot is reserved for ICodeManager::FixContext(ppEndRegion) + // ie. the offset of the end-of-last-executed-filter + unsigned slotsNeeded = 1; + + unsigned handlerNestingLevel = ehMaxHndNestingCount; + + if (opts.compDbgEnC && (handlerNestingLevel < (unsigned)MAX_EnC_HANDLER_NESTING_LEVEL)) + handlerNestingLevel = (unsigned)MAX_EnC_HANDLER_NESTING_LEVEL; + + slotsNeeded += handlerNestingLevel; + + // For a filter (which can be active at the same time as a catch/finally handler) + slotsNeeded++; + // For zero-termination of the shadow-Stack-pointer chain + slotsNeeded++; + + lvaShadowSPslotsVar = lvaGrabTempWithImplicitUse(false DEBUGARG("lvaShadowSPslotsVar")); + lvaSetStruct(lvaShadowSPslotsVar, typGetBlkLayout(slotsNeeded * TARGET_POINTER_SIZE), false); + lvaSetVarAddrExposed(lvaShadowSPslotsVar DEBUGARG(AddressExposedReason::EXTERNALLY_VISIBLE_IMPLICITLY)); + } + +#endif // FEATURE_EH_WINDOWS_X86 + + // We should not make any more alterations to the EH table structure. + // + ehTableFinalized = true; +} + #if FEATURE_LOOP_ALIGN //------------------------------------------------------------------------ diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index a342d97acd901b..89ccbd2cdccf8e 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -2715,6 +2715,9 @@ class Compiler #endif // FEATURE_EH_WINDOWS_X86 + bool ehTableFinalized = false; + void FinalizeEH(); + static bool jitIsBetween(unsigned value, unsigned start, unsigned end); static bool jitIsBetweenInclusive(unsigned value, unsigned start, unsigned end); diff --git a/src/coreclr/jit/compiler.hpp b/src/coreclr/jit/compiler.hpp index 770ab66c1c3384..a1e706fde6083e 100644 --- a/src/coreclr/jit/compiler.hpp +++ b/src/coreclr/jit/compiler.hpp @@ -2482,21 +2482,17 @@ inline void LclVarDsc::incRefCnts(weight_t weight, Compiler* comp, RefCountState #endif } -/***************************************************************************** - Is this a synchronized instance method? If so, we will need to report "this" - in the GC information, so that the EE can release the object lock - in case of an exception - - We also need to report "this" and keep it alive for all shared generic - code that gets the actual generic context from the "this" pointer and - has exception handlers. - - For example, if List::m() is shared between T = object and T = string, - then inside m() an exception handler "catch E" needs to be able to fetch - the 'this' pointer to find out what 'T' is in order to tell if we - should catch the exception or not. - */ - +//------------------------------------------------------------------------ +// lvaKeepAliveAndReportThis: check if there implicit references to this during method execution +// +// Returns: +// true if this must remain alive throughout the method, even if unreferenced +// +// Notes: +// In a synchronized instance method we need to report "this" +// in the GC information, so that the EE can release the object lock +// in case of an exception +// inline bool Compiler::lvaKeepAliveAndReportThis() { if (info.compIsStatic || (lvaTable[0].TypeGet() != TYP_REF)) @@ -2507,17 +2503,11 @@ inline bool Compiler::lvaKeepAliveAndReportThis() const bool genericsContextIsThis = (info.compMethodInfo->options & CORINFO_GENERICS_CTXT_FROM_THIS) != 0; #ifdef JIT32_GCENCODER - if (info.compFlags & CORINFO_FLG_SYNCH) return true; if (genericsContextIsThis) { - // TODO: Check if any of the exception clauses are - // typed using a generic type. Else, we do not need to report this. - if (compHndBBtabCount > 0) - return true; - if (opts.compDbgCode) return true; diff --git a/src/coreclr/jit/jiteh.cpp b/src/coreclr/jit/jiteh.cpp index c833f2164fa0bc..946ac89df435e7 100644 --- a/src/coreclr/jit/jiteh.cpp +++ b/src/coreclr/jit/jiteh.cpp @@ -1513,6 +1513,7 @@ void Compiler::fgRemoveEHTableEntry(unsigned XTnum) { assert(compHndBBtabCount > 0); assert(XTnum < compHndBBtabCount); + assert(!ehTableFinalized); EHblkDsc* HBtab; @@ -1727,6 +1728,8 @@ void Compiler::fgRemoveEHTableEntry(unsigned XTnum) // EHblkDsc* Compiler::fgTryAddEHTableEntries(unsigned XTnum, unsigned count, bool deferAdding) { + assert(!ehTableFinalized); + bool reallocate = false; bool const insert = (XTnum != compHndBBtabCount); unsigned const newCount = compHndBBtabCount + count; diff --git a/src/coreclr/jit/lclvars.cpp b/src/coreclr/jit/lclvars.cpp index 1bee35953e59fd..38e251139928b4 100644 --- a/src/coreclr/jit/lclvars.cpp +++ b/src/coreclr/jit/lclvars.cpp @@ -3643,35 +3643,6 @@ PhaseStatus Compiler::lvaMarkLocalVars() unsigned const lvaCountOrig = lvaCount; -#if defined(FEATURE_EH_WINDOWS_X86) - - // Grab space for exception handling - - if (!UsesFunclets() && ehNeedsShadowSPslots()) - { - // The first slot is reserved for ICodeManager::FixContext(ppEndRegion) - // ie. the offset of the end-of-last-executed-filter - unsigned slotsNeeded = 1; - - unsigned handlerNestingLevel = ehMaxHndNestingCount; - - if (opts.compDbgEnC && (handlerNestingLevel < (unsigned)MAX_EnC_HANDLER_NESTING_LEVEL)) - handlerNestingLevel = (unsigned)MAX_EnC_HANDLER_NESTING_LEVEL; - - slotsNeeded += handlerNestingLevel; - - // For a filter (which can be active at the same time as a catch/finally handler) - slotsNeeded++; - // For zero-termination of the shadow-Stack-pointer chain - slotsNeeded++; - - lvaShadowSPslotsVar = lvaGrabTempWithImplicitUse(false DEBUGARG("lvaShadowSPslotsVar")); - lvaSetStruct(lvaShadowSPslotsVar, typGetBlkLayout(slotsNeeded * TARGET_POINTER_SIZE), false); - lvaSetVarAddrExposed(lvaShadowSPslotsVar DEBUGARG(AddressExposedReason::EXTERNALLY_VISIBLE_IMPLICITLY)); - } - -#endif // FEATURE_EH_WINDOWS_X86 - #ifdef JIT32_GCENCODER // LocAllocSPvar is only required by the implicit frame layout expected by the VM on x86. Whether // a function contains a Localloc is conveyed in the GC information, in the InfoHdrSmall.localloc From 0cd05724f3994532707219a96f1d497d549ca657 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Fri, 28 Feb 2025 15:17:09 -0800 Subject: [PATCH 06/18] reject inlinees with catch clauses (for now) --- src/coreclr/jit/fgbasic.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/coreclr/jit/fgbasic.cpp b/src/coreclr/jit/fgbasic.cpp index ec22a2a99c26ea..d758b3b687bc86 100644 --- a/src/coreclr/jit/fgbasic.cpp +++ b/src/coreclr/jit/fgbasic.cpp @@ -3439,6 +3439,28 @@ void Compiler::fgFindBasicBlocks() info.compCompHnd->getEHinfo(info.compMethodHnd, XTnum, &clause); noway_assert(clause.HandlerLength != (unsigned)-1); + // If we're inlining, and the inlinee hasa catch, we are currently + // unable to convey the type of the catch properly, as it is represented + // by a token. So, abandon inlining. + // + // TODO: if inlining methods with catches is rare, consider + // transforming class catches into runtime filters like we do in + // fgCreateFiltersForGenericExceptions + // + if (compIsForInlining()) + { + const bool isFinallyFaultOrFilter = + (clause.Flags & (CORINFO_EH_CLAUSE_FINALLY | CORINFO_EH_CLAUSE_FAULT | CORINFO_EH_CLAUSE_FILTER)) != + 0; + + if (!isFinallyFaultOrFilter) + { + JITDUMP("Inlinee EH clause %u is a catch; we can't inline these (yet)\n", XTnum); + compInlineResult->NoteFatal(InlineObservation::CALLEE_HAS_EH); + return; + } + } + if (clause.TryLength <= 0) { BADCODE("try block length <=0"); From 73abbf3e023bd7aa314398da645c60771a4ef9f6 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Fri, 28 Feb 2025 16:30:01 -0800 Subject: [PATCH 07/18] nesting level needsd to be one larger than actual level --- src/coreclr/jit/compiler.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index c47a726ee637c8..82e3577caed683 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -5183,8 +5183,8 @@ void Compiler::FinalizeEH() { // Recompute the handler nesting levels, as they may have changed. // - unsigned oldHandlerNestingLevel = ehMaxHndNestingCount; - ehMaxHndNestingCount = 0; + unsigned const oldHandlerNestingCount = ehMaxHndNestingCount; + ehMaxHndNestingCount = 0; if (compHndBBtabCount > 0) { @@ -5209,11 +5209,15 @@ void Compiler::FinalizeEH() HBtab->ebdHandlerNestingLevel = 0; } } + + // When there is EH, we need to record nesting level + 1 + // + ehMaxHndNestingCount++; } - if (oldHandlerNestingLevel != ehMaxHndNestingCount) + if (oldHandlerNestingCount != ehMaxHndNestingCount) { - JITDUMP("Finalize EH: max handler nesting level now %u (was %u)\n", oldHandlerNestingLevel, + JITDUMP("Finalize EH: max handler nesting count now %u (was %u)\n", oldHandlerNestingCount, ehMaxHndNestingCount); } From bc50b97ce0a26c1ac96595e071df5ac0e9f14c7f Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Mon, 3 Mar 2025 10:56:40 -0800 Subject: [PATCH 08/18] JIT: stop using ehTrueEnclosingTryIndexIL outside of importation Once we can inline methods with EH, IL ranges are no longer a reliable indicator of a mutual-protect try regions. Instead, after importation, we can rely on mutual-protect trys having the same start and end blocks. Also update other case where we were using `info.compXcptnsCount` in morph to decide if we needed a frame pointer. This lets us simplify the logic around frame pointers and EH (though I still think we're making up our minds too early). Contributes to #108900. --- src/coreclr/jit/codegencommon.cpp | 8 ++-- src/coreclr/jit/compiler.h | 5 +++ src/coreclr/jit/fgehopt.cpp | 2 +- src/coreclr/jit/flowgraph.cpp | 4 +- src/coreclr/jit/jiteh.cpp | 67 +++++++++++++++++++++++++++---- src/coreclr/jit/morph.cpp | 37 +++-------------- src/coreclr/jit/optimizer.cpp | 4 +- 7 files changed, 81 insertions(+), 46 deletions(-) diff --git a/src/coreclr/jit/codegencommon.cpp b/src/coreclr/jit/codegencommon.cpp index 7fb0d7982f93df..6edc4f3f6ea9e5 100644 --- a/src/coreclr/jit/codegencommon.cpp +++ b/src/coreclr/jit/codegencommon.cpp @@ -2355,8 +2355,8 @@ void CodeGen::genReportEH() { for (XTnum = 0; XTnum < compiler->compHndBBtabCount; XTnum++) { - for (enclosingTryIndex = compiler->ehTrueEnclosingTryIndexIL(XTnum); // find the true enclosing try index, - // ignoring 'mutual protect' trys + for (enclosingTryIndex = compiler->ehTrueEnclosingTryIndex(XTnum); // find the true enclosing try index, + // ignoring 'mutual protect' trys enclosingTryIndex != EHblkDsc::NO_ENCLOSING_INDEX; enclosingTryIndex = compiler->ehGetEnclosingTryIndex(enclosingTryIndex)) { @@ -2662,8 +2662,8 @@ void CodeGen::genReportEH() EHblkDsc* fletTab = compiler->ehGetDsc(XTnum2); - for (enclosingTryIndex = compiler->ehTrueEnclosingTryIndexIL(XTnum2); // find the true enclosing try index, - // ignoring 'mutual protect' trys + for (enclosingTryIndex = compiler->ehTrueEnclosingTryIndex(XTnum2); // find the true enclosing try index, + // ignoring 'mutual protect' trys enclosingTryIndex != EHblkDsc::NO_ENCLOSING_INDEX; enclosingTryIndex = compiler->ehGetEnclosingTryIndex(enclosingTryIndex)) { diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index e5d99dc152fd33..43b23d1372a84c 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -2779,6 +2779,9 @@ class Compiler // Find the true enclosing try index, ignoring 'mutual protect' try. Uses IL ranges to check. unsigned ehTrueEnclosingTryIndexIL(unsigned regionIndex); + // Find the true enclosing try index, ignoring 'mutual protect' try. Uses blocks to check. + unsigned ehTrueEnclosingTryIndex(unsigned regionIndex); + // Return the index of the most nested enclosing region for a particular EH region. Returns NO_ENCLOSING_INDEX // if there is no enclosing region. If the returned index is not NO_ENCLOSING_INDEX, then '*inTryRegion' // is set to 'true' if the enclosing region is a 'try', or 'false' if the enclosing region is a handler. @@ -5285,6 +5288,8 @@ class Compiler // This is derived from the profile data // or is BB_UNITY_WEIGHT when we don't have profile data + bool fgImportDone = false; // true once importation has finished + bool fgFuncletsCreated = false; // true if the funclet creation phase has been run bool fgGlobalMorph = false; // indicates if we are during the global morphing phase diff --git a/src/coreclr/jit/fgehopt.cpp b/src/coreclr/jit/fgehopt.cpp index 7ea85224a863bc..6e0b726266bad6 100644 --- a/src/coreclr/jit/fgehopt.cpp +++ b/src/coreclr/jit/fgehopt.cpp @@ -2736,7 +2736,7 @@ BasicBlock* Compiler::fgCloneTryRegion(BasicBlock* tryEntry, CloneTryInfo& info, break; } outermostEbd = ehGetDsc(enclosingTryIndex); - if (!EHblkDsc::ebdIsSameILTry(outermostEbd, tryEbd)) + if (!EHblkDsc::ebdIsSameTry(outermostEbd, tryEbd)) { break; } diff --git a/src/coreclr/jit/flowgraph.cpp b/src/coreclr/jit/flowgraph.cpp index e5eb99a3bd3bd2..c900ae003fe1dc 100644 --- a/src/coreclr/jit/flowgraph.cpp +++ b/src/coreclr/jit/flowgraph.cpp @@ -590,6 +590,8 @@ PhaseStatus Compiler::fgImport() INDEBUG(fgPgoDeferredInconsistency = false); } + fgImportDone = true; + return PhaseStatus::MODIFIED_EVERYTHING; } @@ -6140,7 +6142,7 @@ bool FlowGraphNaturalLoop::CanDuplicateWithEH(INDEBUG(const char** reason)) // const bool headerInTry = header->hasTryIndex(); unsigned blockIndex = block->getTryIndex(); - unsigned outermostBlockIndex = comp->ehTrueEnclosingTryIndexIL(blockIndex); + unsigned outermostBlockIndex = comp->ehTrueEnclosingTryIndex(blockIndex); if ((headerInTry && (outermostBlockIndex == header->getTryIndex())) || (!headerInTry && (outermostBlockIndex == EHblkDsc::NO_ENCLOSING_INDEX))) diff --git a/src/coreclr/jit/jiteh.cpp b/src/coreclr/jit/jiteh.cpp index 8f840953193b91..e647925f036edc 100644 --- a/src/coreclr/jit/jiteh.cpp +++ b/src/coreclr/jit/jiteh.cpp @@ -799,14 +799,24 @@ unsigned Compiler::ehGetMostNestedRegionIndex(BasicBlock* block, bool* inTryRegi return mostNestedRegion; } -/***************************************************************************** - * Returns the try index of the enclosing try, skipping all EH regions with the - * same try region (that is, all 'mutual protect' regions). If there is no such - * enclosing try, returns EHblkDsc::NO_ENCLOSING_INDEX. - */ +//------------------------------------------------------------- +// ehTrueEnclosingTryIndexIL: find the outermost enclosing try +// region that is not a mutual-protect try +// +// Arguments: +// regionIndex - index of interest +// +// Returns: +// Index of enclosng non-mutual protect try region, or EHblkDsc::NO_ENCLOSING_INDEX. +// +// Notes: +// Only safe to use during importation, before we have normalize the +// EH in the flow graph. Post importation use, the non-IL version. +// unsigned Compiler::ehTrueEnclosingTryIndexIL(unsigned regionIndex) { assert(regionIndex != EHblkDsc::NO_ENCLOSING_INDEX); + assert(!fgImportDone); EHblkDsc* ehDscRoot = ehGetDsc(regionIndex); EHblkDsc* HBtab = ehDscRoot; @@ -832,6 +842,49 @@ unsigned Compiler::ehTrueEnclosingTryIndexIL(unsigned regionIndex) return regionIndex; } +//------------------------------------------------------------- +// ehTrueEnclosingTryIndex: find the closest enclosing try +// region that is not a mutual-protect try +// +// Arguments: +// regionIndex - index of interest +// +// Returns: +// Index of enclosng non-mutual protect try region, or EHblkDsc::NO_ENCLOSING_INDEX. +// +// Notes: +// Only safe to use after importation, once we have normalized the +// EH in the flow graph. For importation, use the IL version. +// +unsigned Compiler::ehTrueEnclosingTryIndex(unsigned regionIndex) +{ + assert(regionIndex != EHblkDsc::NO_ENCLOSING_INDEX); + assert(fgImportDone); + + EHblkDsc* ehDscRoot = ehGetDsc(regionIndex); + EHblkDsc* HBtab = ehDscRoot; + + for (;;) + { + regionIndex = HBtab->ebdEnclosingTryIndex; + if (regionIndex == EHblkDsc::NO_ENCLOSING_INDEX) + { + // No enclosing 'try'; we're done + break; + } + + HBtab = ehGetDsc(regionIndex); + if (!EHblkDsc::ebdIsSameTry(ehDscRoot, HBtab)) + { + // Found an enclosing 'try' that has a different 'try' region (is not mutually-protect with the + // original region). Return it. + break; + } + } + + return regionIndex; +} + unsigned Compiler::ehGetEnclosingRegionIndex(unsigned regionIndex, bool* inTryRegion) { assert(regionIndex != EHblkDsc::NO_ENCLOSING_INDEX); @@ -3614,8 +3667,8 @@ void Compiler::fgVerifyHandlerTab() // on the block. for (XTnum = 0, HBtab = compHndBBtab; XTnum < compHndBBtabCount; XTnum++, HBtab++) { - unsigned enclosingTryIndex = ehTrueEnclosingTryIndexIL(XTnum); // find the true enclosing try index, - // ignoring 'mutual protect' trys + unsigned enclosingTryIndex = ehTrueEnclosingTryIndex(XTnum); // find the true enclosing try index, + // ignoring 'mutual protect' trys if (enclosingTryIndex != EHblkDsc::NO_ENCLOSING_INDEX) { // The handler funclet for 'XTnum' has a try index of 'enclosingTryIndex' (at least, the parts of the diff --git a/src/coreclr/jit/morph.cpp b/src/coreclr/jit/morph.cpp index 133671ac5afc06..b649967ef42521 100644 --- a/src/coreclr/jit/morph.cpp +++ b/src/coreclr/jit/morph.cpp @@ -13388,47 +13388,22 @@ void Compiler::fgSetOptions() codeGen->setFramePointerRequired(true); } - // Assert that the EH table has been initialized by now. Note that - // compHndBBtabAllocCount never decreases; it is a high-water mark - // of table allocation. In contrast, compHndBBtabCount does shrink - // if we delete a dead EH region, and if it shrinks to zero, the - // table pointer compHndBBtab is unreliable. - assert(compHndBBtabAllocCount >= info.compXcptnsCount); - -#ifdef TARGET_X86 - - // Note: this case, and the !X86 case below, should both use the - // !X86 path. This would require a few more changes for X86 to use - // compHndBBtabCount (the current number of EH clauses) instead of - // info.compXcptnsCount (the number of EH clauses in IL), such as - // in ehNeedsShadowSPslots(). This is because sometimes the IL has - // an EH clause that we delete as statically dead code before we - // get here, leaving no EH clauses left, and thus no requirement - // to use a frame pointer because of EH. But until all the code uses - // the same test, leave info.compXcptnsCount here. Also test for - // CORINFO_FLG_SYNCH methods which are converted into try-finally - // with Monitor helper calls in funclet ABI and need to be treated - // as methods with EH. - if (info.compXcptnsCount > 0 || (UsesFunclets() && (info.compFlags & CORINFO_FLG_SYNCH))) + // If there is EH, we need a frame pointer. + // Note this may premature... we can eliminate all EH after morph, sometimes. + // + if (compHndBBtabCount > 0) { codeGen->setFramePointerRequiredEH(true); +#ifdef TARGET_X86 if (UsesFunclets()) { assert(!codeGen->isGCTypeFixed()); // Enforce fully interruptible codegen for funclet unwinding SetInterruptible(true); } - } - -#else // !TARGET_X86 - - if (compHndBBtabCount > 0) - { - codeGen->setFramePointerRequiredEH(true); - } - #endif // TARGET_X86 + } if (compMethodRequiresPInvokeFrame()) { diff --git a/src/coreclr/jit/optimizer.cpp b/src/coreclr/jit/optimizer.cpp index fa27c21f9772f7..e0793bb0ee1e37 100644 --- a/src/coreclr/jit/optimizer.cpp +++ b/src/coreclr/jit/optimizer.cpp @@ -2866,7 +2866,7 @@ bool Compiler::optCreatePreheader(FlowGraphNaturalLoop* loop) { // Preheader should be in the true enclosing region of the header. // - preheaderEHRegion = ehTrueEnclosingTryIndexIL(preheaderEHRegion); + preheaderEHRegion = ehTrueEnclosingTryIndex(preheaderEHRegion); inSameRegionAsHeader = false; break; } @@ -5208,7 +5208,7 @@ void Compiler::fgSetEHRegionForNewPreheaderOrExit(BasicBlock* block) { // `next` is the beginning of a try block. Figure out the EH region to use. assert(next->hasTryIndex()); - unsigned newTryIndex = ehTrueEnclosingTryIndexIL(next->getTryIndex()); + unsigned newTryIndex = ehTrueEnclosingTryIndex(next->getTryIndex()); if (newTryIndex == EHblkDsc::NO_ENCLOSING_INDEX) { // No EH try index. From e25665b24dcac30ba7055e80ed617a85157019f9 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Tue, 4 Mar 2025 12:08:13 -0800 Subject: [PATCH 09/18] do not inline pinvoke methods with EH --- src/coreclr/jit/importer.cpp | 2 +- src/coreclr/jit/importercalls.cpp | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index 3b95a84444211a..1399be4fe0ce6d 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -12964,7 +12964,7 @@ void Compiler::impMakeDiscretionaryInlineObservations(InlineInfo* pInlineInfo, I // // Arguments: // fncHandle -- inline candidate method -// methInfo -- method info from VN +// methInfo -- method info from VM // forceInline -- true if method is marked with AggressiveInlining // inlineResult -- ongoing inline evaluation // diff --git a/src/coreclr/jit/importercalls.cpp b/src/coreclr/jit/importercalls.cpp index 098056fe84d7c3..a0e39099489822 100644 --- a/src/coreclr/jit/importercalls.cpp +++ b/src/coreclr/jit/importercalls.cpp @@ -7763,12 +7763,23 @@ void Compiler::impMarkInlineCandidateHelper(GenTreeCall* call, return; } - // We cannot inline methods with EH into filter clauses, even if marked as aggressive inline - // - if ((inlineCandidateInfo->methInfo.EHcount > 0) && bbInFilterBBRange(compCurBB)) + if (inlineCandidateInfo->methInfo.EHcount > 0) { - inlineResult->NoteFatal(InlineObservation::CALLSITE_IS_WITHIN_FILTER); - return; + // We cannot inline methods with EH into filter clauses, even if marked as aggressive inline + // + if (bbInFilterBBRange(compCurBB)) + { + inlineResult->NoteFatal(InlineObservation::CALLSITE_IS_WITHIN_FILTER); + return; + } + + // Do not inline pinvoke stubs with EH. + // + if ((methAttr & CORINFO_FLG_PINVOKE) != 0) + { + inlineResult->NoteFatal(InlineObservation::CALLEE_HAS_EH); + return; + } } // The old value should be null OR this call should be a guarded devirtualization candidate. From dfc40a8310a19e54eee44d4d75e12c3796123a6a Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Mon, 10 Mar 2025 08:27:09 -0700 Subject: [PATCH 10/18] make JitInlineLimit more useful for bisection --- src/coreclr/jit/fginline.cpp | 13 ++++++++++ src/coreclr/jit/inlinepolicy.cpp | 44 -------------------------------- 2 files changed, 13 insertions(+), 44 deletions(-) diff --git a/src/coreclr/jit/fginline.cpp b/src/coreclr/jit/fginline.cpp index a8ffe9577b9e92..dbbf387461a905 100644 --- a/src/coreclr/jit/fginline.cpp +++ b/src/coreclr/jit/fginline.cpp @@ -1035,6 +1035,19 @@ void Compiler::fgMorphCallInlineHelper(GenTreeCall* call, InlineResult* result, // Don't expect any surprises here. assert(result->IsCandidate()); +#if defined(DEBUG) + // Fail if we're inlining and we've reached the acceptance limit. + // + int limit = JitConfig.JitInlineLimit(); + unsigned current = m_inlineStrategy->GetInlineCount(); + + if ((limit >= 0) && (current >= static_cast(limit))) + { + result->NoteFatal(InlineObservation::CALLSITE_OVER_INLINE_LIMIT); + return; + } +#endif // defined(DEBUG) + if (lvaCount >= MAX_LV_NUM_COUNT_FOR_INLINING) { // For now, attributing this to call site, though it's really diff --git a/src/coreclr/jit/inlinepolicy.cpp b/src/coreclr/jit/inlinepolicy.cpp index d22676a62a3ea2..357cf6090b7957 100644 --- a/src/coreclr/jit/inlinepolicy.cpp +++ b/src/coreclr/jit/inlinepolicy.cpp @@ -911,21 +911,6 @@ int DefaultPolicy::DetermineCallsiteNativeSizeEstimate(CORINFO_METHOD_INFO* meth void DefaultPolicy::DetermineProfitability(CORINFO_METHOD_INFO* methodInfo) { - -#if defined(DEBUG) - - // Punt if we're inlining and we've reached the acceptance limit. - int limit = JitConfig.JitInlineLimit(); - unsigned current = m_RootCompiler->m_inlineStrategy->GetInlineCount(); - - if (!m_IsPrejitRoot && (limit >= 0) && (current >= static_cast(limit))) - { - SetFailure(InlineObservation::CALLSITE_OVER_INLINE_LIMIT); - return; - } - -#endif // defined(DEBUG) - assert(InlDecisionIsCandidate(m_Decision)); assert(m_Observation == InlineObservation::CALLEE_IS_DISCRETIONARY_INLINE); @@ -1134,20 +1119,6 @@ void RandomPolicy::DetermineProfitability(CORINFO_METHOD_INFO* methodInfo) assert(InlDecisionIsCandidate(m_Decision)); assert(m_Observation == InlineObservation::CALLEE_IS_DISCRETIONARY_INLINE); -#if defined(DEBUG) - - // Punt if we're inlining and we've reached the acceptance limit. - int limit = JitConfig.JitInlineLimit(); - unsigned current = m_RootCompiler->m_inlineStrategy->GetInlineCount(); - - if (!m_IsPrejitRoot && (limit >= 0) && (current >= static_cast(limit))) - { - SetFailure(InlineObservation::CALLSITE_OVER_INLINE_LIMIT); - return; - } - -#endif // defined(DEBUG) - // Budget check. const bool overBudget = this->BudgetCheck(); if (overBudget) @@ -2400,21 +2371,6 @@ bool DiscretionaryPolicy::PropagateNeverToRuntime() const void DiscretionaryPolicy::DetermineProfitability(CORINFO_METHOD_INFO* methodInfo) { - -#if defined(DEBUG) - - // Punt if we're inlining and we've reached the acceptance limit. - int limit = JitConfig.JitInlineLimit(); - unsigned current = m_RootCompiler->m_inlineStrategy->GetInlineCount(); - - if (!m_IsPrejitRoot && (limit >= 0) && (current >= static_cast(limit))) - { - SetFailure(InlineObservation::CALLSITE_OVER_INLINE_LIMIT); - return; - } - -#endif // defined(DEBUG) - // Make additional observations based on the method info MethodInfoObservations(methodInfo); From 695435bb2ff8b9aa47ecbe99c39f294f03b609f5 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Mon, 10 Mar 2025 18:36:08 -0700 Subject: [PATCH 11/18] possible arm64 fix --- src/coreclr/jit/codegenarm64.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/jit/codegenarm64.cpp b/src/coreclr/jit/codegenarm64.cpp index 87e64d6e0afc35..f1a8f9f6e3d604 100644 --- a/src/coreclr/jit/codegenarm64.cpp +++ b/src/coreclr/jit/codegenarm64.cpp @@ -3491,7 +3491,7 @@ void CodeGen::genCodeForNegNot(GenTree* tree) GenTree* operand = tree->gtGetOp1(); // The src must be a register. - if (tree->OperIs(GT_NEG, GT_NOT) && operand->isContained()) + if (tree->OperIs(GT_NEG, GT_NOT) && operand->isContained() && ((tree->gtFlags & GTF_SET_FLAGS) == 0)) { genTreeOps oper = operand->OperGet(); switch (oper) From 0ae2ad3670140cdc59059fdd6941a7fae9318a44 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Tue, 11 Mar 2025 07:44:56 -0700 Subject: [PATCH 12/18] Revert "possible arm64 fix" This reverts commit 695435bb2ff8b9aa47ecbe99c39f294f03b609f5. --- src/coreclr/jit/codegenarm64.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/jit/codegenarm64.cpp b/src/coreclr/jit/codegenarm64.cpp index f1a8f9f6e3d604..87e64d6e0afc35 100644 --- a/src/coreclr/jit/codegenarm64.cpp +++ b/src/coreclr/jit/codegenarm64.cpp @@ -3491,7 +3491,7 @@ void CodeGen::genCodeForNegNot(GenTree* tree) GenTree* operand = tree->gtGetOp1(); // The src must be a register. - if (tree->OperIs(GT_NEG, GT_NOT) && operand->isContained() && ((tree->gtFlags & GTF_SET_FLAGS) == 0)) + if (tree->OperIs(GT_NEG, GT_NOT) && operand->isContained()) { genTreeOps oper = operand->OperGet(); switch (oper) From 09738a573ddd06a3b2c516ab36ab72f564443dad Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Tue, 11 Mar 2025 07:46:08 -0700 Subject: [PATCH 13/18] better arm64 fix --- src/coreclr/jit/gentree.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index af9cea738e7682..cba67fb04ee114 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -19984,7 +19984,7 @@ bool GenTree::SupportsSettingZeroFlag() } // We do not support setting zero flag for madd/msub. - if (OperIs(GT_ADD, GT_SUB) && (!gtGetOp2()->OperIs(GT_MUL) || !gtGetOp2()->isContained())) + if (OperIs(GT_ADD, GT_SUB, GT_NEG) && (!gtGetOp2()->OperIs(GT_MUL) || !gtGetOp2()->isContained())) { return true; } From 7b4b06e0e7a8e58e235b95c5365419d9954657d9 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Tue, 11 Mar 2025 11:40:26 -0700 Subject: [PATCH 14/18] actual fix --- src/coreclr/jit/gentree.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index cba67fb04ee114..17ca0e3a46b1da 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -19978,13 +19978,18 @@ bool GenTree::SupportsSettingZeroFlag() } #endif #elif defined(TARGET_ARM64) - if (OperIs(GT_AND, GT_AND_NOT, GT_NEG)) + if (OperIs(GT_AND, GT_AND_NOT)) { return true; } // We do not support setting zero flag for madd/msub. - if (OperIs(GT_ADD, GT_SUB, GT_NEG) && (!gtGetOp2()->OperIs(GT_MUL) || !gtGetOp2()->isContained())) + if (OperIs(GT_NEG) && (!gtGetOp1()->OperIs(GT_MUL) || !gtGetOp1()->isContained())) + { + return true; + } + + if (OperIs(GT_ADD, GT_SUB) && (!gtGetOp2()->OperIs(GT_MUL) || !gtGetOp2()->isContained())) { return true; } From 22921634e251d3f9bcaec22b4d99310ea8362524 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Tue, 11 Mar 2025 15:59:31 -0700 Subject: [PATCH 15/18] do up front capacity check; fix some comments --- src/coreclr/jit/fgbasic.cpp | 20 ++++++++++++++++---- src/coreclr/jit/fginline.cpp | 17 ++++++++--------- src/coreclr/jit/importer.cpp | 2 -- src/coreclr/jit/inline.def | 1 + 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/coreclr/jit/fgbasic.cpp b/src/coreclr/jit/fgbasic.cpp index d758b3b687bc86..071430900499be 100644 --- a/src/coreclr/jit/fgbasic.cpp +++ b/src/coreclr/jit/fgbasic.cpp @@ -3425,21 +3425,33 @@ void Compiler::fgFindBasicBlocks() unsigned XTnum; - /* Are there any exception handlers? */ - + // Are there any exception handlers? + // if (info.compXcptnsCount > 0) { assert(!compIsForInlining() || opts.compInlineMethodsWithEH); - /* Check and mark all the exception handlers */ + if (compIsForInlining()) + { + // Verify we can expand the EH table as needed to incorporate the callee's EH clauses. + // Failing here should be extremely rare. + // + EHblkDsc* const dsc = fgTryAddEHTableEntries(0, info.compXcptnsCount, /* deferAdding */ true); + if (dsc == nullptr) + { + compInlineResult->NoteFatal(InlineObservation::CALLSITE_EH_TABLE_FULL); + } + } + // Check and mark all the exception handlers + // for (XTnum = 0; XTnum < info.compXcptnsCount; XTnum++) { CORINFO_EH_CLAUSE clause; info.compCompHnd->getEHinfo(info.compMethodHnd, XTnum, &clause); noway_assert(clause.HandlerLength != (unsigned)-1); - // If we're inlining, and the inlinee hasa catch, we are currently + // If we're inlining, and the inlinee has a catch clause, we are currently // unable to convey the type of the catch properly, as it is represented // by a token. So, abandon inlining. // diff --git a/src/coreclr/jit/fginline.cpp b/src/coreclr/jit/fginline.cpp index 9f7aacfdf99c22..c571bb92d17470 100644 --- a/src/coreclr/jit/fginline.cpp +++ b/src/coreclr/jit/fginline.cpp @@ -1615,29 +1615,31 @@ void Compiler::fgInsertInlineeBlocks(InlineInfo* pInlineInfo) // inlinee eh0 -> eh1 // inlinee eh1 -> eh2 // - // root eh0 remains as is + // root eh0 -> eh0 // inlineeIndexShift = compHndBBtabCount; insertBeforeIndex = compHndBBtabCount; } else { - // enclosingRegion is shifted up by one, eg EH#0 will report as 1.. - // - // // The call site is in an EH region, so we can put the inlinee EH clauses // just before the enclosing region // + // Note enclosingRegion is region index + 1. So EH#0 will be represented by 1 here. + // // For example, if the enclosing EH regions are try#2 and hnd#3, and the inlinee has 2 eh clauses // - // enclosingRegion will be 3 + // enclosingRegion will be 3 (try2 + 1) // inlineeIndexShift will be 2 // insertBeforeIndex will be 2 // // inlinee eh0 -> eh2 // inlinee eh1 -> eh3 // - // root eh2 -> eh4; + // root eh0 -> eh0 + // root eh1 -> eh1 + // + // root eh2 -> eh4 // root eh3 -> eh5 // inlineeIndexShift = enclosingRegion - 1; @@ -1940,9 +1942,6 @@ void Compiler::fgInsertInlineeBlocks(InlineInfo* pInlineInfo) // If the call site is not in a try and the callee has a throw, // we may introduce inconsistency. // - // Technically we should check if the callee has a throw not in a try, but since - // we can't inline methods with EH yet we don't see those. - // if (InlineeCompiler->fgThrowCount > 0) { JITDUMP("INLINER: may-throw inlinee\n"); diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index 18ec6147406c06..581dfe5c5d4ab1 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -12552,9 +12552,7 @@ void Compiler::impImport() // If the method had EH, we may be missing some pred edges // (notably those from BBJ_EHFINALLYRET blocks). Add them. - // Only needed for the root method, since inlinees can't have EH. // - // TODO --------------- if haseh... if (info.compXcptnsCount > 0) { impFixPredLists(); diff --git a/src/coreclr/jit/inline.def b/src/coreclr/jit/inline.def index 2b045ad5d20009..44d6e83929e0ba 100644 --- a/src/coreclr/jit/inline.def +++ b/src/coreclr/jit/inline.def @@ -133,6 +133,7 @@ INLINE_OBSERVATION(CANT_CLASS_INIT, bool, "can't class init", INLINE_OBSERVATION(COMPILATION_ERROR, bool, "compilation error", FATAL, CALLSITE) INLINE_OBSERVATION(COMPILATION_FAILURE, bool, "failed to compile", FATAL, CALLSITE) INLINE_OBSERVATION(EXPLICIT_TAIL_PREFIX, bool, "explicit tail prefix", FATAL, CALLSITE) +INLINE_OBSERVATION(EH_TABLE_FULL, bool, "callee has eh, eh table is full", FATAL, CALLSITE) INLINE_OBSERVATION(GENERIC_DICTIONARY_LOOKUP, bool, "runtime dictionary lookup", FATAL, CALLSITE) INLINE_OBSERVATION(HAS_CALL_VIA_LDVIRTFTN, bool, "call via ldvirtftn", FATAL, CALLSITE) INLINE_OBSERVATION(HAS_COMPLEX_HANDLE, bool, "complex handle access", FATAL, CALLSITE) From 2a3fab4f912786a17b2d21a267f23be0cebd17a9 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Tue, 11 Mar 2025 18:03:54 -0700 Subject: [PATCH 16/18] fix case where root method doesn't yet have an eh table --- src/coreclr/jit/jiteh.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/coreclr/jit/jiteh.cpp b/src/coreclr/jit/jiteh.cpp index 3356349e61adc4..7c57746cfc3f3e 100644 --- a/src/coreclr/jit/jiteh.cpp +++ b/src/coreclr/jit/jiteh.cpp @@ -1728,8 +1728,9 @@ EHblkDsc* Compiler::fgTryAddEHTableEntries(unsigned XTnum, unsigned count, bool if (deferAdding) { // We can add count entries... + // (we may not have allocated a table, so return a dummy non-null entry) // - return compHndBBtab; + return (EHblkDsc*)(0x1); } if (newCount > compHndBBtabAllocCount) From 5ea82efd2431745d7c04019539e3e552e1ddc24c Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Fri, 14 Mar 2025 12:49:59 -0700 Subject: [PATCH 17/18] enable EH tab verificatino for inlinees --- src/coreclr/jit/jiteh.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/coreclr/jit/jiteh.cpp b/src/coreclr/jit/jiteh.cpp index 747093edde4a32..5290a29ca24a34 100644 --- a/src/coreclr/jit/jiteh.cpp +++ b/src/coreclr/jit/jiteh.cpp @@ -3233,12 +3233,6 @@ void Compiler::dispOutgoingEHClause(unsigned num, const CORINFO_EH_CLAUSE& claus void Compiler::fgVerifyHandlerTab() { - if (compIsForInlining()) - { - // We don't inline functions with EH. Don't bother verifying the EH table in the inlinee Compiler. - return; - } - if (compHndBBtabCount == 0) { return; From b6e1a2c2ec5bdf14374d8f7c8d829a71f69de127 Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Sun, 16 Mar 2025 16:56:08 -0700 Subject: [PATCH 18/18] enable inlining case for loops with eh --- src/tests/JIT/opt/Cloning/loops_with_eh.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tests/JIT/opt/Cloning/loops_with_eh.cs b/src/tests/JIT/opt/Cloning/loops_with_eh.cs index ac0d1ee336b415..7cebcf9f7e4a7e 100644 --- a/src/tests/JIT/opt/Cloning/loops_with_eh.cs +++ b/src/tests/JIT/opt/Cloning/loops_with_eh.cs @@ -1242,7 +1242,6 @@ public static int Sum_TFLTFiTF(int[] data, int n) return sum; } - // [Fact] public static int Test_TFLITFiTF() => Sum_TFLITFiTF(data, n) - 131; public static int Sum_TFLITFiTF(int[] data, int n)