Ensure Log2 can actually be imported as intrinsic - #128678

Merged
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2
Jun 2, 2026
Merged

Ensure Log2 can actually be imported as intrinsic#128678
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2

Conversation

@tannergooding

Copy link
Copy Markdown
Member

Previously this was code was never being hit because we were checking against the non-precise var_type and so we always saw TYP_INT even for unsigned inputs. Now we correctly handle checking for unsigned inputs and allow importing signed ones as well.

CopilotAI review requested due to automatic review settings May 28, 2026 03:48
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 28, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the CoreCLR JIT importer for the NI_PRIMITIVE_Log2 primitive intrinsic to correctly distinguish signed vs. unsigned inputs using the precise type, enabling the intrinsic expansion for unsigned arguments and adding a conditional throw path for signed arguments.

Changes:

  • Fixes signed/unsigned detection for Log2 by using JitType2PreciseVarType(baseJitType) rather than the non-precise baseType.
  • Implements the Log2 expansion via LeadingZeroCount(value | 1) to satisfy the 0 -> 0 contract.
  • Adds a conditional throw for negative signed inputs using a qmark-based fallback path.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergoodingforce-pushed the better-log2 branch 2 times, most recently from f76559c to eaeb042CompareMay 28, 2026 06:38
CopilotAI review requested due to automatic review settings May 28, 2026 06:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
CopilotAI review requested due to automatic review settings May 28, 2026 16:43

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergooding marked this pull request as ready for review May 29, 2026 02:37
CopilotAI review requested due to automatic review settings May 29, 2026 02:37
@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK) and move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

The diffs here are significant. We see -321k bytes of codegen on Linux Arm64

Literally 99% of diffs are in libraries_tests.run. and all example diffs imply that

[Intrinsic]publicstaticuintLog2(uintvalue)=>(uint)BitOperations.Log2(value);

is not inlined. I suspect it might be either PGO-driven or it needs an AggressiveInlining.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:
{A3A3FBCC-75AB-42F7-A074-1D18928B1FED}

or just slap AggressiveInlining everywhere

cc @dotnet/jit-contrib for opinions

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

Copy link
Copy Markdown
MemberAuthor

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK)

I am doubtful that QMARK will ever fully go away as they have a significant benefit in that they allow introducing block like functionality (namely a compare + branch to throw an exception) into the code. If anything, it will just be replaced with a helper that does the creation of the if/else blocks at the point you're currently creating the QMARK node instead. They exist because it simplifies a bit of handling in the early phases and even allows some constant folding without forcing the block to actually be produced.

move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

Most compilers do this for the fundamental math APIs like this, we're not special and even have stricter timing requirements which means there is more reason for us to handle such APIs intrinsically as it significantly reduces the amount of work the JIT has to do. The inliner is always going to have limitations and there's no reason to contribute to its pessimizations for these foundational APIs everything else is built on.

Literally 99% of diffs are in libraries_tests.run.

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others. That's why I explicitly called out the hits we also have in other areas. But that's also representative of this code being "hot" and common to code that most apps and binaries end up using, even if indirectly.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:

I don't think this is a fix and it just pushes things down the road more.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000:ldarg.0IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)IL_0006: ret

But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't. We skip all those checks, all the eating of the budget, and all the expensive handling by treating these key APIs as intrinsic. It completely bypasses the other expensive transforms and checks which ultimately simplifies what the JIT has to do, by adding a few lines to the importer to handle the known special scenarios.

@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am doubtful that QMARK will ever fully go away as they have a significant benefit

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

The inliner is always going to have limitations

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users? I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about. Are you going to continue pushing various small methods as JIT intrinsics?

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

I do think we need to stop using it as an excuse. I agree on importing things as special opcodes so then JIT can emit those opcodes as part of other transformations and rely on proper expansion - that made sense to me, but it's no the case here as we just mimic inliner's work, not more than that.
Another downside of all of these intrinsics is that we continue adding binary size to jit while the original C# code can be trimmed.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000: ldarg.0
IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)
IL_0006: ret
But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't

I think we need to study why we give up, it might be something fundamental like abstract generic resolution in importer.
Or you just copied a diff from Tier0 that is not inlined. What is the Tier1 method we should look at?

Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

tannergooding commented May 29, 2026

Copy link
Copy Markdown
MemberAuthor

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

My point here was rather that we have a fundamental need to introduce IR that represents x = cond ? throw : y and right now that is handled via QMARK. Even if we remove QMARK, it will just have to be replaced by something else that puts in the relevant blocks directly instead.

So even if they aren't the best IR today, the code is logically correct and will maintain roughly the current shape even when QMARKs disappear.

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users?

Because intrinsifying everything is both impossible and a negative. Most methods are not foundational in that sense and it would effectively cause "too much inlining", regress throughput, regress codegen size, etc.

Log2 is such a foundational helper, it and many of the APIs on the primitive types are the building blocks for all the other algorithms. It is an API that most compilers explicitly recognize and handle as intrinsic in some fashion, accordingly.

I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about.

I agree, but we also know this is much more complex work and that it will always have limits. So for foundational cases that are common to other compilers and where we can avoid some of the redundant work, it makes sense.

Are you going to continue pushing various small methods as JIT intrinsics?

The short answer is not really, fixing the existing Log2 handling really rounds out the set of them.

I'm not and have never been looking to intrinsify the world, only the key foundational math APIs like this one, i.e. the building blocks for the rest of .NET. Most of these were already handled and Log2 was one where we notably had handling but it was never firing because we were only checking TYP_INT and not TYP_UINT.

Of the foundational math APIs that aren't intrinsified, we only have integral Abs, Log10, Max, and Min. However, these likely aren't ever going to get handling because they aren't common to other compilers, don't have trivial hardware acceleration available, or do not have the size/ir complexity that other intrinsics have.

At best we might consider having Max/Min directly import as GT_CONDITIONAL to save on inlining and converting to those anyways, but then we really need the JIT to have better GT_CONDITIONAL support in the first place and because there isn't the size/ir complexity that other APIs have and we also want DPGO to work, I find it much less likely we do that.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

Does the FullOpts metric include T0? If it does we should probably split that out so we can actually differentiate MinOpts vs T0 vs T1 vs FullOpts, I had assumed it was just T1+FullOpts

I had downloaded the full diffs and saw most of the impactful changes in code that was actually proper fullopts or T1, i.e. the asmdiff says ; FullOpts code

One of the more robust cases is:

- ; 93 inlinees with PGO data; 314 single block inlinees; 54 inlinees without PGO data
+ ; 41 inlinees with PGO data; 85 single block inlinees; 11 inlinees without PGO data

going from 611 locals with a frame size of 632 down to 243 locals with a frame size of 104 and from 10112 bytes of codegen down to 1847 bytes of codegen

But for non tests it improves the codegen around string formatting for all integer primitives (via CountDigits and CountHexDigits), array sorting and the various Sorted* collections, some BigInteger handling particularly around ModPow, etc

This is also why there are so many triggers in tests, because Log2 is used in APIs that most apps, even trivial ones, end up using, so it triggers for a lot of code.

@EgorBoEgorBo 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.

Leaving this up to you, I am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it. I understand that the debugging experience is worse in optimized code but I believe we've never changed the content of the exception before based on opts.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it

CC. @jkotas for input on this bit. The consideration is essentially whether it is okay for T1 (optimized) codegen for Log2 to differ in the message included as part of ArgumentOutOfRangeException from T0 (unoptimized/debug/t0) codegen?

I don't feel its worth complicating the JIT/VM to support preserving that message (ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum) in the actual JIT throw helper support and it is okay for T1 code to differ here, particularly since its the only exception for this API.

The alternatives would be:

  • Also drop the info from the exception in the managed fallback path
  • Instead of producing a gtNewMustThrowException node, keep a second Log2 intrinsic and use the existing rationalization rewriting support to force it back to a call (this loses info that it definitively throws and is cold, which can be added is just a bit more complex)

Leaving this up to you

As for the overall change @EgorBo, I do think its worth it today and plan on taking it. We know Log2 is a building block API and had already had the rest of the foundational support here, it is very rare we get a change where the diffs (-321k bytes) and TP (-0.30% instructions retired) improvement are so definitively good (including both tests and some key production code methods, such as the primitive formatting/parsing helpers).

I also do understand the desire to handle these scenarios more generally via the inliner, but that's not going to happen today or even this release. I am more than happy to see us drop such JIT support in the future when the inliner is capable of handling it, the same as we've done with other intrinsic functionality (such as when we finally dropped the direct support for S.N.Vector2/3/4, Quaternion, Plane, etc).

@jkotas

Copy link
Copy Markdown
Member

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

I'll see the diffs around the second alternative I gave then, carrying the throw path through as the rewritable intrinsic so it forces it back to the managed path in that case and preserves the exception.

CopilotAI review requested due to automatic review settings June 1, 2026 23:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings June 2, 2026 02:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@tannergooding
tannergooding merged commit 7dc7499 into dotnet:mainJun 2, 2026
163 of 167 checks passed
@tannergooding
tannergooding deleted the better-log2 branch June 2, 2026 12:25
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 3, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@tannergooding@EgorBo@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Ensure Log2 can actually be imported as intrinsic - #128678

Merged
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2
Jun 2, 2026
Merged

Ensure Log2 can actually be imported as intrinsic#128678
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2

Conversation

@tannergooding

Copy link
Copy Markdown
Member

Previously this was code was never being hit because we were checking against the non-precise var_type and so we always saw TYP_INT even for unsigned inputs. Now we correctly handle checking for unsigned inputs and allow importing signed ones as well.

CopilotAI review requested due to automatic review settings May 28, 2026 03:48
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 28, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the CoreCLR JIT importer for the NI_PRIMITIVE_Log2 primitive intrinsic to correctly distinguish signed vs. unsigned inputs using the precise type, enabling the intrinsic expansion for unsigned arguments and adding a conditional throw path for signed arguments.

Changes:

  • Fixes signed/unsigned detection for Log2 by using JitType2PreciseVarType(baseJitType) rather than the non-precise baseType.
  • Implements the Log2 expansion via LeadingZeroCount(value | 1) to satisfy the 0 -> 0 contract.
  • Adds a conditional throw for negative signed inputs using a qmark-based fallback path.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergoodingforce-pushed the better-log2 branch 2 times, most recently from f76559c to eaeb042CompareMay 28, 2026 06:38
CopilotAI review requested due to automatic review settings May 28, 2026 06:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
CopilotAI review requested due to automatic review settings May 28, 2026 16:43

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergooding marked this pull request as ready for review May 29, 2026 02:37
CopilotAI review requested due to automatic review settings May 29, 2026 02:37
@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK) and move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

The diffs here are significant. We see -321k bytes of codegen on Linux Arm64

Literally 99% of diffs are in libraries_tests.run. and all example diffs imply that

[Intrinsic]publicstaticuintLog2(uintvalue)=>(uint)BitOperations.Log2(value);

is not inlined. I suspect it might be either PGO-driven or it needs an AggressiveInlining.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:
{A3A3FBCC-75AB-42F7-A074-1D18928B1FED}

or just slap AggressiveInlining everywhere

cc @dotnet/jit-contrib for opinions

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

Copy link
Copy Markdown
MemberAuthor

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK)

I am doubtful that QMARK will ever fully go away as they have a significant benefit in that they allow introducing block like functionality (namely a compare + branch to throw an exception) into the code. If anything, it will just be replaced with a helper that does the creation of the if/else blocks at the point you're currently creating the QMARK node instead. They exist because it simplifies a bit of handling in the early phases and even allows some constant folding without forcing the block to actually be produced.

move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

Most compilers do this for the fundamental math APIs like this, we're not special and even have stricter timing requirements which means there is more reason for us to handle such APIs intrinsically as it significantly reduces the amount of work the JIT has to do. The inliner is always going to have limitations and there's no reason to contribute to its pessimizations for these foundational APIs everything else is built on.

Literally 99% of diffs are in libraries_tests.run.

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others. That's why I explicitly called out the hits we also have in other areas. But that's also representative of this code being "hot" and common to code that most apps and binaries end up using, even if indirectly.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:

I don't think this is a fix and it just pushes things down the road more.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000:ldarg.0IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)IL_0006: ret

But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't. We skip all those checks, all the eating of the budget, and all the expensive handling by treating these key APIs as intrinsic. It completely bypasses the other expensive transforms and checks which ultimately simplifies what the JIT has to do, by adding a few lines to the importer to handle the known special scenarios.

@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am doubtful that QMARK will ever fully go away as they have a significant benefit

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

The inliner is always going to have limitations

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users? I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about. Are you going to continue pushing various small methods as JIT intrinsics?

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

I do think we need to stop using it as an excuse. I agree on importing things as special opcodes so then JIT can emit those opcodes as part of other transformations and rely on proper expansion - that made sense to me, but it's no the case here as we just mimic inliner's work, not more than that.
Another downside of all of these intrinsics is that we continue adding binary size to jit while the original C# code can be trimmed.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000: ldarg.0
IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)
IL_0006: ret
But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't

I think we need to study why we give up, it might be something fundamental like abstract generic resolution in importer.
Or you just copied a diff from Tier0 that is not inlined. What is the Tier1 method we should look at?

Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

tannergooding commented May 29, 2026

Copy link
Copy Markdown
MemberAuthor

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

My point here was rather that we have a fundamental need to introduce IR that represents x = cond ? throw : y and right now that is handled via QMARK. Even if we remove QMARK, it will just have to be replaced by something else that puts in the relevant blocks directly instead.

So even if they aren't the best IR today, the code is logically correct and will maintain roughly the current shape even when QMARKs disappear.

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users?

Because intrinsifying everything is both impossible and a negative. Most methods are not foundational in that sense and it would effectively cause "too much inlining", regress throughput, regress codegen size, etc.

Log2 is such a foundational helper, it and many of the APIs on the primitive types are the building blocks for all the other algorithms. It is an API that most compilers explicitly recognize and handle as intrinsic in some fashion, accordingly.

I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about.

I agree, but we also know this is much more complex work and that it will always have limits. So for foundational cases that are common to other compilers and where we can avoid some of the redundant work, it makes sense.

Are you going to continue pushing various small methods as JIT intrinsics?

The short answer is not really, fixing the existing Log2 handling really rounds out the set of them.

I'm not and have never been looking to intrinsify the world, only the key foundational math APIs like this one, i.e. the building blocks for the rest of .NET. Most of these were already handled and Log2 was one where we notably had handling but it was never firing because we were only checking TYP_INT and not TYP_UINT.

Of the foundational math APIs that aren't intrinsified, we only have integral Abs, Log10, Max, and Min. However, these likely aren't ever going to get handling because they aren't common to other compilers, don't have trivial hardware acceleration available, or do not have the size/ir complexity that other intrinsics have.

At best we might consider having Max/Min directly import as GT_CONDITIONAL to save on inlining and converting to those anyways, but then we really need the JIT to have better GT_CONDITIONAL support in the first place and because there isn't the size/ir complexity that other APIs have and we also want DPGO to work, I find it much less likely we do that.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

Does the FullOpts metric include T0? If it does we should probably split that out so we can actually differentiate MinOpts vs T0 vs T1 vs FullOpts, I had assumed it was just T1+FullOpts

I had downloaded the full diffs and saw most of the impactful changes in code that was actually proper fullopts or T1, i.e. the asmdiff says ; FullOpts code

One of the more robust cases is:

- ; 93 inlinees with PGO data; 314 single block inlinees; 54 inlinees without PGO data
+ ; 41 inlinees with PGO data; 85 single block inlinees; 11 inlinees without PGO data

going from 611 locals with a frame size of 632 down to 243 locals with a frame size of 104 and from 10112 bytes of codegen down to 1847 bytes of codegen

But for non tests it improves the codegen around string formatting for all integer primitives (via CountDigits and CountHexDigits), array sorting and the various Sorted* collections, some BigInteger handling particularly around ModPow, etc

This is also why there are so many triggers in tests, because Log2 is used in APIs that most apps, even trivial ones, end up using, so it triggers for a lot of code.

@EgorBoEgorBo 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.

Leaving this up to you, I am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it. I understand that the debugging experience is worse in optimized code but I believe we've never changed the content of the exception before based on opts.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it

CC. @jkotas for input on this bit. The consideration is essentially whether it is okay for T1 (optimized) codegen for Log2 to differ in the message included as part of ArgumentOutOfRangeException from T0 (unoptimized/debug/t0) codegen?

I don't feel its worth complicating the JIT/VM to support preserving that message (ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum) in the actual JIT throw helper support and it is okay for T1 code to differ here, particularly since its the only exception for this API.

The alternatives would be:

  • Also drop the info from the exception in the managed fallback path
  • Instead of producing a gtNewMustThrowException node, keep a second Log2 intrinsic and use the existing rationalization rewriting support to force it back to a call (this loses info that it definitively throws and is cold, which can be added is just a bit more complex)

Leaving this up to you

As for the overall change @EgorBo, I do think its worth it today and plan on taking it. We know Log2 is a building block API and had already had the rest of the foundational support here, it is very rare we get a change where the diffs (-321k bytes) and TP (-0.30% instructions retired) improvement are so definitively good (including both tests and some key production code methods, such as the primitive formatting/parsing helpers).

I also do understand the desire to handle these scenarios more generally via the inliner, but that's not going to happen today or even this release. I am more than happy to see us drop such JIT support in the future when the inliner is capable of handling it, the same as we've done with other intrinsic functionality (such as when we finally dropped the direct support for S.N.Vector2/3/4, Quaternion, Plane, etc).

@jkotas

Copy link
Copy Markdown
Member

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

I'll see the diffs around the second alternative I gave then, carrying the throw path through as the rewritable intrinsic so it forces it back to the managed path in that case and preserves the exception.

CopilotAI review requested due to automatic review settings June 1, 2026 23:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings June 2, 2026 02:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@tannergooding
tannergooding merged commit 7dc7499 into dotnet:mainJun 2, 2026
163 of 167 checks passed
@tannergooding
tannergooding deleted the better-log2 branch June 2, 2026 12:25
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 3, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@tannergooding@EgorBo@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Ensure Log2 can actually be imported as intrinsic - #128678

Merged
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2
Jun 2, 2026
Merged

Ensure Log2 can actually be imported as intrinsic#128678
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2

Conversation

@tannergooding

Copy link
Copy Markdown
Member

Previously this was code was never being hit because we were checking against the non-precise var_type and so we always saw TYP_INT even for unsigned inputs. Now we correctly handle checking for unsigned inputs and allow importing signed ones as well.

CopilotAI review requested due to automatic review settings May 28, 2026 03:48
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 28, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the CoreCLR JIT importer for the NI_PRIMITIVE_Log2 primitive intrinsic to correctly distinguish signed vs. unsigned inputs using the precise type, enabling the intrinsic expansion for unsigned arguments and adding a conditional throw path for signed arguments.

Changes:

  • Fixes signed/unsigned detection for Log2 by using JitType2PreciseVarType(baseJitType) rather than the non-precise baseType.
  • Implements the Log2 expansion via LeadingZeroCount(value | 1) to satisfy the 0 -> 0 contract.
  • Adds a conditional throw for negative signed inputs using a qmark-based fallback path.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergoodingforce-pushed the better-log2 branch 2 times, most recently from f76559c to eaeb042CompareMay 28, 2026 06:38
CopilotAI review requested due to automatic review settings May 28, 2026 06:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
CopilotAI review requested due to automatic review settings May 28, 2026 16:43

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergooding marked this pull request as ready for review May 29, 2026 02:37
CopilotAI review requested due to automatic review settings May 29, 2026 02:37
@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK) and move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

The diffs here are significant. We see -321k bytes of codegen on Linux Arm64

Literally 99% of diffs are in libraries_tests.run. and all example diffs imply that

[Intrinsic]publicstaticuintLog2(uintvalue)=>(uint)BitOperations.Log2(value);

is not inlined. I suspect it might be either PGO-driven or it needs an AggressiveInlining.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:
{A3A3FBCC-75AB-42F7-A074-1D18928B1FED}

or just slap AggressiveInlining everywhere

cc @dotnet/jit-contrib for opinions

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

Copy link
Copy Markdown
MemberAuthor

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK)

I am doubtful that QMARK will ever fully go away as they have a significant benefit in that they allow introducing block like functionality (namely a compare + branch to throw an exception) into the code. If anything, it will just be replaced with a helper that does the creation of the if/else blocks at the point you're currently creating the QMARK node instead. They exist because it simplifies a bit of handling in the early phases and even allows some constant folding without forcing the block to actually be produced.

move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

Most compilers do this for the fundamental math APIs like this, we're not special and even have stricter timing requirements which means there is more reason for us to handle such APIs intrinsically as it significantly reduces the amount of work the JIT has to do. The inliner is always going to have limitations and there's no reason to contribute to its pessimizations for these foundational APIs everything else is built on.

Literally 99% of diffs are in libraries_tests.run.

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others. That's why I explicitly called out the hits we also have in other areas. But that's also representative of this code being "hot" and common to code that most apps and binaries end up using, even if indirectly.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:

I don't think this is a fix and it just pushes things down the road more.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000:ldarg.0IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)IL_0006: ret

But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't. We skip all those checks, all the eating of the budget, and all the expensive handling by treating these key APIs as intrinsic. It completely bypasses the other expensive transforms and checks which ultimately simplifies what the JIT has to do, by adding a few lines to the importer to handle the known special scenarios.

@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am doubtful that QMARK will ever fully go away as they have a significant benefit

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

The inliner is always going to have limitations

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users? I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about. Are you going to continue pushing various small methods as JIT intrinsics?

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

I do think we need to stop using it as an excuse. I agree on importing things as special opcodes so then JIT can emit those opcodes as part of other transformations and rely on proper expansion - that made sense to me, but it's no the case here as we just mimic inliner's work, not more than that.
Another downside of all of these intrinsics is that we continue adding binary size to jit while the original C# code can be trimmed.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000: ldarg.0
IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)
IL_0006: ret
But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't

I think we need to study why we give up, it might be something fundamental like abstract generic resolution in importer.
Or you just copied a diff from Tier0 that is not inlined. What is the Tier1 method we should look at?

Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

tannergooding commented May 29, 2026

Copy link
Copy Markdown
MemberAuthor

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

My point here was rather that we have a fundamental need to introduce IR that represents x = cond ? throw : y and right now that is handled via QMARK. Even if we remove QMARK, it will just have to be replaced by something else that puts in the relevant blocks directly instead.

So even if they aren't the best IR today, the code is logically correct and will maintain roughly the current shape even when QMARKs disappear.

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users?

Because intrinsifying everything is both impossible and a negative. Most methods are not foundational in that sense and it would effectively cause "too much inlining", regress throughput, regress codegen size, etc.

Log2 is such a foundational helper, it and many of the APIs on the primitive types are the building blocks for all the other algorithms. It is an API that most compilers explicitly recognize and handle as intrinsic in some fashion, accordingly.

I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about.

I agree, but we also know this is much more complex work and that it will always have limits. So for foundational cases that are common to other compilers and where we can avoid some of the redundant work, it makes sense.

Are you going to continue pushing various small methods as JIT intrinsics?

The short answer is not really, fixing the existing Log2 handling really rounds out the set of them.

I'm not and have never been looking to intrinsify the world, only the key foundational math APIs like this one, i.e. the building blocks for the rest of .NET. Most of these were already handled and Log2 was one where we notably had handling but it was never firing because we were only checking TYP_INT and not TYP_UINT.

Of the foundational math APIs that aren't intrinsified, we only have integral Abs, Log10, Max, and Min. However, these likely aren't ever going to get handling because they aren't common to other compilers, don't have trivial hardware acceleration available, or do not have the size/ir complexity that other intrinsics have.

At best we might consider having Max/Min directly import as GT_CONDITIONAL to save on inlining and converting to those anyways, but then we really need the JIT to have better GT_CONDITIONAL support in the first place and because there isn't the size/ir complexity that other APIs have and we also want DPGO to work, I find it much less likely we do that.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

Does the FullOpts metric include T0? If it does we should probably split that out so we can actually differentiate MinOpts vs T0 vs T1 vs FullOpts, I had assumed it was just T1+FullOpts

I had downloaded the full diffs and saw most of the impactful changes in code that was actually proper fullopts or T1, i.e. the asmdiff says ; FullOpts code

One of the more robust cases is:

- ; 93 inlinees with PGO data; 314 single block inlinees; 54 inlinees without PGO data
+ ; 41 inlinees with PGO data; 85 single block inlinees; 11 inlinees without PGO data

going from 611 locals with a frame size of 632 down to 243 locals with a frame size of 104 and from 10112 bytes of codegen down to 1847 bytes of codegen

But for non tests it improves the codegen around string formatting for all integer primitives (via CountDigits and CountHexDigits), array sorting and the various Sorted* collections, some BigInteger handling particularly around ModPow, etc

This is also why there are so many triggers in tests, because Log2 is used in APIs that most apps, even trivial ones, end up using, so it triggers for a lot of code.

@EgorBoEgorBo 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.

Leaving this up to you, I am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it. I understand that the debugging experience is worse in optimized code but I believe we've never changed the content of the exception before based on opts.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it

CC. @jkotas for input on this bit. The consideration is essentially whether it is okay for T1 (optimized) codegen for Log2 to differ in the message included as part of ArgumentOutOfRangeException from T0 (unoptimized/debug/t0) codegen?

I don't feel its worth complicating the JIT/VM to support preserving that message (ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum) in the actual JIT throw helper support and it is okay for T1 code to differ here, particularly since its the only exception for this API.

The alternatives would be:

  • Also drop the info from the exception in the managed fallback path
  • Instead of producing a gtNewMustThrowException node, keep a second Log2 intrinsic and use the existing rationalization rewriting support to force it back to a call (this loses info that it definitively throws and is cold, which can be added is just a bit more complex)

Leaving this up to you

As for the overall change @EgorBo, I do think its worth it today and plan on taking it. We know Log2 is a building block API and had already had the rest of the foundational support here, it is very rare we get a change where the diffs (-321k bytes) and TP (-0.30% instructions retired) improvement are so definitively good (including both tests and some key production code methods, such as the primitive formatting/parsing helpers).

I also do understand the desire to handle these scenarios more generally via the inliner, but that's not going to happen today or even this release. I am more than happy to see us drop such JIT support in the future when the inliner is capable of handling it, the same as we've done with other intrinsic functionality (such as when we finally dropped the direct support for S.N.Vector2/3/4, Quaternion, Plane, etc).

@jkotas

Copy link
Copy Markdown
Member

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

I'll see the diffs around the second alternative I gave then, carrying the throw path through as the rewritable intrinsic so it forces it back to the managed path in that case and preserves the exception.

CopilotAI review requested due to automatic review settings June 1, 2026 23:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings June 2, 2026 02:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@tannergooding
tannergooding merged commit 7dc7499 into dotnet:mainJun 2, 2026
163 of 167 checks passed
@tannergooding
tannergooding deleted the better-log2 branch June 2, 2026 12:25
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 3, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@tannergooding@EgorBo@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Ensure Log2 can actually be imported as intrinsic - #128678

Merged
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2
Jun 2, 2026
Merged

Ensure Log2 can actually be imported as intrinsic#128678
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2

Conversation

@tannergooding

Copy link
Copy Markdown
Member

Previously this was code was never being hit because we were checking against the non-precise var_type and so we always saw TYP_INT even for unsigned inputs. Now we correctly handle checking for unsigned inputs and allow importing signed ones as well.

CopilotAI review requested due to automatic review settings May 28, 2026 03:48
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 28, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the CoreCLR JIT importer for the NI_PRIMITIVE_Log2 primitive intrinsic to correctly distinguish signed vs. unsigned inputs using the precise type, enabling the intrinsic expansion for unsigned arguments and adding a conditional throw path for signed arguments.

Changes:

  • Fixes signed/unsigned detection for Log2 by using JitType2PreciseVarType(baseJitType) rather than the non-precise baseType.
  • Implements the Log2 expansion via LeadingZeroCount(value | 1) to satisfy the 0 -> 0 contract.
  • Adds a conditional throw for negative signed inputs using a qmark-based fallback path.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergoodingforce-pushed the better-log2 branch 2 times, most recently from f76559c to eaeb042CompareMay 28, 2026 06:38
CopilotAI review requested due to automatic review settings May 28, 2026 06:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
CopilotAI review requested due to automatic review settings May 28, 2026 16:43

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergooding marked this pull request as ready for review May 29, 2026 02:37
CopilotAI review requested due to automatic review settings May 29, 2026 02:37
@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK) and move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

The diffs here are significant. We see -321k bytes of codegen on Linux Arm64

Literally 99% of diffs are in libraries_tests.run. and all example diffs imply that

[Intrinsic]publicstaticuintLog2(uintvalue)=>(uint)BitOperations.Log2(value);

is not inlined. I suspect it might be either PGO-driven or it needs an AggressiveInlining.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:
{A3A3FBCC-75AB-42F7-A074-1D18928B1FED}

or just slap AggressiveInlining everywhere

cc @dotnet/jit-contrib for opinions

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

Copy link
Copy Markdown
MemberAuthor

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK)

I am doubtful that QMARK will ever fully go away as they have a significant benefit in that they allow introducing block like functionality (namely a compare + branch to throw an exception) into the code. If anything, it will just be replaced with a helper that does the creation of the if/else blocks at the point you're currently creating the QMARK node instead. They exist because it simplifies a bit of handling in the early phases and even allows some constant folding without forcing the block to actually be produced.

move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

Most compilers do this for the fundamental math APIs like this, we're not special and even have stricter timing requirements which means there is more reason for us to handle such APIs intrinsically as it significantly reduces the amount of work the JIT has to do. The inliner is always going to have limitations and there's no reason to contribute to its pessimizations for these foundational APIs everything else is built on.

Literally 99% of diffs are in libraries_tests.run.

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others. That's why I explicitly called out the hits we also have in other areas. But that's also representative of this code being "hot" and common to code that most apps and binaries end up using, even if indirectly.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:

I don't think this is a fix and it just pushes things down the road more.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000:ldarg.0IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)IL_0006: ret

But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't. We skip all those checks, all the eating of the budget, and all the expensive handling by treating these key APIs as intrinsic. It completely bypasses the other expensive transforms and checks which ultimately simplifies what the JIT has to do, by adding a few lines to the importer to handle the known special scenarios.

@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am doubtful that QMARK will ever fully go away as they have a significant benefit

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

The inliner is always going to have limitations

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users? I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about. Are you going to continue pushing various small methods as JIT intrinsics?

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

I do think we need to stop using it as an excuse. I agree on importing things as special opcodes so then JIT can emit those opcodes as part of other transformations and rely on proper expansion - that made sense to me, but it's no the case here as we just mimic inliner's work, not more than that.
Another downside of all of these intrinsics is that we continue adding binary size to jit while the original C# code can be trimmed.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000: ldarg.0
IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)
IL_0006: ret
But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't

I think we need to study why we give up, it might be something fundamental like abstract generic resolution in importer.
Or you just copied a diff from Tier0 that is not inlined. What is the Tier1 method we should look at?

Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

tannergooding commented May 29, 2026

Copy link
Copy Markdown
MemberAuthor

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

My point here was rather that we have a fundamental need to introduce IR that represents x = cond ? throw : y and right now that is handled via QMARK. Even if we remove QMARK, it will just have to be replaced by something else that puts in the relevant blocks directly instead.

So even if they aren't the best IR today, the code is logically correct and will maintain roughly the current shape even when QMARKs disappear.

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users?

Because intrinsifying everything is both impossible and a negative. Most methods are not foundational in that sense and it would effectively cause "too much inlining", regress throughput, regress codegen size, etc.

Log2 is such a foundational helper, it and many of the APIs on the primitive types are the building blocks for all the other algorithms. It is an API that most compilers explicitly recognize and handle as intrinsic in some fashion, accordingly.

I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about.

I agree, but we also know this is much more complex work and that it will always have limits. So for foundational cases that are common to other compilers and where we can avoid some of the redundant work, it makes sense.

Are you going to continue pushing various small methods as JIT intrinsics?

The short answer is not really, fixing the existing Log2 handling really rounds out the set of them.

I'm not and have never been looking to intrinsify the world, only the key foundational math APIs like this one, i.e. the building blocks for the rest of .NET. Most of these were already handled and Log2 was one where we notably had handling but it was never firing because we were only checking TYP_INT and not TYP_UINT.

Of the foundational math APIs that aren't intrinsified, we only have integral Abs, Log10, Max, and Min. However, these likely aren't ever going to get handling because they aren't common to other compilers, don't have trivial hardware acceleration available, or do not have the size/ir complexity that other intrinsics have.

At best we might consider having Max/Min directly import as GT_CONDITIONAL to save on inlining and converting to those anyways, but then we really need the JIT to have better GT_CONDITIONAL support in the first place and because there isn't the size/ir complexity that other APIs have and we also want DPGO to work, I find it much less likely we do that.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

Does the FullOpts metric include T0? If it does we should probably split that out so we can actually differentiate MinOpts vs T0 vs T1 vs FullOpts, I had assumed it was just T1+FullOpts

I had downloaded the full diffs and saw most of the impactful changes in code that was actually proper fullopts or T1, i.e. the asmdiff says ; FullOpts code

One of the more robust cases is:

- ; 93 inlinees with PGO data; 314 single block inlinees; 54 inlinees without PGO data
+ ; 41 inlinees with PGO data; 85 single block inlinees; 11 inlinees without PGO data

going from 611 locals with a frame size of 632 down to 243 locals with a frame size of 104 and from 10112 bytes of codegen down to 1847 bytes of codegen

But for non tests it improves the codegen around string formatting for all integer primitives (via CountDigits and CountHexDigits), array sorting and the various Sorted* collections, some BigInteger handling particularly around ModPow, etc

This is also why there are so many triggers in tests, because Log2 is used in APIs that most apps, even trivial ones, end up using, so it triggers for a lot of code.

@EgorBoEgorBo 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.

Leaving this up to you, I am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it. I understand that the debugging experience is worse in optimized code but I believe we've never changed the content of the exception before based on opts.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it

CC. @jkotas for input on this bit. The consideration is essentially whether it is okay for T1 (optimized) codegen for Log2 to differ in the message included as part of ArgumentOutOfRangeException from T0 (unoptimized/debug/t0) codegen?

I don't feel its worth complicating the JIT/VM to support preserving that message (ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum) in the actual JIT throw helper support and it is okay for T1 code to differ here, particularly since its the only exception for this API.

The alternatives would be:

  • Also drop the info from the exception in the managed fallback path
  • Instead of producing a gtNewMustThrowException node, keep a second Log2 intrinsic and use the existing rationalization rewriting support to force it back to a call (this loses info that it definitively throws and is cold, which can be added is just a bit more complex)

Leaving this up to you

As for the overall change @EgorBo, I do think its worth it today and plan on taking it. We know Log2 is a building block API and had already had the rest of the foundational support here, it is very rare we get a change where the diffs (-321k bytes) and TP (-0.30% instructions retired) improvement are so definitively good (including both tests and some key production code methods, such as the primitive formatting/parsing helpers).

I also do understand the desire to handle these scenarios more generally via the inliner, but that's not going to happen today or even this release. I am more than happy to see us drop such JIT support in the future when the inliner is capable of handling it, the same as we've done with other intrinsic functionality (such as when we finally dropped the direct support for S.N.Vector2/3/4, Quaternion, Plane, etc).

@jkotas

Copy link
Copy Markdown
Member

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

I'll see the diffs around the second alternative I gave then, carrying the throw path through as the rewritable intrinsic so it forces it back to the managed path in that case and preserves the exception.

CopilotAI review requested due to automatic review settings June 1, 2026 23:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings June 2, 2026 02:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@tannergooding
tannergooding merged commit 7dc7499 into dotnet:mainJun 2, 2026
163 of 167 checks passed
@tannergooding
tannergooding deleted the better-log2 branch June 2, 2026 12:25
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 3, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@tannergooding@EgorBo@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Ensure Log2 can actually be imported as intrinsic - #128678

Merged
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2
Jun 2, 2026
Merged

Ensure Log2 can actually be imported as intrinsic#128678
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2

Conversation

@tannergooding

Copy link
Copy Markdown
Member

Previously this was code was never being hit because we were checking against the non-precise var_type and so we always saw TYP_INT even for unsigned inputs. Now we correctly handle checking for unsigned inputs and allow importing signed ones as well.

CopilotAI review requested due to automatic review settings May 28, 2026 03:48
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 28, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the CoreCLR JIT importer for the NI_PRIMITIVE_Log2 primitive intrinsic to correctly distinguish signed vs. unsigned inputs using the precise type, enabling the intrinsic expansion for unsigned arguments and adding a conditional throw path for signed arguments.

Changes:

  • Fixes signed/unsigned detection for Log2 by using JitType2PreciseVarType(baseJitType) rather than the non-precise baseType.
  • Implements the Log2 expansion via LeadingZeroCount(value | 1) to satisfy the 0 -> 0 contract.
  • Adds a conditional throw for negative signed inputs using a qmark-based fallback path.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergoodingforce-pushed the better-log2 branch 2 times, most recently from f76559c to eaeb042CompareMay 28, 2026 06:38
CopilotAI review requested due to automatic review settings May 28, 2026 06:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
CopilotAI review requested due to automatic review settings May 28, 2026 16:43

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergooding marked this pull request as ready for review May 29, 2026 02:37
CopilotAI review requested due to automatic review settings May 29, 2026 02:37
@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK) and move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

The diffs here are significant. We see -321k bytes of codegen on Linux Arm64

Literally 99% of diffs are in libraries_tests.run. and all example diffs imply that

[Intrinsic]publicstaticuintLog2(uintvalue)=>(uint)BitOperations.Log2(value);

is not inlined. I suspect it might be either PGO-driven or it needs an AggressiveInlining.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:
{A3A3FBCC-75AB-42F7-A074-1D18928B1FED}

or just slap AggressiveInlining everywhere

cc @dotnet/jit-contrib for opinions

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

Copy link
Copy Markdown
MemberAuthor

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK)

I am doubtful that QMARK will ever fully go away as they have a significant benefit in that they allow introducing block like functionality (namely a compare + branch to throw an exception) into the code. If anything, it will just be replaced with a helper that does the creation of the if/else blocks at the point you're currently creating the QMARK node instead. They exist because it simplifies a bit of handling in the early phases and even allows some constant folding without forcing the block to actually be produced.

move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

Most compilers do this for the fundamental math APIs like this, we're not special and even have stricter timing requirements which means there is more reason for us to handle such APIs intrinsically as it significantly reduces the amount of work the JIT has to do. The inliner is always going to have limitations and there's no reason to contribute to its pessimizations for these foundational APIs everything else is built on.

Literally 99% of diffs are in libraries_tests.run.

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others. That's why I explicitly called out the hits we also have in other areas. But that's also representative of this code being "hot" and common to code that most apps and binaries end up using, even if indirectly.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:

I don't think this is a fix and it just pushes things down the road more.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000:ldarg.0IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)IL_0006: ret

But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't. We skip all those checks, all the eating of the budget, and all the expensive handling by treating these key APIs as intrinsic. It completely bypasses the other expensive transforms and checks which ultimately simplifies what the JIT has to do, by adding a few lines to the importer to handle the known special scenarios.

@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am doubtful that QMARK will ever fully go away as they have a significant benefit

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

The inliner is always going to have limitations

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users? I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about. Are you going to continue pushing various small methods as JIT intrinsics?

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

I do think we need to stop using it as an excuse. I agree on importing things as special opcodes so then JIT can emit those opcodes as part of other transformations and rely on proper expansion - that made sense to me, but it's no the case here as we just mimic inliner's work, not more than that.
Another downside of all of these intrinsics is that we continue adding binary size to jit while the original C# code can be trimmed.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000: ldarg.0
IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)
IL_0006: ret
But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't

I think we need to study why we give up, it might be something fundamental like abstract generic resolution in importer.
Or you just copied a diff from Tier0 that is not inlined. What is the Tier1 method we should look at?

Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

tannergooding commented May 29, 2026

Copy link
Copy Markdown
MemberAuthor

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

My point here was rather that we have a fundamental need to introduce IR that represents x = cond ? throw : y and right now that is handled via QMARK. Even if we remove QMARK, it will just have to be replaced by something else that puts in the relevant blocks directly instead.

So even if they aren't the best IR today, the code is logically correct and will maintain roughly the current shape even when QMARKs disappear.

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users?

Because intrinsifying everything is both impossible and a negative. Most methods are not foundational in that sense and it would effectively cause "too much inlining", regress throughput, regress codegen size, etc.

Log2 is such a foundational helper, it and many of the APIs on the primitive types are the building blocks for all the other algorithms. It is an API that most compilers explicitly recognize and handle as intrinsic in some fashion, accordingly.

I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about.

I agree, but we also know this is much more complex work and that it will always have limits. So for foundational cases that are common to other compilers and where we can avoid some of the redundant work, it makes sense.

Are you going to continue pushing various small methods as JIT intrinsics?

The short answer is not really, fixing the existing Log2 handling really rounds out the set of them.

I'm not and have never been looking to intrinsify the world, only the key foundational math APIs like this one, i.e. the building blocks for the rest of .NET. Most of these were already handled and Log2 was one where we notably had handling but it was never firing because we were only checking TYP_INT and not TYP_UINT.

Of the foundational math APIs that aren't intrinsified, we only have integral Abs, Log10, Max, and Min. However, these likely aren't ever going to get handling because they aren't common to other compilers, don't have trivial hardware acceleration available, or do not have the size/ir complexity that other intrinsics have.

At best we might consider having Max/Min directly import as GT_CONDITIONAL to save on inlining and converting to those anyways, but then we really need the JIT to have better GT_CONDITIONAL support in the first place and because there isn't the size/ir complexity that other APIs have and we also want DPGO to work, I find it much less likely we do that.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

Does the FullOpts metric include T0? If it does we should probably split that out so we can actually differentiate MinOpts vs T0 vs T1 vs FullOpts, I had assumed it was just T1+FullOpts

I had downloaded the full diffs and saw most of the impactful changes in code that was actually proper fullopts or T1, i.e. the asmdiff says ; FullOpts code

One of the more robust cases is:

- ; 93 inlinees with PGO data; 314 single block inlinees; 54 inlinees without PGO data
+ ; 41 inlinees with PGO data; 85 single block inlinees; 11 inlinees without PGO data

going from 611 locals with a frame size of 632 down to 243 locals with a frame size of 104 and from 10112 bytes of codegen down to 1847 bytes of codegen

But for non tests it improves the codegen around string formatting for all integer primitives (via CountDigits and CountHexDigits), array sorting and the various Sorted* collections, some BigInteger handling particularly around ModPow, etc

This is also why there are so many triggers in tests, because Log2 is used in APIs that most apps, even trivial ones, end up using, so it triggers for a lot of code.

@EgorBoEgorBo 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.

Leaving this up to you, I am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it. I understand that the debugging experience is worse in optimized code but I believe we've never changed the content of the exception before based on opts.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it

CC. @jkotas for input on this bit. The consideration is essentially whether it is okay for T1 (optimized) codegen for Log2 to differ in the message included as part of ArgumentOutOfRangeException from T0 (unoptimized/debug/t0) codegen?

I don't feel its worth complicating the JIT/VM to support preserving that message (ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum) in the actual JIT throw helper support and it is okay for T1 code to differ here, particularly since its the only exception for this API.

The alternatives would be:

  • Also drop the info from the exception in the managed fallback path
  • Instead of producing a gtNewMustThrowException node, keep a second Log2 intrinsic and use the existing rationalization rewriting support to force it back to a call (this loses info that it definitively throws and is cold, which can be added is just a bit more complex)

Leaving this up to you

As for the overall change @EgorBo, I do think its worth it today and plan on taking it. We know Log2 is a building block API and had already had the rest of the foundational support here, it is very rare we get a change where the diffs (-321k bytes) and TP (-0.30% instructions retired) improvement are so definitively good (including both tests and some key production code methods, such as the primitive formatting/parsing helpers).

I also do understand the desire to handle these scenarios more generally via the inliner, but that's not going to happen today or even this release. I am more than happy to see us drop such JIT support in the future when the inliner is capable of handling it, the same as we've done with other intrinsic functionality (such as when we finally dropped the direct support for S.N.Vector2/3/4, Quaternion, Plane, etc).

@jkotas

Copy link
Copy Markdown
Member

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

I'll see the diffs around the second alternative I gave then, carrying the throw path through as the rewritable intrinsic so it forces it back to the managed path in that case and preserves the exception.

CopilotAI review requested due to automatic review settings June 1, 2026 23:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings June 2, 2026 02:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@tannergooding
tannergooding merged commit 7dc7499 into dotnet:mainJun 2, 2026
163 of 167 checks passed
@tannergooding
tannergooding deleted the better-log2 branch June 2, 2026 12:25
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 3, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@tannergooding@EgorBo@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Ensure Log2 can actually be imported as intrinsic - #128678

Merged
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2
Jun 2, 2026
Merged

Ensure Log2 can actually be imported as intrinsic#128678
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2

Conversation

@tannergooding

Copy link
Copy Markdown
Member

Previously this was code was never being hit because we were checking against the non-precise var_type and so we always saw TYP_INT even for unsigned inputs. Now we correctly handle checking for unsigned inputs and allow importing signed ones as well.

CopilotAI review requested due to automatic review settings May 28, 2026 03:48
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 28, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the CoreCLR JIT importer for the NI_PRIMITIVE_Log2 primitive intrinsic to correctly distinguish signed vs. unsigned inputs using the precise type, enabling the intrinsic expansion for unsigned arguments and adding a conditional throw path for signed arguments.

Changes:

  • Fixes signed/unsigned detection for Log2 by using JitType2PreciseVarType(baseJitType) rather than the non-precise baseType.
  • Implements the Log2 expansion via LeadingZeroCount(value | 1) to satisfy the 0 -> 0 contract.
  • Adds a conditional throw for negative signed inputs using a qmark-based fallback path.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergoodingforce-pushed the better-log2 branch 2 times, most recently from f76559c to eaeb042CompareMay 28, 2026 06:38
CopilotAI review requested due to automatic review settings May 28, 2026 06:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
CopilotAI review requested due to automatic review settings May 28, 2026 16:43

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergooding marked this pull request as ready for review May 29, 2026 02:37
CopilotAI review requested due to automatic review settings May 29, 2026 02:37
@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK) and move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

The diffs here are significant. We see -321k bytes of codegen on Linux Arm64

Literally 99% of diffs are in libraries_tests.run. and all example diffs imply that

[Intrinsic]publicstaticuintLog2(uintvalue)=>(uint)BitOperations.Log2(value);

is not inlined. I suspect it might be either PGO-driven or it needs an AggressiveInlining.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:
{A3A3FBCC-75AB-42F7-A074-1D18928B1FED}

or just slap AggressiveInlining everywhere

cc @dotnet/jit-contrib for opinions

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

Copy link
Copy Markdown
MemberAuthor

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK)

I am doubtful that QMARK will ever fully go away as they have a significant benefit in that they allow introducing block like functionality (namely a compare + branch to throw an exception) into the code. If anything, it will just be replaced with a helper that does the creation of the if/else blocks at the point you're currently creating the QMARK node instead. They exist because it simplifies a bit of handling in the early phases and even allows some constant folding without forcing the block to actually be produced.

move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

Most compilers do this for the fundamental math APIs like this, we're not special and even have stricter timing requirements which means there is more reason for us to handle such APIs intrinsically as it significantly reduces the amount of work the JIT has to do. The inliner is always going to have limitations and there's no reason to contribute to its pessimizations for these foundational APIs everything else is built on.

Literally 99% of diffs are in libraries_tests.run.

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others. That's why I explicitly called out the hits we also have in other areas. But that's also representative of this code being "hot" and common to code that most apps and binaries end up using, even if indirectly.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:

I don't think this is a fix and it just pushes things down the road more.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000:ldarg.0IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)IL_0006: ret

But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't. We skip all those checks, all the eating of the budget, and all the expensive handling by treating these key APIs as intrinsic. It completely bypasses the other expensive transforms and checks which ultimately simplifies what the JIT has to do, by adding a few lines to the importer to handle the known special scenarios.

@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am doubtful that QMARK will ever fully go away as they have a significant benefit

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

The inliner is always going to have limitations

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users? I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about. Are you going to continue pushing various small methods as JIT intrinsics?

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

I do think we need to stop using it as an excuse. I agree on importing things as special opcodes so then JIT can emit those opcodes as part of other transformations and rely on proper expansion - that made sense to me, but it's no the case here as we just mimic inliner's work, not more than that.
Another downside of all of these intrinsics is that we continue adding binary size to jit while the original C# code can be trimmed.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000: ldarg.0
IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)
IL_0006: ret
But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't

I think we need to study why we give up, it might be something fundamental like abstract generic resolution in importer.
Or you just copied a diff from Tier0 that is not inlined. What is the Tier1 method we should look at?

Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

tannergooding commented May 29, 2026

Copy link
Copy Markdown
MemberAuthor

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

My point here was rather that we have a fundamental need to introduce IR that represents x = cond ? throw : y and right now that is handled via QMARK. Even if we remove QMARK, it will just have to be replaced by something else that puts in the relevant blocks directly instead.

So even if they aren't the best IR today, the code is logically correct and will maintain roughly the current shape even when QMARKs disappear.

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users?

Because intrinsifying everything is both impossible and a negative. Most methods are not foundational in that sense and it would effectively cause "too much inlining", regress throughput, regress codegen size, etc.

Log2 is such a foundational helper, it and many of the APIs on the primitive types are the building blocks for all the other algorithms. It is an API that most compilers explicitly recognize and handle as intrinsic in some fashion, accordingly.

I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about.

I agree, but we also know this is much more complex work and that it will always have limits. So for foundational cases that are common to other compilers and where we can avoid some of the redundant work, it makes sense.

Are you going to continue pushing various small methods as JIT intrinsics?

The short answer is not really, fixing the existing Log2 handling really rounds out the set of them.

I'm not and have never been looking to intrinsify the world, only the key foundational math APIs like this one, i.e. the building blocks for the rest of .NET. Most of these were already handled and Log2 was one where we notably had handling but it was never firing because we were only checking TYP_INT and not TYP_UINT.

Of the foundational math APIs that aren't intrinsified, we only have integral Abs, Log10, Max, and Min. However, these likely aren't ever going to get handling because they aren't common to other compilers, don't have trivial hardware acceleration available, or do not have the size/ir complexity that other intrinsics have.

At best we might consider having Max/Min directly import as GT_CONDITIONAL to save on inlining and converting to those anyways, but then we really need the JIT to have better GT_CONDITIONAL support in the first place and because there isn't the size/ir complexity that other APIs have and we also want DPGO to work, I find it much less likely we do that.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

Does the FullOpts metric include T0? If it does we should probably split that out so we can actually differentiate MinOpts vs T0 vs T1 vs FullOpts, I had assumed it was just T1+FullOpts

I had downloaded the full diffs and saw most of the impactful changes in code that was actually proper fullopts or T1, i.e. the asmdiff says ; FullOpts code

One of the more robust cases is:

- ; 93 inlinees with PGO data; 314 single block inlinees; 54 inlinees without PGO data
+ ; 41 inlinees with PGO data; 85 single block inlinees; 11 inlinees without PGO data

going from 611 locals with a frame size of 632 down to 243 locals with a frame size of 104 and from 10112 bytes of codegen down to 1847 bytes of codegen

But for non tests it improves the codegen around string formatting for all integer primitives (via CountDigits and CountHexDigits), array sorting and the various Sorted* collections, some BigInteger handling particularly around ModPow, etc

This is also why there are so many triggers in tests, because Log2 is used in APIs that most apps, even trivial ones, end up using, so it triggers for a lot of code.

@EgorBoEgorBo 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.

Leaving this up to you, I am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it. I understand that the debugging experience is worse in optimized code but I believe we've never changed the content of the exception before based on opts.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it

CC. @jkotas for input on this bit. The consideration is essentially whether it is okay for T1 (optimized) codegen for Log2 to differ in the message included as part of ArgumentOutOfRangeException from T0 (unoptimized/debug/t0) codegen?

I don't feel its worth complicating the JIT/VM to support preserving that message (ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum) in the actual JIT throw helper support and it is okay for T1 code to differ here, particularly since its the only exception for this API.

The alternatives would be:

  • Also drop the info from the exception in the managed fallback path
  • Instead of producing a gtNewMustThrowException node, keep a second Log2 intrinsic and use the existing rationalization rewriting support to force it back to a call (this loses info that it definitively throws and is cold, which can be added is just a bit more complex)

Leaving this up to you

As for the overall change @EgorBo, I do think its worth it today and plan on taking it. We know Log2 is a building block API and had already had the rest of the foundational support here, it is very rare we get a change where the diffs (-321k bytes) and TP (-0.30% instructions retired) improvement are so definitively good (including both tests and some key production code methods, such as the primitive formatting/parsing helpers).

I also do understand the desire to handle these scenarios more generally via the inliner, but that's not going to happen today or even this release. I am more than happy to see us drop such JIT support in the future when the inliner is capable of handling it, the same as we've done with other intrinsic functionality (such as when we finally dropped the direct support for S.N.Vector2/3/4, Quaternion, Plane, etc).

@jkotas

Copy link
Copy Markdown
Member

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

I'll see the diffs around the second alternative I gave then, carrying the throw path through as the rewritable intrinsic so it forces it back to the managed path in that case and preserves the exception.

CopilotAI review requested due to automatic review settings June 1, 2026 23:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings June 2, 2026 02:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@tannergooding
tannergooding merged commit 7dc7499 into dotnet:mainJun 2, 2026
163 of 167 checks passed
@tannergooding
tannergooding deleted the better-log2 branch June 2, 2026 12:25
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 3, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@tannergooding@EgorBo@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Ensure Log2 can actually be imported as intrinsic - #128678

Merged
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2
Jun 2, 2026
Merged

Ensure Log2 can actually be imported as intrinsic#128678
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2

Conversation

@tannergooding

Copy link
Copy Markdown
Member

Previously this was code was never being hit because we were checking against the non-precise var_type and so we always saw TYP_INT even for unsigned inputs. Now we correctly handle checking for unsigned inputs and allow importing signed ones as well.

CopilotAI review requested due to automatic review settings May 28, 2026 03:48
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 28, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the CoreCLR JIT importer for the NI_PRIMITIVE_Log2 primitive intrinsic to correctly distinguish signed vs. unsigned inputs using the precise type, enabling the intrinsic expansion for unsigned arguments and adding a conditional throw path for signed arguments.

Changes:

  • Fixes signed/unsigned detection for Log2 by using JitType2PreciseVarType(baseJitType) rather than the non-precise baseType.
  • Implements the Log2 expansion via LeadingZeroCount(value | 1) to satisfy the 0 -> 0 contract.
  • Adds a conditional throw for negative signed inputs using a qmark-based fallback path.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergoodingforce-pushed the better-log2 branch 2 times, most recently from f76559c to eaeb042CompareMay 28, 2026 06:38
CopilotAI review requested due to automatic review settings May 28, 2026 06:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
CopilotAI review requested due to automatic review settings May 28, 2026 16:43

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergooding marked this pull request as ready for review May 29, 2026 02:37
CopilotAI review requested due to automatic review settings May 29, 2026 02:37
@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK) and move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

The diffs here are significant. We see -321k bytes of codegen on Linux Arm64

Literally 99% of diffs are in libraries_tests.run. and all example diffs imply that

[Intrinsic]publicstaticuintLog2(uintvalue)=>(uint)BitOperations.Log2(value);

is not inlined. I suspect it might be either PGO-driven or it needs an AggressiveInlining.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:
{A3A3FBCC-75AB-42F7-A074-1D18928B1FED}

or just slap AggressiveInlining everywhere

cc @dotnet/jit-contrib for opinions

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

Copy link
Copy Markdown
MemberAuthor

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK)

I am doubtful that QMARK will ever fully go away as they have a significant benefit in that they allow introducing block like functionality (namely a compare + branch to throw an exception) into the code. If anything, it will just be replaced with a helper that does the creation of the if/else blocks at the point you're currently creating the QMARK node instead. They exist because it simplifies a bit of handling in the early phases and even allows some constant folding without forcing the block to actually be produced.

move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

Most compilers do this for the fundamental math APIs like this, we're not special and even have stricter timing requirements which means there is more reason for us to handle such APIs intrinsically as it significantly reduces the amount of work the JIT has to do. The inliner is always going to have limitations and there's no reason to contribute to its pessimizations for these foundational APIs everything else is built on.

Literally 99% of diffs are in libraries_tests.run.

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others. That's why I explicitly called out the hits we also have in other areas. But that's also representative of this code being "hot" and common to code that most apps and binaries end up using, even if indirectly.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:

I don't think this is a fix and it just pushes things down the road more.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000:ldarg.0IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)IL_0006: ret

But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't. We skip all those checks, all the eating of the budget, and all the expensive handling by treating these key APIs as intrinsic. It completely bypasses the other expensive transforms and checks which ultimately simplifies what the JIT has to do, by adding a few lines to the importer to handle the known special scenarios.

@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am doubtful that QMARK will ever fully go away as they have a significant benefit

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

The inliner is always going to have limitations

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users? I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about. Are you going to continue pushing various small methods as JIT intrinsics?

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

I do think we need to stop using it as an excuse. I agree on importing things as special opcodes so then JIT can emit those opcodes as part of other transformations and rely on proper expansion - that made sense to me, but it's no the case here as we just mimic inliner's work, not more than that.
Another downside of all of these intrinsics is that we continue adding binary size to jit while the original C# code can be trimmed.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000: ldarg.0
IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)
IL_0006: ret
But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't

I think we need to study why we give up, it might be something fundamental like abstract generic resolution in importer.
Or you just copied a diff from Tier0 that is not inlined. What is the Tier1 method we should look at?

Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

tannergooding commented May 29, 2026

Copy link
Copy Markdown
MemberAuthor

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

My point here was rather that we have a fundamental need to introduce IR that represents x = cond ? throw : y and right now that is handled via QMARK. Even if we remove QMARK, it will just have to be replaced by something else that puts in the relevant blocks directly instead.

So even if they aren't the best IR today, the code is logically correct and will maintain roughly the current shape even when QMARKs disappear.

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users?

Because intrinsifying everything is both impossible and a negative. Most methods are not foundational in that sense and it would effectively cause "too much inlining", regress throughput, regress codegen size, etc.

Log2 is such a foundational helper, it and many of the APIs on the primitive types are the building blocks for all the other algorithms. It is an API that most compilers explicitly recognize and handle as intrinsic in some fashion, accordingly.

I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about.

I agree, but we also know this is much more complex work and that it will always have limits. So for foundational cases that are common to other compilers and where we can avoid some of the redundant work, it makes sense.

Are you going to continue pushing various small methods as JIT intrinsics?

The short answer is not really, fixing the existing Log2 handling really rounds out the set of them.

I'm not and have never been looking to intrinsify the world, only the key foundational math APIs like this one, i.e. the building blocks for the rest of .NET. Most of these were already handled and Log2 was one where we notably had handling but it was never firing because we were only checking TYP_INT and not TYP_UINT.

Of the foundational math APIs that aren't intrinsified, we only have integral Abs, Log10, Max, and Min. However, these likely aren't ever going to get handling because they aren't common to other compilers, don't have trivial hardware acceleration available, or do not have the size/ir complexity that other intrinsics have.

At best we might consider having Max/Min directly import as GT_CONDITIONAL to save on inlining and converting to those anyways, but then we really need the JIT to have better GT_CONDITIONAL support in the first place and because there isn't the size/ir complexity that other APIs have and we also want DPGO to work, I find it much less likely we do that.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

Does the FullOpts metric include T0? If it does we should probably split that out so we can actually differentiate MinOpts vs T0 vs T1 vs FullOpts, I had assumed it was just T1+FullOpts

I had downloaded the full diffs and saw most of the impactful changes in code that was actually proper fullopts or T1, i.e. the asmdiff says ; FullOpts code

One of the more robust cases is:

- ; 93 inlinees with PGO data; 314 single block inlinees; 54 inlinees without PGO data
+ ; 41 inlinees with PGO data; 85 single block inlinees; 11 inlinees without PGO data

going from 611 locals with a frame size of 632 down to 243 locals with a frame size of 104 and from 10112 bytes of codegen down to 1847 bytes of codegen

But for non tests it improves the codegen around string formatting for all integer primitives (via CountDigits and CountHexDigits), array sorting and the various Sorted* collections, some BigInteger handling particularly around ModPow, etc

This is also why there are so many triggers in tests, because Log2 is used in APIs that most apps, even trivial ones, end up using, so it triggers for a lot of code.

@EgorBoEgorBo 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.

Leaving this up to you, I am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it. I understand that the debugging experience is worse in optimized code but I believe we've never changed the content of the exception before based on opts.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it

CC. @jkotas for input on this bit. The consideration is essentially whether it is okay for T1 (optimized) codegen for Log2 to differ in the message included as part of ArgumentOutOfRangeException from T0 (unoptimized/debug/t0) codegen?

I don't feel its worth complicating the JIT/VM to support preserving that message (ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum) in the actual JIT throw helper support and it is okay for T1 code to differ here, particularly since its the only exception for this API.

The alternatives would be:

  • Also drop the info from the exception in the managed fallback path
  • Instead of producing a gtNewMustThrowException node, keep a second Log2 intrinsic and use the existing rationalization rewriting support to force it back to a call (this loses info that it definitively throws and is cold, which can be added is just a bit more complex)

Leaving this up to you

As for the overall change @EgorBo, I do think its worth it today and plan on taking it. We know Log2 is a building block API and had already had the rest of the foundational support here, it is very rare we get a change where the diffs (-321k bytes) and TP (-0.30% instructions retired) improvement are so definitively good (including both tests and some key production code methods, such as the primitive formatting/parsing helpers).

I also do understand the desire to handle these scenarios more generally via the inliner, but that's not going to happen today or even this release. I am more than happy to see us drop such JIT support in the future when the inliner is capable of handling it, the same as we've done with other intrinsic functionality (such as when we finally dropped the direct support for S.N.Vector2/3/4, Quaternion, Plane, etc).

@jkotas

Copy link
Copy Markdown
Member

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

I'll see the diffs around the second alternative I gave then, carrying the throw path through as the rewritable intrinsic so it forces it back to the managed path in that case and preserves the exception.

CopilotAI review requested due to automatic review settings June 1, 2026 23:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings June 2, 2026 02:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@tannergooding
tannergooding merged commit 7dc7499 into dotnet:mainJun 2, 2026
163 of 167 checks passed
@tannergooding
tannergooding deleted the better-log2 branch June 2, 2026 12:25
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 3, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@tannergooding@EgorBo@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Ensure Log2 can actually be imported as intrinsic - #128678

Merged
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2
Jun 2, 2026
Merged

Ensure Log2 can actually be imported as intrinsic#128678
tannergooding merged 9 commits into
dotnet:mainfrom
tannergooding:better-log2

Conversation

@tannergooding

Copy link
Copy Markdown
Member

Previously this was code was never being hit because we were checking against the non-precise var_type and so we always saw TYP_INT even for unsigned inputs. Now we correctly handle checking for unsigned inputs and allow importing signed ones as well.

CopilotAI review requested due to automatic review settings May 28, 2026 03:48
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 28, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the CoreCLR JIT importer for the NI_PRIMITIVE_Log2 primitive intrinsic to correctly distinguish signed vs. unsigned inputs using the precise type, enabling the intrinsic expansion for unsigned arguments and adding a conditional throw path for signed arguments.

Changes:

  • Fixes signed/unsigned detection for Log2 by using JitType2PreciseVarType(baseJitType) rather than the non-precise baseType.
  • Implements the Log2 expansion via LeadingZeroCount(value | 1) to satisfy the 0 -> 0 contract.
  • Adds a conditional throw for negative signed inputs using a qmark-based fallback path.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergoodingforce-pushed the better-log2 branch 2 times, most recently from f76559c to eaeb042CompareMay 28, 2026 06:38
CopilotAI review requested due to automatic review settings May 28, 2026 06:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
CopilotAI review requested due to automatic review settings May 28, 2026 16:43

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
@tannergooding
tannergooding marked this pull request as ready for review May 29, 2026 02:37
CopilotAI review requested due to automatic review settings May 29, 2026 02:37
@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK) and move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

The diffs here are significant. We see -321k bytes of codegen on Linux Arm64

Literally 99% of diffs are in libraries_tests.run. and all example diffs imply that

[Intrinsic]publicstaticuintLog2(uintvalue)=>(uint)BitOperations.Log2(value);

is not inlined. I suspect it might be either PGO-driven or it needs an AggressiveInlining.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:
{A3A3FBCC-75AB-42F7-A074-1D18928B1FED}

or just slap AggressiveInlining everywhere

cc @dotnet/jit-contrib for opinions

Comment threadsrc/coreclr/jit/importercalls.cpp Outdated
Comment threadsrc/coreclr/jit/importercalls.cpp
Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

Copy link
Copy Markdown
MemberAuthor

I am not a fan of these intrinsifications (especially when it has to import yet another QMARK)

I am doubtful that QMARK will ever fully go away as they have a significant benefit in that they allow introducing block like functionality (namely a compare + branch to throw an exception) into the code. If anything, it will just be replaced with a helper that does the creation of the if/else blocks at the point you're currently creating the QMARK node instead. They exist because it simplifies a bit of handling in the early phases and even allows some constant folding without forcing the block to actually be produced.

move all possible math functions to JIT intrinsics while it should just naturally be handled in the inliner.

Most compilers do this for the fundamental math APIs like this, we're not special and even have stricter timing requirements which means there is more reason for us to handle such APIs intrinsically as it significantly reduces the amount of work the JIT has to do. The inliner is always going to have limitations and there's no reason to contribute to its pessimizations for these foundational APIs everything else is built on.

Literally 99% of diffs are in libraries_tests.run.

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others. That's why I explicitly called out the hits we also have in other areas. But that's also representative of this code being "hot" and common to code that most apps and binaries end up using, even if indirectly.

My opinion we should just either look why e.g. in such a small method we don't inline it without AggressiveInlining on it:

I don't think this is a fix and it just pushes things down the road more.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000:ldarg.0IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)IL_0006: ret

But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't. We skip all those checks, all the eating of the budget, and all the expensive handling by treating these key APIs as intrinsic. It completely bypasses the other expensive transforms and checks which ultimately simplifies what the JIT has to do, by adding a few lines to the importer to handle the known special scenarios.

@EgorBo

EgorBo commented May 29, 2026

Copy link
Copy Markdown
Member

I am doubtful that QMARK will ever fully go away as they have a significant benefit

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

The inliner is always going to have limitations

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users? I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about. Are you going to continue pushing various small methods as JIT intrinsics?

This is the case with most diffs, as libraries_tests is a significant amount of code compared to the others.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

The general problem here is already known as well and we've discussed it many times, which is the inliner has a budget and it means we still give up on trivially getters, setters, and other direct calls like this.

I do think we need to stop using it as an excuse. I agree on importing things as special opcodes so then JIT can emit those opcodes as part of other transformations and rely on proper expansion - that made sense to me, but it's no the case here as we just mimic inliner's work, not more than that.
Another downside of all of these intrinsics is that we continue adding binary size to jit while the original C# code can be trimmed.

AggressiveInlining should even do nothing here because we are below the skipBudgetChecksSize (currently 12):

L_0000: ldarg.0
IL_0001: call int32 [System.Runtime]System.Numerics.BitOperations::Log2(uint32)
IL_0006: ret
But it does give up anyways, because we have dozens of heuristics and other checks for something that ultimately looks expensive but really isn't

I think we need to study why we give up, it might be something fundamental like abstract generic resolution in importer.
Or you just copied a diff from Tier0 that is not inlined. What is the Tier1 method we should look at?

Comment threadsrc/coreclr/jit/importercalls.cpp
@tannergooding

tannergooding commented May 29, 2026

Copy link
Copy Markdown
MemberAuthor

Many phases between importer and global morph just bail on QMARKs (e.g. escape analysis), also, it introduces a very unobvious execution order (esp. nested qmarks) in trees all phases have to keep in mind, so removing it eventually will be a nice simplification. I am not saying it's bad, just needs a justification. E.g. I almost removed QMARKs for early cast expansion, my next goal is to remove the entire importervectorization.cpp, move it to a late phase.

My point here was rather that we have a fundamental need to introduce IR that represents x = cond ? throw : y and right now that is handled via QMARK. Even if we remove QMARK, it will just have to be replaced by something else that puts in the relevant blocks directly instead.

So even if they aren't the best IR today, the code is logically correct and will maintain roughly the current shape even when QMARKs disappear.

Why don't we intrinsify everything then, where is the line? Is Log2 that important for .NET users?

Because intrinsifying everything is both impossible and a negative. Most methods are not foundational in that sense and it would effectively cause "too much inlining", regress throughput, regress codegen size, etc.

Log2 is such a foundational helper, it and many of the APIs on the primitive types are the building blocks for all the other algorithms. It is an API that most compilers explicitly recognize and handle as intrinsic in some fashion, accordingly.

I think we need to continue investing into improving inliner instead. Making it much more reliable for things we care about.

I agree, but we also know this is much more complex work and that it will always have limits. So for foundational cases that are common to other compilers and where we can avoid some of the redundant work, it makes sense.

Are you going to continue pushing various small methods as JIT intrinsics?

The short answer is not really, fixing the existing Log2 handling really rounds out the set of them.

I'm not and have never been looking to intrinsify the world, only the key foundational math APIs like this one, i.e. the building blocks for the rest of .NET. Most of these were already handled and Log2 was one where we notably had handling but it was never firing because we were only checking TYP_INT and not TYP_UINT.

Of the foundational math APIs that aren't intrinsified, we only have integral Abs, Log10, Max, and Min. However, these likely aren't ever going to get handling because they aren't common to other compilers, don't have trivial hardware acceleration available, or do not have the size/ir complexity that other intrinsics have.

At best we might consider having Max/Min directly import as GT_CONDITIONAL to save on inlining and converting to those anyways, but then we really need the JIT to have better GT_CONDITIONAL support in the first place and because there isn't the size/ir complexity that other APIs have and we also want DPGO to work, I find it much less likely we do that.

All I see is a lot of pretty trivial tests for Log2 there which are now e.g. folded into constants in Tier0 thanks to this change.

Does the FullOpts metric include T0? If it does we should probably split that out so we can actually differentiate MinOpts vs T0 vs T1 vs FullOpts, I had assumed it was just T1+FullOpts

I had downloaded the full diffs and saw most of the impactful changes in code that was actually proper fullopts or T1, i.e. the asmdiff says ; FullOpts code

One of the more robust cases is:

- ; 93 inlinees with PGO data; 314 single block inlinees; 54 inlinees without PGO data
+ ; 41 inlinees with PGO data; 85 single block inlinees; 11 inlinees without PGO data

going from 611 locals with a frame size of 632 down to 243 locals with a frame size of 104 and from 10112 bytes of codegen down to 1847 bytes of codegen

But for non tests it improves the codegen around string formatting for all integer primitives (via CountDigits and CountHexDigits), array sorting and the various Sorted* collections, some BigInteger handling particularly around ModPow, etc

This is also why there are so many triggers in tests, because Log2 is used in APIs that most apps, even trivial ones, end up using, so it triggers for a lot of code.

@EgorBoEgorBo 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.

Leaving this up to you, I am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it. I understand that the debugging experience is worse in optimized code but I believe we've never changed the content of the exception before based on opts.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

am a litte bit concerned about the changed behavior with the different Exception message between Tier0/Debug/Tier1 and if it's worth it

CC. @jkotas for input on this bit. The consideration is essentially whether it is okay for T1 (optimized) codegen for Log2 to differ in the message included as part of ArgumentOutOfRangeException from T0 (unoptimized/debug/t0) codegen?

I don't feel its worth complicating the JIT/VM to support preserving that message (ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum) in the actual JIT throw helper support and it is okay for T1 code to differ here, particularly since its the only exception for this API.

The alternatives would be:

  • Also drop the info from the exception in the managed fallback path
  • Instead of producing a gtNewMustThrowException node, keep a second Log2 intrinsic and use the existing rationalization rewriting support to force it back to a call (this loses info that it definitively throws and is cold, which can be added is just a bit more complex)

Leaving this up to you

As for the overall change @EgorBo, I do think its worth it today and plan on taking it. We know Log2 is a building block API and had already had the rest of the foundational support here, it is very rare we get a change where the diffs (-321k bytes) and TP (-0.30% instructions retired) improvement are so definitively good (including both tests and some key production code methods, such as the primitive formatting/parsing helpers).

I also do understand the desire to handle these scenarios more generally via the inliner, but that's not going to happen today or even this release. I am more than happy to see us drop such JIT support in the future when the inliner is capable of handling it, the same as we've done with other intrinsic functionality (such as when we finally dropped the direct support for S.N.Vector2/3/4, Quaternion, Plane, etc).

@jkotas

Copy link
Copy Markdown
Member

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

@tannergooding

Copy link
Copy Markdown
MemberAuthor

I do not think this is a good reason to start deviating error messages between debug and release for methods that are expected to be fully deterministic.

I'll see the diffs around the second alternative I gave then, carrying the throw path through as the rewritable intrinsic so it forces it back to the managed path in that case and preserves the exception.

CopilotAI review requested due to automatic review settings June 1, 2026 23:59

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings June 2, 2026 02:31

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@tannergooding
tannergooding merged commit 7dc7499 into dotnet:mainJun 2, 2026
163 of 167 checks passed
@tannergooding
tannergooding deleted the better-log2 branch June 2, 2026 12:25
@dotnet-milestone-botdotnet-milestone-botBot added this to the 11.0-preview6 milestone Jun 3, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@tannergooding@EgorBo@jkotas