Refactor and simplify Inliner's weighCallSite routine#24110
Conversation
3842dc1 to
363e06c
Compare
|
Added 363e06c which is a fix for something I believe to be a bug. When scaling the estimated size of a candidate for inlining based on frequency and the number of estimated calls nested in the candidate, the calculation inadvertently ignores the number of estimated calls and uses a value derived from the frequency twice. This doesn't appear to be the intention of the code at all. |
|
I am suggesting @nbhuiyan and @vijaysun-omr as reviewers. |
nbhuiyan
left a comment
There was a problem hiding this comment.
This is a very important piece of work in my opinion, and a move in the right direction. I have a few comments in this first round (of many!) review of your changes.
| virtual bool supportsMultipleTargetInlining() { return true; } | ||
|
|
||
| private: | ||
| int32_t estimateCallTargetSizeFromCompiledBodyIfAvailable(TR_CallTarget *callTarget); |
There was a problem hiding this comment.
This does not seem to be mentioned in the commit msg, and also not defined/used anywhere.
There was a problem hiding this comment.
This was from a later patch that isn't part of this PR. Removed, thanks.
| if (shouldSkipReflectionMethod(calltarget)) | ||
| return; | ||
|
|
||
| if (calltarget->_calleeSymbol->getRecognizedMethod() == TR::java_lang_Class_newInstance |
There was a problem hiding this comment.
This might fail the functionality-preserving claim of the refactoring work. The original state was that these recognized methods would go through ECS and then upon return, were being set as calltarget->_partialInline->setCallNodeTreeTop(callNodeTreeTop);, followed by a fixed size 10, and then skip all of the stuff in the else block of the original code. However, they now get various adjustments in the original code's else block that were previously skipped (for example, cold block frequency scaling). One easy way around this might be to add all these methods to alwaysWorthInlining so that the scaled frequency does not impact their inlining outcomes.
Edit: I read the PR description discussing this earlier, but forgot about it when I was doing the review, where you pointed that out. However, my comment is still the same regarding the fact that this refactoring has a functional change that's not documented in the commit message.
There was a problem hiding this comment.
If the intention behind having special handling of these few methods was to always inline them, then maybe they could be removed entirely from weighCallSite and instead be all part of alwaysWorthInlining.
There was a problem hiding this comment.
Yeah, adding them to alwaysWorthInlining would make more sense since that is ultimately the intent here, but that's called in a few places where as this set of methods is only considered here. I'll test that out and see what it affects.
I've modified the commit message to remove the erroneous text.
There was a problem hiding this comment.
I've also moved
if (calltarget->_isPartialInliningCandidate && calltarget->_partialInline)
calltarget->_partialInline->setCallNodeTreeTop(callNodeTreeTop);
out of and after estimateCallTargetSize() since it has nothing to do with size estimation and is done for both the recognized methods and all other methods when ecs->calculateCodeSize(...) == true.
There was a problem hiding this comment.
Yeah, adding them to alwaysWorthInlining would make more sense since that is ultimately the intent here, but that's called in a few places where as this set of methods is only considered here. I'll test that out and see what it affects.
Any update on this?
| float avgMethodSize = (float)size / (float)ecs->getNumOfEstimatedCalls(); | ||
| float numCallsFactor = (float)(avgMethodSize) / 110.0f; | ||
| numCallsFactor = std::max(factor, 0.1f); | ||
| numCallsFactor = std::max(numCallsFactor, 0.1f); |
There was a problem hiding this comment.
Interestingly, the original error actually shielded us from a potential issue. ecs->getNumOfEstimatedCalls() gets us _numOfEstimatedCalls, which starts at 0. The only way it increases is if method being estimated has callees to analyse recursively, which is the only thing that increments that field. So for a leaf method that was analyzed by ECS, the value returned would be 0, so we have a division by zero here, so now numCallsFactor will be +infinity in many instances. Then the size is obtained multiplying infinity with factor and size, followed by an int cast, which I believe is undefined if numCallsFactor was infinity.
The fix would be first checking if numEstimatedCalls is >= 1.
There was a problem hiding this comment.
Good catch, thanks. I've changed the denominator to be 1 + ecs->getNumOfEstimatedCalls() since if the call target has no nested calls size should be estimated entirely from the call target itself and avgMethodSize should be equal to size.
There was a problem hiding this comment.
Thinking about this again, in a scenario where we have a method with size 2000 at this point, with numEstimatedCalls() 0, then the numCallsFactor would be 18. Let's say the frequency is 9000+, so this would be a candidate we would want to inline, and therefore scaled down. In this case, what happens is the opposite of what this code is claiming to do. We multiply 2000 * factor * 18, and let's say the factor is 0.4f, so then it results in size getting blown up by 7x in a place that intends to reduce size of such callees. The tables you shared demonstrate that pattern and the undefined cuttoff point, and I think it highlights the need for some sort of adjustments we should make. The purpose of this path is to reduce sizes, it should not also be silently blowing up sizes in some scenario like the one I just described, and all that method would need to not get blown up in size is to have 100+ nested callees, which does not make sense to me.
The reason we need to be extra careful with this is that this change is going to have a big impact on inlining outcomes across the board, as the buggy equation that always scaled down large callees is now changed to exclude lots of callees that were not blown up in size before.
Let me know your thoughts on this.
363e06c to
e2d1058
Compare
|
jenkins test sanity.functional all jdk25 |
|
I doubt the failures here are "real" since they were also seen in #24199 where I launched testing around the same time, but you can check and post. |
|
AIX build failure is an infra issue. Mac x86-64 test failure is an instance of #24214. |
nbhuiyan
left a comment
There was a problem hiding this comment.
I have done another round of review, and have a few additional comments inline.
A nit: based on CONTRIBUTING.md:
In addition, individual commits that include GenAI authored content must attribute that at the end of the commit message (before any human authorship signatures).
Assisted-by: IBM Bob appears at the end of your commit fed73ea.
| if (shouldSkipReflectionMethod(calltarget)) | ||
| return; | ||
|
|
||
| if (calltarget->_calleeSymbol->getRecognizedMethod() == TR::java_lang_Class_newInstance |
There was a problem hiding this comment.
Yeah, adding them to alwaysWorthInlining would make more sense since that is ultimately the intent here, but that's called in a few places where as this set of methods is only considered here. I'll test that out and see what it affects.
Any update on this?
| } | ||
| } | ||
| tracer()->insertCounter(ECS_Failed, callNodeTreeTop); | ||
| heuristicTrace(tracer(), "Not Adding Call Target %p to list of targets to be inlined"); |
There was a problem hiding this comment.
This is a pre-existing bug where we have a %p without the corresponding arg. Since this code is touched here it makes sense to fix that.
There was a problem hiding this comment.
Fixed via another patch on top.
| float avgMethodSize = (float)size / (float)ecs->getNumOfEstimatedCalls(); | ||
| float numCallsFactor = (float)(avgMethodSize) / 110.0f; | ||
| numCallsFactor = std::max(factor, 0.1f); | ||
| numCallsFactor = std::max(numCallsFactor, 0.1f); |
There was a problem hiding this comment.
Thinking about this again, in a scenario where we have a method with size 2000 at this point, with numEstimatedCalls() 0, then the numCallsFactor would be 18. Let's say the frequency is 9000+, so this would be a candidate we would want to inline, and therefore scaled down. In this case, what happens is the opposite of what this code is claiming to do. We multiply 2000 * factor * 18, and let's say the factor is 0.4f, so then it results in size getting blown up by 7x in a place that intends to reduce size of such callees. The tables you shared demonstrate that pattern and the undefined cuttoff point, and I think it highlights the need for some sort of adjustments we should make. The purpose of this path is to reduce sizes, it should not also be silently blowing up sizes in some scenario like the one I just described, and all that method would need to not get blown up in size is to have 100+ nested callees, which does not make sense to me.
The reason we need to be extra careful with this is that this change is going to have a big impact on inlining outcomes across the board, as the buggy equation that always scaled down large callees is now changed to exclude lots of callees that were not blown up in size before.
Let me know your thoughts on this.
|
|
||
| void TR_MultipleCallTargetInliner::weighCallSite(TR_CallStack *callStack, TR_CallSite *callsite, | ||
| bool currentBlockHasExceptionSuccessors, bool dontAddCalls) | ||
| int32_t getCallNodeFrequency(TR::Compilation *comp, TR::TreeTop *callNodeTreeTop, TR_RandomGenerator *randomGen) |
There was a problem hiding this comment.
nit: this should probably be declared with static, at least for consistency with other places in the JIT.
| } | ||
|
|
||
| TR_CallTarget *calltarget = callsite->getTarget(k); | ||
| bool TR_MultipleCallTargetInliner::shouldSkipReflectionMethod(TR_CallTarget *calltarget) |
There was a problem hiding this comment.
This is answering whether something is a reflection method, not whether to skip. We should probably rename that.
There was a problem hiding this comment.
Renamed and tidied up the code inside and around its call site.
| heuristicTrace(tracer(), "222 Weighing Call Target %p (node = %p)", calltarget, callNode); | ||
|
|
||
| numCallsFactor = std::max(factor, 0.1f); | ||
| // Skip reflection methods |
There was a problem hiding this comment.
The comment could be fixed here to to describe that weighCallSite is going to terminate if <reason>
There was a problem hiding this comment.
I removed the comment altogether, it was added by AI for whatever reason and the code reads pretty clearly now after some tweaking, so such a comment would be low information at best.
It would be worth having a comment explaining at a higher level why bailing out of weighing the entire call site is appropriate if at least one target is a generated reflection method while the top-level method is not, but I couldn't figure out why after a bit of searching.
- Consistently refer to callNode, callNodeTreeTop - Get rid of `if (callNodeTreeTop)` because it can't be null - Add a static function called getCallNodeFrequency() and do all the frequency stuff in there. - Remove size tweaks for java_util_GregorianCalendar_computeFields that are only done under -Xaggressive. - Remove unused `isCold` flag Signed-off-by: Younes Manton <ymanton@ca.ibm.com>
This refactoring was done by IBM Bob with some hand-holding by me. Summary of Bob's results: Helper Methods Created: 1. shouldSkipReflectionMethod() - Checks if a call target is a reflection method that should be skipped 2. estimateCallTargetSize() - Handles size estimation for call targets, including recognized methods and exception handling 3. applySizeAdjustmentHeuristics() - Applies size adjustments based on method characteristics (synchronized, BigDecimal, toString, etc.) 4. getFrequencyThresholds() - Determines frequency thresholds based on compilation mode and options 5. scaleBasedOnFrequency() - Scales call target size based on block frequency and large compiled method checks Results: - All helper methods declared in J9Inliner.hpp - All helper methods implemented in InlinerTempForJ9.cpp - Main weighCallSite function refactored to use helpers - Project builds successfully - JVM runs correctly (verified with java -version) The refactoring improves code maintainability by breaking down the 300+ line function into logical, reusable components while preserving all original functionality. Signed-off-by: Younes Manton <ymanton@ca.ibm.com> Assisted-by: IBM Bob
TR_CompiledMethodCallGraphThreshold and TR_CompiledMethodByteCodeThreshold both override values that can be set just as easily via -Xjit options. Signed-off-by: Younes Manton <ymanton@ca.ibm.com>
Passed in variables are easily accessed via options so there's no need to burden the caller. Signed-off-by: Younes Manton <ymanton@ca.ibm.com>
When scaling the estimated size of a candidate for inlining based on frequency and the number of estimated calls nested in the candidate, the calculation inadvertently ignores the number of estimated calls and uses a value derived from the frequency twice. This doesn't appear to be the intention of the code at all. Signed-off-by: Younes Manton <ymanton@ca.ibm.com>
Signed-off-by: Younes Manton <ymanton@ca.ibm.com>
e2d1058 to
44c66db
Compare
Fixed, thanks.
(Can't respond to this inline, reproducing here.) Most of these methods are obsolete and no longer exist in any JCL we support. The ones that are still relevant are: None of the above are relevant in the benchmarks I've been running, so I didn't observe any difference. It's possible that the above are important to some application or benchmark somewhere, but given that they appear together in a list with methods that haven't existed since before JDK8 and the inliner has (I assume) changed significantly since then I'd be inclined to just remove this code path altogether and treat these methods like any other until there's a fresh reason to do otherwise. |
Had a hard time understanding why you said "The purpose of this path is to reduce sizes, it should not also be silently blowing up sizes in some scenario like the one I just described" until I looked at the surrounding code. I assume your point is that the formula used inside the following check together with the accompanying comment has no business scaling up the size under any circumstances, especially since the adjacent code handles the lower frequencies with alternative formulas. I'd agree with that; sadly the bug was in some ways more true to the ultimate intention it seems. I didn't really pay attention to the other parts of that chain of |








This PR contains a series of patches that try to make
TR_MultipleCallTargetInliner::weighCallSitea little easier to work with in the future. Probably easier to review commit by commit. Note that the heavy lifting for 2582e0b was done by AI.Note that a handful of methods that were previously not subjected to the full processing of
weighCallSite()will now go through the path that all other methods take. The methods are:The bytecode sizes of these methods were estimated (and along the way other facts learned), but they were then given a fixed size of 10 and the size was not scaled up/down based on other heuristics. The refactoring will cause them to still be given a size of 10 but the size will then go through the rest of the heuristics, and they could potentially be removed from the list of inlining candidates if they exceed the size threshold.