Skip to content

Refactor and simplify Inliner's weighCallSite routine#24110

Open
ymanton wants to merge 6 commits into
eclipse-openj9:masterfrom
ymanton:simplify-inliner-weighCallSite
Open

Refactor and simplify Inliner's weighCallSite routine#24110
ymanton wants to merge 6 commits into
eclipse-openj9:masterfrom
ymanton:simplify-inliner-weighCallSite

Conversation

@ymanton

@ymanton ymanton commented Jun 9, 2026

Copy link
Copy Markdown
Member

This PR contains a series of patches that try to make TR_MultipleCallTargetInliner::weighCallSite a 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:

java_lang_Class_newInstance
java_util_Arrays_fill
java_util_Arrays_equals
java_lang_String_equals
sun_io_ByteToCharSingleByte_convert
sun_io_ByteToCharDBCS_EBCDIC_convert
sun_io_CharToByteSingleByte_convert
sun_io_ByteToCharSingleByte_JITintrinsicConvert
java_math_BigDecimal_subMulAddAddMulSetScale

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.

@ymanton ymanton force-pushed the simplify-inliner-weighCallSite branch from 3842dc1 to 363e06c Compare June 10, 2026 17:53
@ymanton

ymanton commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

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.

@mpirvu

mpirvu commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

I am suggesting @nbhuiyan and @vijaysun-omr as reviewers.
We will need to run some throughput oriented benchmarks to make sure there are no regressions.

@nbhuiyan nbhuiyan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not seem to be mentioned in the commit msg, and also not defined/used anywhere.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@nbhuiyan nbhuiyan Jun 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ymanton ymanton Jun 22, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ymanton ymanton force-pushed the simplify-inliner-weighCallSite branch from 363e06c to e2d1058 Compare June 22, 2026 18:42
@ymanton

ymanton commented Jun 23, 2026

Copy link
Copy Markdown
Member Author

Regarding the size scaling based on getNumOfEstimatedCalls(), in the interest of understanding the previous behavior and the new behaviour, I created some tables that show the scaled size based on input sizes and number of calls for three different frequencies.

Green numbers indicate size was scaled down, inlining made more likely.
Red numbers indicate size was scaled up, inlining made less likely.

For frequency=1000

Original buggy equation:

image

Fixed equation:

image

For frequency=5000:

image image

For frequency=9000:

image image

The old buggy equation scaled the size down under regardless of the number of calls as expected, and quite aggressively based on frequency alone.

The fixed equation scales size down as frequency and number of calls increase, up to a cutoff point, and then scales size up. The behaviour looks sensible to me.

@vijaysun-omr

Copy link
Copy Markdown
Contributor

jenkins test sanity.functional all jdk25

@vijaysun-omr

Copy link
Copy Markdown
Contributor

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.

@ymanton

ymanton commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

AIX build failure is an infra issue. Mac x86-64 test failure is an instance of #24214.

@nbhuiyan nbhuiyan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this should probably be declared with static, at least for consistency with other places in the JIT.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, thanks.

}

TR_CallTarget *calltarget = callsite->getTarget(k);
bool TR_MultipleCallTargetInliner::shouldSkipReflectionMethod(TR_CallTarget *calltarget)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is answering whether something is a reflection method, not whether to skip. We should probably rename that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment could be fixed here to to describe that weighCallSite is going to terminate if <reason>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

ymanton added 6 commits June 25, 2026 13:14
- 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>
@ymanton ymanton force-pushed the simplify-inliner-weighCallSite branch from e2d1058 to 44c66db Compare June 25, 2026 20:17
@ymanton

ymanton commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

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.

Fixed, thanks.

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?

(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:

java_lang_Class_newInstance
java_util_Arrays_fill
java_util_Arrays_equals
java_lang_String_equals

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.

@ymanton

ymanton commented Jun 25, 2026

Copy link
Copy Markdown
Member Author

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.

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

if (frequency > borderFrequency) {
        // High frequency: reduce size to encourage inlining

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 if-else-ifs or whether the size scaling being done in each case makes sense, but I'll do that now and respond after.

@ymanton

ymanton commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

For reference, the scaling code handles frequencies in 4 buckets:

maxFrequency >= frequency > borderFrequency -> discussed in this PR; scale based on frequency and number of calls
borderFrequency >= frequency >= coldBorderFrequency -> no scaling
coldBorderFrequency > frequency >= veryColdBorderFrequency -> inverse linear w.r.t. frequency (size grows linearly as frequency decreases)
veryColdBorderFrequency > frequency >= 0 -> inverse quadratic w.r.t. frequency (size grows faster than linear as frequency decreases)

Based on that it certainly makes no sense for size to ever be scaled up if we're in the first bucket.

The easiest solution is to use a reciprocal-based formula that incorporates both frequency and number of calls:

normalizedFreq = frequency / maxFrequency;
numCalls = ecs->getNumOfEstimatedCalls();
scalingFactor = 1.0 / (1.0 + normalizedFreq * (1.0 + numCalls));
size = size * scalingFactor;

Scales based on frequency alone when numCalls==0, naturally clamped to the range [size,0).

At frequency=9000 it would look like this:

image

Alternatively, if we presume that number of calls as a factor has diminishing returns, we can use a slightly different formula that naturally reduces the influence of number of calls as that factor gets larger:

normalizedFreq = frequency / maxFrequency;
numCalls = ecs->getNumOfEstimatedCalls();
callFactor = log(1.0 + numCalls);
scalingFactor = 1.0 / (1.0 + normalizedFreq * (1.0 + callFactor));
size = size * scalingFactor;

Again when numCalls==0 it's the same as above (freq only scaling). As numCalls increases, callFactor increases slower, so 0->5 calls has a bigger impact than 20->50 calls.

At frequency=9000:

image

I'll test these alternatives and see what happens.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants