This repository was archived by the owner on Jan 23, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Do not inline methods that never return #6103
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8035,6 +8035,31 @@ GenTreePtr Compiler::fgMorphCall(GenTreeCall* call) | |
return result; | ||
} | ||
|
||
if (call->IsNoReturn()) | ||
{ | ||
// | ||
// If we know that the call does not return then we can set fgRemoveRestOfBlock | ||
// to remove all subsequent statements and change the call's basic block to BBJ_THROW. | ||
// As a result the compiler won't need to preserve live registers across the call. | ||
// | ||
// This isn't need for tail calls as there shouldn't be any code after the call anyway. | ||
// Besides, the tail call code is part of the epilog and converting the block to | ||
// BBJ_THROW would result in the tail call being dropped as the epilog is generated | ||
// only for BBJ_RETURN blocks. | ||
// | ||
// Currently this doesn't work for non-void callees. Some of the code that handles | ||
// fgRemoveRestOfBlock expects the tree to have GTF_EXCEPT flag set but call nodes | ||
// do not have this flag by default. We could add the flag here but the proper solution | ||
// would be to replace the return expression with a local var node during inlining | ||
// so the rest of the call tree stays in a separate statement. That statement can then | ||
// be removed by fgRemoveRestOfBlock without needing to add GTF_EXCEPT anywhere. | ||
// | ||
|
||
if (!call->IsTailCall() && call->TypeGet() == TYP_VOID) | ||
{ | ||
fgRemoveRestOfBlock = true; | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Somebody else on the jit team can chime in, but there should be some way to indicate that the return value is not actually going to be produced, so that this restriction can be lifted. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was primarily caused by an assert I hit in a test. It would seem that the call node needs to have |
||
|
||
return call; | ||
} | ||
|
75 changes: 75 additions & 0 deletions
75
tests/src/JIT/Performance/CodeQuality/Inlining/NoThrowInline.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using Microsoft.Xunit.Performance; | ||
using System; | ||
using System.Linq; | ||
using System.Runtime.CompilerServices; | ||
using System.Reflection; | ||
using System.Collections.Generic; | ||
using Xunit; | ||
|
||
[assembly: OptimizeForBenchmarks] | ||
[assembly: MeasureInstructionsRetired] | ||
|
||
public static class NoThrowInline | ||
{ | ||
#if DEBUG | ||
public const int Iterations = 1; | ||
#else | ||
public const int Iterations = 100000000; | ||
#endif | ||
|
||
static void ThrowIfNull(string s) | ||
{ | ||
if (s == null) | ||
ThrowArgumentNullException(); | ||
} | ||
|
||
static void ThrowArgumentNullException() | ||
{ | ||
throw new ArgumentNullException(); | ||
} | ||
|
||
// | ||
// We expect ThrowArgumentNullException to not be inlined into Bench, the throw code is pretty | ||
// large and throws are extremly slow. However, we need to be careful not to degrade the | ||
// non-exception path performance by preserving registers across the call. For this the compiler | ||
// will have to understand that ThrowArgumentNullException never returns and omit the register | ||
// preservation code. | ||
// | ||
// For example, the Bench method below has 4 arguments (all passed in registers on x64) and fairly | ||
// typical argument validation code. If the compiler does not inline ThrowArgumentNullException | ||
// and does not make use of the "no return" information then all 4 register arguments will have | ||
// to be spilled and then reloaded. That would add 8 unnecessary memory accesses. | ||
// | ||
|
||
[MethodImpl(MethodImplOptions.NoInlining)] | ||
static int Bench(string a, string b, string c, string d) | ||
{ | ||
ThrowIfNull(a); | ||
ThrowIfNull(b); | ||
ThrowIfNull(c); | ||
ThrowIfNull(d); | ||
|
||
return a.Length + b.Length + c.Length + d.Length; | ||
} | ||
|
||
[Benchmark] | ||
public static void Test() | ||
{ | ||
foreach (var iteration in Benchmark.Iterations) | ||
{ | ||
using (iteration.StartMeasurement()) | ||
{ | ||
Bench("a", "bc", "def", "ghij"); | ||
} | ||
} | ||
} | ||
|
||
public static int Main() | ||
{ | ||
return (Bench("a", "bc", "def", "ghij") == 10) ? 100 : -1; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I worry that this might be too general. I've seen cases where people have written performance-sensitive code that is effectively
while (true) ...
and then relied on exceptions to terminate processing, and such methods can benefit from being inlined.Ideally we'd like to check that the inlinee does very little other than throw an exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exception throwing has always been super slow path. I do not think there is any benefit in inlining infinite loops that are terminated by throwing exceptions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As @jkotas already pointed out exceptions are very slow anyway. The typical throw sequence already needs at least 3 calls - new, ctor and throw - and the throw itself is very expensive.
Note that this does not affect methods like this one:
Not inlining this would be bad since you'd pay the cost of a call even if
value
is not null.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some numbers: if in my test I pass a
null
to theTest
method then the execution time is 50 minutes instead of 500ms! That's 6000 times slower than the no-exception case.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exceptions are slow. But the point is that
X is postdominated by a throw
does not implyX is not performance sensitive
.This is a tricky thing to get right because there are cases where a relatively large amount of computation is done solely for the purpose of throwing a more detailed exception.
My preference would be to start off conservatively here, only matching simple methods with this heuristic, for instance methods that are loop-free and relatively small. This should catch most of the idiomatic throw helper cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had similar doubts myself back when I did this experiment but I just can't find a reasonable use case that could be impacted by this. That X needs to be small enough for the method to be an inline candidate and it has to contain a loop otherwise the performance X will be dwarfed by the throw. And then the loop performance needs to be better when inlined due to optimizations such as constant propagation, otherwise the only impact we could possible measure would be the delay of the loop start by the time taken by the call.
I'd love to see an example that shows that this is indeed a problem. The current implementation already fails to reject inlining in some cases where there's no reason to inline, I'm somewhat reluctant to impose additional limitations.
I'll have to check but I'm not sure there's any loop information available during inlining. Probably the next best thing would be to just match a single BB of type BBJ_THROW, possibly preceded by a list of BBJ_ALWAYS BBs. To make it even more conservative we could match only BBJ_THROW containing a single statement.
Wouldn't this be redundant as the inliner already takes code size into account?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the current inliner -- via LegacyPolicy -- is conservative with respect to size of inlinees. But I'd rather not bake that assumption in here. Going forward, policies will likely be somewhat less conservative.
It would be simple enough here to just impose a limit on the number of BB's. Or turn this into an informational observation and let the policy decide whether or not to inline, and set the limits there.