Need a mechanism to break up long methods without call overhead #69699
-
Often it is helpful to break up long functions into several shorter functions (for readability). However, this can have significant performance consequences, especially in inner-loops. I had originally posted, hoping that local functions might be that mechanism. Specifically, I hoped that under limited circumstances I could count on them being inlined. However, based on a reply from @jaredpar, I see that this may have been a simplistic hope. I'm not certain what the correct fix is for this somewhat common problem. In my own specific case, if I were able to tell JIT to always inline a specific local function, it would meet my needs...perhaps with something more consistently honored than A decoration on some sort of named local block, perhaps telling a pre-processor to cut/paste that section of code to a different location (where it was referenced) would also work. Since the solutions sort of straddle multiple spaces: C# language, JIT, and C# compiler. I'll leave this post here for now. Perhaps, I'll try posting elsewhere as well. More likely, I'll just learn to live with my long messy method :) I was simply hoping I could get something like the following: public class MyClass
{
public void MyLongMethod()
{
Step1(); // Called in this single place and nowhere else
Step2(); // Called in this single place and nowhere else
Step3(); // Called in this single place and nowhere else
MultipleUse(); // Called at least twice (first call)
MultipleUse(); // Called at least twice (second call)
void Step1()
{
// Logic for step 1
}
void Step2()
{
// Logic for step 2
}
void Step3()
{
// Logic for step 3
}
void MultipleUse
{
// Logic for multiple use
}
}
} To build with the same performance as: public void MyLongMethod()
{
// Logic for step 1
// Logic for step 2
// Logic for step 3
MultipleUse(); // Called at least twice (first call)
MultipleUse(); // Called at least twice (second call)
void MultipleUse
{
// Logic for multiple use
}
}
} |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 16 replies
-
Inlining a function will not necessarily improve performance. In particular, it can stop other optimizations from happening. The best place for this to happen is in the JIT where they ahve all the information and can do things like PGO. |
Beta Was this translation helpful? Give feedback.
-
As mentioned, I can understand that where a function has multiple sites of invocation, there are all sorts of considerations where the cost of inlining outweighs the benefits. Can you provide a concrete example where inlining a single-reference, local function, thus saving a call, would not provide a performance improvement? If this is the case, this suggests there are advantages beyond readability/maintainability to breaking up longer functions. However, this would be contrary to almost everything I've ever read on the topic or seen in my own benchmarking. UPDATE: Also, as mentioned, I'm not against this happening in JIT as long as it was well-documented and consistent. As it is, I have a real-world issue where I have a necessarily very long method that is called inside an inner loop. I hate that I can't break it up for readability without trashing performance. I've gotten as far as I can using aggressive inlining hints, but the size limitations for JIT inlining are killing me on this one. |
Beta Was this translation helpful? Give feedback.
-
This is not a relevant example. The throw helpers you mentioned are usually called in multiple places. Otherwise, why bother writing them?Also, they are generally not local functions...sometimes private instance, but not local/nested. |
Beta Was this translation helpful? Give feedback.
-
Thank you for your interest. Without a relevant example, I can't really comment on your opinion. I've updated my original post to provide greater clarity, and an example, since the term "local function" might be confused for "private instance function" and since the meaning of single-reference might have been unclear. |
Beta Was this translation helpful? Give feedback.
-
I disagree with the little risk part of this assessment. @CyrusNajmabadi has outlined many of the risks but the ones that always come to mind for me are: Inlining can reduce optimizations.The JIT operates on a series of heuristics and inlining methods can trigger heuristics that cause the JIT to generate less efficient code. A classic example of that problem is void M(string arg) {
if (arg is null) {
ThrowInvalidArg();
return;
}
...
void ThrowArgNull() => throw new InvalidArgumentException();
} This is a case where inlining a method subverts the intent of the developer and generates less efficient code as a result. The compiler cannot know what heuristics are in play at compile time because they come from the underlying runtime. A given piece of compiled code can execute on many different runtimes. As such we can't even limit such inlinings to places where it won't trip up JIT heuristics (except in a few narrow cases). Inlining can introduce crashesOne reason for having local functions is to reduce the size of the current stack frame. A common pattern in recursive methods that except deep recursion is to use local functions for intermediate calculations. Essentially it's a scratch buffer to hold locals that don't need to persist across recursion calls. This reduces the total stack frame size and hence increases the depth to which recursion can happen. void M(MyType input) {
if (input ...) {
M(Reduce(input));
}
}
MyType Reduce(MyType input) {
Locals ...
}
} Inlining here would once again subvert the intent of the developer, cause stack frame size to increase and potentially lead to program crashes. This is a very real problem the C# compiler itself faces. Inlining is not possibleThere are other cases where inlining is just not possible:
Inlining raises questions ...Local functions can have metadata attached to them like attributes. The compiler has to assume the attributes have meaning and will be inspected by other tools. What do we do with the attributes when we inline? Dropping them means we're very likely subverting other tooling that expected them. Moving them to the calling method is not possible (can conflict, changes intent of developer, etc ...). Little items like this pop up in a lot of cases and don't have clear answers. Overall this is not a "little risk" issue. It's an area that has significant risk points that have to be evaluated and considered. |
Beta Was this translation helpful? Give feedback.
I disagree with the little risk part of this assessment. @CyrusNajmabadi has outlined many of the risks but the ones that always come to mind for me are:
Inlining can reduce optimizations.
The JIT operates on a series of heuristics and inlining methods can trigger heuristics that cause the JIT to generate less efficient code. A classic example of that problem is
throw
. Historically the JIT does not optimize methods that usethrow
as good as it does methods that don't. In high perf code bases it's a common pattern to use throw helper methods to alleviate this problem