Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit c4da56e

Browse files
authored
Merge pull request #6103 from mikedn/nothrowinl
Do not inline methods that never return
2 parents 400ea02 + e3c3330 commit c4da56e

File tree

10 files changed

+318
-10
lines changed

10 files changed

+318
-10
lines changed

src/jit/flowgraph.cpp

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5750,14 +5750,48 @@ void Compiler::fgFindBasicBlocks()
57505750

57515751
if (compIsForInlining())
57525752
{
5753+
bool hasReturnBlocks = false;
5754+
bool hasMoreThanOneReturnBlock = false;
5755+
5756+
for (BasicBlock* block = fgFirstBB; block != nullptr; block = block->bbNext)
5757+
{
5758+
if (block->bbJumpKind == BBJ_RETURN)
5759+
{
5760+
if (hasReturnBlocks)
5761+
{
5762+
hasMoreThanOneReturnBlock = true;
5763+
break;
5764+
}
5765+
5766+
hasReturnBlocks = true;
5767+
}
5768+
}
5769+
5770+
if (!hasReturnBlocks && !compInlineResult->UsesLegacyPolicy())
5771+
{
5772+
//
5773+
// Mark the call node as "no return". The inliner might ignore CALLEE_DOES_NOT_RETURN and
5774+
// fail inline for a different reasons. In that case we still want to make the "no return"
5775+
// information available to the caller as it can impact caller's code quality.
5776+
//
5777+
5778+
impInlineInfo->iciCall->gtCallMoreFlags |= GTF_CALL_M_DOES_NOT_RETURN;
5779+
}
5780+
5781+
compInlineResult->NoteBool(InlineObservation::CALLEE_DOES_NOT_RETURN, !hasReturnBlocks);
5782+
5783+
if (compInlineResult->IsFailure())
5784+
{
5785+
return;
5786+
}
5787+
57535788
noway_assert(info.compXcptnsCount == 0);
57545789
compHndBBtab = impInlineInfo->InlinerCompiler->compHndBBtab;
57555790
compHndBBtabAllocCount = impInlineInfo->InlinerCompiler->compHndBBtabAllocCount; // we probably only use the table, not add to it.
57565791
compHndBBtabCount = impInlineInfo->InlinerCompiler->compHndBBtabCount;
57575792
info.compXcptnsCount = impInlineInfo->InlinerCompiler->info.compXcptnsCount;
57585793

5759-
if (info.compRetNativeType != TYP_VOID &&
5760-
fgMoreThanOneReturnBlock())
5794+
if (info.compRetNativeType != TYP_VOID && hasMoreThanOneReturnBlock)
57615795
{
57625796
// The lifetime of this var might expand multiple BBs. So it is a long lifetime compiler temp.
57635797
lvaInlineeReturnSpillTemp = lvaGrabTemp(false DEBUGARG("Inline candidate multiple BBJ_RETURN spill temp"));

src/jit/gentree.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2886,6 +2886,7 @@ struct GenTreeCall final : public GenTree
28862886
// know when these flags are set.
28872887

28882888
#define GTF_CALL_M_R2R_REL_INDIRECT 0x2000 // GT_CALL -- ready to run call is indirected through a relative address
2889+
#define GTF_CALL_M_DOES_NOT_RETURN 0x4000 // GT_CALL -- call does not return
28892890

28902891
bool IsUnmanaged() const { return (gtFlags & GTF_CALL_UNMANAGED) != 0; }
28912892
bool NeedsNullCheck() const { return (gtFlags & GTF_CALL_NULLCHECK) != 0; }
@@ -3006,6 +3007,8 @@ struct GenTreeCall final : public GenTree
30063007

30073008
bool IsVarargs() const { return (gtCallMoreFlags & GTF_CALL_M_VARARGS) != 0; }
30083009

3010+
bool IsNoReturn() const { return (gtCallMoreFlags & GTF_CALL_M_DOES_NOT_RETURN) != 0; }
3011+
30093012
unsigned short gtCallMoreFlags; // in addition to gtFlags
30103013

30113014
unsigned char gtCallType :3; // value from the gtCallTypes enumeration

src/jit/inline.def

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ INLINE_OBSERVATION(ARG_FEEDS_RANGE_CHECK, bool, "argument feeds range chec
7575
INLINE_OBSERVATION(BEGIN_OPCODE_SCAN, bool, "prepare to look at opcodes", INFORMATION, CALLEE)
7676
INLINE_OBSERVATION(BELOW_ALWAYS_INLINE_SIZE, bool, "below ALWAYS_INLINE size", INFORMATION, CALLEE)
7777
INLINE_OBSERVATION(CLASS_PROMOTABLE, bool, "promotable value class", INFORMATION, CALLEE)
78+
INLINE_OBSERVATION(DOES_NOT_RETURN, bool, "does not return", INFORMATION, CALLEE)
7879
INLINE_OBSERVATION(END_OPCODE_SCAN, bool, "done looking at opcodes", INFORMATION, CALLEE)
7980
INLINE_OBSERVATION(HAS_SIMD, bool, "has SIMD arg, local, or ret", INFORMATION, CALLEE)
8081
INLINE_OBSERVATION(HAS_SWITCH, bool, "has switch", INFORMATION, CALLEE)

src/jit/inline.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ struct InlineInfo
564564
bool hasSIMDTypeArgLocalOrReturn;
565565
#endif // FEATURE_SIMD
566566

567-
GenTree * iciCall; // The GT_CALL node to be inlined.
567+
GenTreeCall * iciCall; // The GT_CALL node to be inlined.
568568
GenTree * iciStmt; // The statement iciCall is in.
569569
BasicBlock * iciBlock; // The basic block iciStmt is in.
570570
};

src/jit/inlinepolicy.cpp

Lines changed: 100 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,21 +77,24 @@ InlinePolicy* InlinePolicy::GetPolicy(Compiler* compiler, bool isPrejitRoot)
7777

7878
#endif // defined(DEBUG) || defined(INLINE_DATA)
7979

80-
InlinePolicy* policy = nullptr;
80+
// Optionally install the ModelPolicy.
8181
bool useModelPolicy = JitConfig.JitInlinePolicyModel() != 0;
8282

8383
if (useModelPolicy)
8484
{
85-
// Optionally install the ModelPolicy.
86-
policy = new (compiler, CMK_Inlining) ModelPolicy(compiler, isPrejitRoot);
85+
return new (compiler, CMK_Inlining) ModelPolicy(compiler, isPrejitRoot);
8786
}
88-
else
87+
88+
// Optionally fallback to the original legacy policy
89+
bool useLegacyPolicy = JitConfig.JitInlinePolicyLegacy() != 0;
90+
91+
if (useLegacyPolicy)
8992
{
90-
// Use the legacy policy
91-
policy = new (compiler, CMK_Inlining) LegacyPolicy(compiler, isPrejitRoot);
93+
return new (compiler, CMK_Inlining) LegacyPolicy(compiler, isPrejitRoot);
9294
}
9395

94-
return policy;
96+
// Use the enhanced legacy policy by default
97+
return new (compiler, CMK_Inlining) EnhancedLegacyPolicy(compiler, isPrejitRoot);
9598
}
9699

97100
//------------------------------------------------------------------------
@@ -849,6 +852,96 @@ int LegacyPolicy::CodeSizeEstimate()
849852
}
850853
}
851854

855+
//------------------------------------------------------------------------
856+
// NoteBool: handle a boolean observation with non-fatal impact
857+
//
858+
// Arguments:
859+
// obs - the current obsevation
860+
// value - the value of the observation
861+
862+
void EnhancedLegacyPolicy::NoteBool(InlineObservation obs, bool value)
863+
{
864+
switch (obs)
865+
{
866+
case InlineObservation::CALLEE_DOES_NOT_RETURN:
867+
m_IsNoReturn = value;
868+
m_IsNoReturnKnown = true;
869+
break;
870+
871+
default:
872+
// Pass all other information to the legacy policy
873+
LegacyPolicy::NoteBool(obs, value);
874+
break;
875+
}
876+
}
877+
878+
//------------------------------------------------------------------------
879+
// NoteInt: handle an observed integer value
880+
//
881+
// Arguments:
882+
// obs - the current obsevation
883+
// value - the value being observed
884+
885+
void EnhancedLegacyPolicy::NoteInt(InlineObservation obs, int value)
886+
{
887+
switch (obs)
888+
{
889+
case InlineObservation::CALLEE_NUMBER_OF_BASIC_BLOCKS:
890+
{
891+
assert(value != 0);
892+
assert(m_IsNoReturnKnown);
893+
894+
//
895+
// Let's be conservative for now and reject inlining of "no return" methods only
896+
// if the callee contains a single basic block. This covers most of the use cases
897+
// (typical throw helpers simply do "throw new X();" and so they have a single block)
898+
// without affecting more exotic cases (loops that do actual work for example) where
899+
// failure to inline could negatively impact code quality.
900+
//
901+
902+
unsigned basicBlockCount = static_cast<unsigned>(value);
903+
904+
if (m_IsNoReturn && (basicBlockCount == 1))
905+
{
906+
SetNever(InlineObservation::CALLEE_DOES_NOT_RETURN);
907+
}
908+
else
909+
{
910+
LegacyPolicy::NoteInt(obs, value);
911+
}
912+
913+
break;
914+
}
915+
916+
default:
917+
// Pass all other information to the legacy policy
918+
LegacyPolicy::NoteInt(obs, value);
919+
break;
920+
}
921+
}
922+
923+
//------------------------------------------------------------------------
924+
// PropagateNeverToRuntime: determine if a never result should cause the
925+
// method to be marked as un-inlinable.
926+
927+
bool EnhancedLegacyPolicy::PropagateNeverToRuntime() const
928+
{
929+
//
930+
// Do not propagate the "no return" observation. If we do this then future inlining
931+
// attempts will fail immediately without marking the call node as "no return".
932+
// This can have an adverse impact on caller's code quality as it may have to preserve
933+
// registers across the call.
934+
// TODO-Throughput: We should persist the "no return" information in the runtime
935+
// so we don't need to re-analyze the inlinee all the time.
936+
//
937+
938+
bool propagate = (m_Observation != InlineObservation::CALLEE_DOES_NOT_RETURN);
939+
940+
propagate &= LegacyPolicy::PropagateNeverToRuntime();
941+
942+
return propagate;
943+
}
944+
852945
#ifdef DEBUG
853946

854947
//------------------------------------------------------------------------

src/jit/inlinepolicy.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,35 @@ class LegacyPolicy : public LegalPolicy
156156
bool m_MethodIsMostlyLoadStore :1;
157157
};
158158

159+
// EnhancedLegacyPolicy extends the legacy policy by rejecting
160+
// inlining of methods that never return because they throw.
161+
162+
class EnhancedLegacyPolicy : public LegacyPolicy
163+
{
164+
public:
165+
EnhancedLegacyPolicy(Compiler* compiler, bool isPrejitRoot)
166+
: LegacyPolicy(compiler, isPrejitRoot)
167+
, m_IsNoReturn(false)
168+
, m_IsNoReturnKnown(false)
169+
{
170+
// empty
171+
}
172+
173+
// Policy observations
174+
void NoteBool(InlineObservation obs, bool value) override;
175+
void NoteInt(InlineObservation obs, int value) override;
176+
177+
// Policy policies
178+
bool PropagateNeverToRuntime() const override;
179+
bool IsLegacyPolicy() const override { return false; }
180+
181+
protected:
182+
183+
// Data members
184+
bool m_IsNoReturn :1;
185+
bool m_IsNoReturnKnown :1;
186+
};
187+
159188
#ifdef DEBUG
160189

161190
// RandomPolicy implements a policy that inlines at random.

src/jit/jitconfigvalues.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ CONFIG_STRING(JitNoInlineRange, W("JitNoInlineRange"))
203203
CONFIG_STRING(JitInlineReplayFile, W("JitInlineReplayFile"))
204204
#endif // defined(DEBUG) || defined(INLINE_DATA)
205205

206+
CONFIG_INTEGER(JitInlinePolicyLegacy, W("JitInlinePolicyLegacy"), 0)
206207
CONFIG_INTEGER(JitInlinePolicyModel, W("JitInlinePolicyModel"), 0)
207208

208209
#undef CONFIG_INTEGER

src/jit/morph.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8033,6 +8033,31 @@ GenTreePtr Compiler::fgMorphCall(GenTreeCall* call)
80338033
return result;
80348034
}
80358035

8036+
if (call->IsNoReturn())
8037+
{
8038+
//
8039+
// If we know that the call does not return then we can set fgRemoveRestOfBlock
8040+
// to remove all subsequent statements and change the call's basic block to BBJ_THROW.
8041+
// As a result the compiler won't need to preserve live registers across the call.
8042+
//
8043+
// This isn't need for tail calls as there shouldn't be any code after the call anyway.
8044+
// Besides, the tail call code is part of the epilog and converting the block to
8045+
// BBJ_THROW would result in the tail call being dropped as the epilog is generated
8046+
// only for BBJ_RETURN blocks.
8047+
//
8048+
// Currently this doesn't work for non-void callees. Some of the code that handles
8049+
// fgRemoveRestOfBlock expects the tree to have GTF_EXCEPT flag set but call nodes
8050+
// do not have this flag by default. We could add the flag here but the proper solution
8051+
// would be to replace the return expression with a local var node during inlining
8052+
// so the rest of the call tree stays in a separate statement. That statement can then
8053+
// be removed by fgRemoveRestOfBlock without needing to add GTF_EXCEPT anywhere.
8054+
//
8055+
8056+
if (!call->IsTailCall() && call->TypeGet() == TYP_VOID)
8057+
{
8058+
fgRemoveRestOfBlock = true;
8059+
}
8060+
}
80368061

80378062
return call;
80388063
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.Xunit.Performance;
6+
using System;
7+
using System.Linq;
8+
using System.Runtime.CompilerServices;
9+
using System.Reflection;
10+
using System.Collections.Generic;
11+
using Xunit;
12+
13+
[assembly: OptimizeForBenchmarks]
14+
[assembly: MeasureInstructionsRetired]
15+
16+
public static class NoThrowInline
17+
{
18+
#if DEBUG
19+
public const int Iterations = 1;
20+
#else
21+
public const int Iterations = 100000000;
22+
#endif
23+
24+
static void ThrowIfNull(string s)
25+
{
26+
if (s == null)
27+
ThrowArgumentNullException();
28+
}
29+
30+
static void ThrowArgumentNullException()
31+
{
32+
throw new ArgumentNullException();
33+
}
34+
35+
//
36+
// We expect ThrowArgumentNullException to not be inlined into Bench, the throw code is pretty
37+
// large and throws are extremly slow. However, we need to be careful not to degrade the
38+
// non-exception path performance by preserving registers across the call. For this the compiler
39+
// will have to understand that ThrowArgumentNullException never returns and omit the register
40+
// preservation code.
41+
//
42+
// For example, the Bench method below has 4 arguments (all passed in registers on x64) and fairly
43+
// typical argument validation code. If the compiler does not inline ThrowArgumentNullException
44+
// and does not make use of the "no return" information then all 4 register arguments will have
45+
// to be spilled and then reloaded. That would add 8 unnecessary memory accesses.
46+
//
47+
48+
[MethodImpl(MethodImplOptions.NoInlining)]
49+
static int Bench(string a, string b, string c, string d)
50+
{
51+
ThrowIfNull(a);
52+
ThrowIfNull(b);
53+
ThrowIfNull(c);
54+
ThrowIfNull(d);
55+
56+
return a.Length + b.Length + c.Length + d.Length;
57+
}
58+
59+
[Benchmark]
60+
public static void Test()
61+
{
62+
foreach (var iteration in Benchmark.Iterations)
63+
{
64+
using (iteration.StartMeasurement())
65+
{
66+
Bench("a", "bc", "def", "ghij");
67+
}
68+
}
69+
}
70+
71+
public static int Main()
72+
{
73+
return (Bench("a", "bc", "def", "ghij") == 10) ? 100 : -1;
74+
}
75+
}

0 commit comments

Comments
 (0)