Skip to content

Eliminate dead branches around typeof comparisons - #102248

Merged
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches
Jun 19, 2024
Merged

Eliminate dead branches around typeof comparisons#102248
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

RyuJIT will already do dead branch elimination for typeof(X) == typeof(Y) patterns, but we couldn't do elimination around foo == typeof(X). This fixes that using whole program knowledge - if we never saw a constructed MT for X, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.

I'm doing this in SubstitutedILProvider instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. SubstitutedILProvider is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).

Cc @dotnet/ilc-contrib

RyuJIT will already do dead branch elimination for `typeof(X) == typeof(Y)` patterns, but we couldn't do elimination around `foo == typeof(X)`. This fixes that using whole program knowledge - if we never saw a constructed `MT` for `X`, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.
I'm doing this in `SubstitutedILProvider` instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. `SubstitutedILProvider` is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@github-actionsgithub-actionsBot mentioned this pull request May 15, 2024
@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

if (reader.ReadILOpcode() is not ILOpcode.callvirt and not ILOpcode.call)
return false;

// We don't actually mind if this is not Object.GetType

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If it is an arbitrary call, can it return a type that happens to be equal to the other type?

Or is the idea that this case will fail the CanReferenceConstructedTypeOrCanonicalFormOfType check below? Ie the other argument can be anything. We are just skipping the specific common patterns here to keep things simple.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should be okay with any value loaded from a local or parameter. So also any value a method call could return.

We just don't have facilities to accept any value, so only a couple recognized patterns are allowed. Allowing any instance method call is less work than also checking if it's object.GetType.

if (knownType.IsCanonicalDefinitionType(CanonicalFormKind.Any))
return false;

if (_devirtualizationManager.CanReferenceConstructedTypeOrCanonicalFormOfType(knownType))

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.

Do we need to call convert ConvertToCanonForm before calling CanReferenceConstructedTypeOrCanonicalFormOfType? Or is the type guaranteed to be normalized somehow?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should convert to canon. Good catch.

@jkotas

Copy link
Copy Markdown
Member

I have run this under debugger on this simple test:

using System;
static class Program
{
static void Main(string[] args)
{
if (typeof(MyType) == args.GetType())
Console.WriteLine(42);
}
}
static class MyType
{
}

I would expect the substitution to trigger for it, but it is not happening. It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

Yes, you need to flip them to the more common pattern.

The problem is in the IL scanner. IL scanner only does the "downgrade result of typeof to necessary MethodTable" for a limited set of IL patterns as well and this one is not it. So we end up with "constructed MethodTable is needed" in the scanning phase, and this can no longer get optimized away.

// We expect pattern:
//
// ldtoken Foo
// call GetTypeFromHandle
// ldtoken Bar
// call GetTypeFromHandle
// call Equals
//
// We check for both ldtoken cases
if((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+6);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}
elseif((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.ldtoken
&&_basicBlocks[_currentOffset+10]==null
&&(ILOpcode)_ilBytes[_currentOffset+10]==ILOpcode.call
&&methodToken==ReadILTokenAt(_currentOffset+11)
&&_basicBlocks[_currentOffset+15]==null
&&(ILOpcode)_ilBytes[_currentOffset+15]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+16);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}

We really need some better facilities to analyze IL in C#, but also I don't know if I want us to build a "proper" IL importer in C#.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

(I plan to look into at least sharing this code between scanner and substitutions in some way.)

@jkotas

Copy link
Copy Markdown
Member

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

It's the tests that are part of this PR. We also have hits in corelib, for example:

if(attributeType==typeof(DecimalConstantAttribute))
{
returnGetRawDecimalConstant(attributeData);
}
elseif(attributeType.IsSubclassOf(typeof(CustomConstantAttribute)))
{
if(attributeType==typeof(DateTimeConstantAttribute))
{
returnGetRawDateTimeConstant(attributeData);
}
returnGetRawConstant(attributeData);
}

(The above will also be a real saving once we can do this optimization in the scanner - this is the only places that boxes DateTime and Decimal and that's a 100 kB saving on an app that uses reflection. It doesn't kick in right now, because the scanner will see we box DateTime/decimal and that destroys our opportunity to get rid of it because DateTime/decimal is referenced in typeof comparisons in other spots.)

@jkotas

Copy link
Copy Markdown
Member

It's the tests that are part of this PR.

I have extracted the test into a small program:

using System;
using System.Runtime.CompilerServices;
static class Program
{
static void Main(string[] args)
{
Type someType = GetTheType();
if (someType == typeof(Never3))
{
Console.WriteLine(42);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static Type GetTheType() => null;
}
class Never3
{
}

I have compiled the test in release mode (the test is under #if !DEBUG). Roslyn optimized out the someType local variable and the IL looks like this:

 IL_0000: call class [System.Runtime]System.Type Program::GetTheType()
IL_0005: ldtoken MyType
IL_000a: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
IL_000f: call bool [System.Runtime]System.Type::op_Equality(class [System.Runtime]System.Type,
class [System.Runtime]System.Type)

It fails the pattern match in TryExpandTypeEquality_TokenOther very early since the ldloc that the pattern match is looking for is gone. What am I missing?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

The tests are all failing in #102374 so the optimization here works. I agree that for the local case this is pretty fragile. This is another case where the expectation is that this will mostly come from a parameter in real world code. Loading it from a local was just equally cheap in the pattern match so I just allowed it. But parameter is the main use case.

@jkotas

jkotas commented May 17, 2024

Copy link
Copy Markdown
Member

I have figured out one of the mysteries:

The dotnet/runtime build sets DebugSymbols property to true globally. DebugSymbols does not actually do what its name suggests. The (portable) symbols are generated regardless of whether this property is true or false. What this property actually does is that it disables C# peephole IL optimizations. The C# peephole IL optimizations break the IL patterns used by the tests added in this PR. Setting the DebugSymbols to false makes the tests fail as demonstrated by #102391 . It would be nice to fix the pattern match and/or the test to work with DebugSymbols set to false.

The ordinary user projects out there do not set DebugSymbols property. I have done my quick ad-hoc test using an ordinary project and it is why it did not work for me. I will look into deleting the DebugSymbols setting so that we build and test our bits using the same settings as our users.

@jkotas

jkotas commented May 18, 2024

Copy link
Copy Markdown
Member

Yes, you need to flip them to the more common pattern.

Ok, this was the other part of the mystery. if (t == typeof(Never)) works as expected, if (typeof(Never) == t) does not work as expected. The code added in this PR handles it, but the pre-existing ldtoken handling in the scanner does not as you have pointed out.

{
Debug.Assert(type.NormalizeInstantiation() == type);
Debug.Assert(ConstructedEETypeNode.CreationAllowed(type));
return _constructedMethodTables.Contains(type);

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.

Should we also assert that we are only adding normalizations into _constructedMethodTables when it is populated?

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

Thanks

@MichalStrehovsky
MichalStrehovsky merged commit e0bd776 into dotnet:mainJun 19, 2024
@MichalStrehovsky
MichalStrehovsky deleted the deadtypeofbranches branch June 19, 2024 14:21
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 19, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 24, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 1, 2024
This fixes the problem discussed at #102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jul 1, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 18, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in #102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 20, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@MichalStrehovsky@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
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;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Eliminate dead branches around typeof comparisons by MichalStrehovsky · Pull Request #102248 · dotnet/runtime · GitHub
Skip to content

Eliminate dead branches around typeof comparisons - #102248

Merged
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches
Jun 19, 2024
Merged

Eliminate dead branches around typeof comparisons#102248
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

RyuJIT will already do dead branch elimination for typeof(X) == typeof(Y) patterns, but we couldn't do elimination around foo == typeof(X). This fixes that using whole program knowledge - if we never saw a constructed MT for X, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.

I'm doing this in SubstitutedILProvider instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. SubstitutedILProvider is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).

Cc @dotnet/ilc-contrib

RyuJIT will already do dead branch elimination for `typeof(X) == typeof(Y)` patterns, but we couldn't do elimination around `foo == typeof(X)`. This fixes that using whole program knowledge - if we never saw a constructed `MT` for `X`, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.
I'm doing this in `SubstitutedILProvider` instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. `SubstitutedILProvider` is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@github-actionsgithub-actionsBot mentioned this pull request May 15, 2024
@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

if (reader.ReadILOpcode() is not ILOpcode.callvirt and not ILOpcode.call)
return false;

// We don't actually mind if this is not Object.GetType

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If it is an arbitrary call, can it return a type that happens to be equal to the other type?

Or is the idea that this case will fail the CanReferenceConstructedTypeOrCanonicalFormOfType check below? Ie the other argument can be anything. We are just skipping the specific common patterns here to keep things simple.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should be okay with any value loaded from a local or parameter. So also any value a method call could return.

We just don't have facilities to accept any value, so only a couple recognized patterns are allowed. Allowing any instance method call is less work than also checking if it's object.GetType.

if (knownType.IsCanonicalDefinitionType(CanonicalFormKind.Any))
return false;

if (_devirtualizationManager.CanReferenceConstructedTypeOrCanonicalFormOfType(knownType))

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.

Do we need to call convert ConvertToCanonForm before calling CanReferenceConstructedTypeOrCanonicalFormOfType? Or is the type guaranteed to be normalized somehow?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should convert to canon. Good catch.

@jkotas

Copy link
Copy Markdown
Member

I have run this under debugger on this simple test:

using System;
static class Program
{
static void Main(string[] args)
{
if (typeof(MyType) == args.GetType())
Console.WriteLine(42);
}
}
static class MyType
{
}

I would expect the substitution to trigger for it, but it is not happening. It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

Yes, you need to flip them to the more common pattern.

The problem is in the IL scanner. IL scanner only does the "downgrade result of typeof to necessary MethodTable" for a limited set of IL patterns as well and this one is not it. So we end up with "constructed MethodTable is needed" in the scanning phase, and this can no longer get optimized away.

// We expect pattern:
//
// ldtoken Foo
// call GetTypeFromHandle
// ldtoken Bar
// call GetTypeFromHandle
// call Equals
//
// We check for both ldtoken cases
if((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+6);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}
elseif((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.ldtoken
&&_basicBlocks[_currentOffset+10]==null
&&(ILOpcode)_ilBytes[_currentOffset+10]==ILOpcode.call
&&methodToken==ReadILTokenAt(_currentOffset+11)
&&_basicBlocks[_currentOffset+15]==null
&&(ILOpcode)_ilBytes[_currentOffset+15]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+16);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}

We really need some better facilities to analyze IL in C#, but also I don't know if I want us to build a "proper" IL importer in C#.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

(I plan to look into at least sharing this code between scanner and substitutions in some way.)

@jkotas

Copy link
Copy Markdown
Member

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

It's the tests that are part of this PR. We also have hits in corelib, for example:

if(attributeType==typeof(DecimalConstantAttribute))
{
returnGetRawDecimalConstant(attributeData);
}
elseif(attributeType.IsSubclassOf(typeof(CustomConstantAttribute)))
{
if(attributeType==typeof(DateTimeConstantAttribute))
{
returnGetRawDateTimeConstant(attributeData);
}
returnGetRawConstant(attributeData);
}

(The above will also be a real saving once we can do this optimization in the scanner - this is the only places that boxes DateTime and Decimal and that's a 100 kB saving on an app that uses reflection. It doesn't kick in right now, because the scanner will see we box DateTime/decimal and that destroys our opportunity to get rid of it because DateTime/decimal is referenced in typeof comparisons in other spots.)

@jkotas

Copy link
Copy Markdown
Member

It's the tests that are part of this PR.

I have extracted the test into a small program:

using System;
using System.Runtime.CompilerServices;
static class Program
{
static void Main(string[] args)
{
Type someType = GetTheType();
if (someType == typeof(Never3))
{
Console.WriteLine(42);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static Type GetTheType() => null;
}
class Never3
{
}

I have compiled the test in release mode (the test is under #if !DEBUG). Roslyn optimized out the someType local variable and the IL looks like this:

 IL_0000: call class [System.Runtime]System.Type Program::GetTheType()
IL_0005: ldtoken MyType
IL_000a: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
IL_000f: call bool [System.Runtime]System.Type::op_Equality(class [System.Runtime]System.Type,
class [System.Runtime]System.Type)

It fails the pattern match in TryExpandTypeEquality_TokenOther very early since the ldloc that the pattern match is looking for is gone. What am I missing?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

The tests are all failing in #102374 so the optimization here works. I agree that for the local case this is pretty fragile. This is another case where the expectation is that this will mostly come from a parameter in real world code. Loading it from a local was just equally cheap in the pattern match so I just allowed it. But parameter is the main use case.

@jkotas

jkotas commented May 17, 2024

Copy link
Copy Markdown
Member

I have figured out one of the mysteries:

The dotnet/runtime build sets DebugSymbols property to true globally. DebugSymbols does not actually do what its name suggests. The (portable) symbols are generated regardless of whether this property is true or false. What this property actually does is that it disables C# peephole IL optimizations. The C# peephole IL optimizations break the IL patterns used by the tests added in this PR. Setting the DebugSymbols to false makes the tests fail as demonstrated by #102391 . It would be nice to fix the pattern match and/or the test to work with DebugSymbols set to false.

The ordinary user projects out there do not set DebugSymbols property. I have done my quick ad-hoc test using an ordinary project and it is why it did not work for me. I will look into deleting the DebugSymbols setting so that we build and test our bits using the same settings as our users.

@jkotas

jkotas commented May 18, 2024

Copy link
Copy Markdown
Member

Yes, you need to flip them to the more common pattern.

Ok, this was the other part of the mystery. if (t == typeof(Never)) works as expected, if (typeof(Never) == t) does not work as expected. The code added in this PR handles it, but the pre-existing ldtoken handling in the scanner does not as you have pointed out.

{
Debug.Assert(type.NormalizeInstantiation() == type);
Debug.Assert(ConstructedEETypeNode.CreationAllowed(type));
return _constructedMethodTables.Contains(type);

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.

Should we also assert that we are only adding normalizations into _constructedMethodTables when it is populated?

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

Thanks

@MichalStrehovsky
MichalStrehovsky merged commit e0bd776 into dotnet:mainJun 19, 2024
@MichalStrehovsky
MichalStrehovsky deleted the deadtypeofbranches branch June 19, 2024 14:21
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 19, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 24, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 1, 2024
This fixes the problem discussed at #102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jul 1, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 18, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in #102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 20, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

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

Eliminate dead branches around typeof comparisons - #102248

Merged
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches
Jun 19, 2024
Merged

Eliminate dead branches around typeof comparisons#102248
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

RyuJIT will already do dead branch elimination for typeof(X) == typeof(Y) patterns, but we couldn't do elimination around foo == typeof(X). This fixes that using whole program knowledge - if we never saw a constructed MT for X, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.

I'm doing this in SubstitutedILProvider instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. SubstitutedILProvider is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).

Cc @dotnet/ilc-contrib

RyuJIT will already do dead branch elimination for `typeof(X) == typeof(Y)` patterns, but we couldn't do elimination around `foo == typeof(X)`. This fixes that using whole program knowledge - if we never saw a constructed `MT` for `X`, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.
I'm doing this in `SubstitutedILProvider` instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. `SubstitutedILProvider` is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@github-actionsgithub-actionsBot mentioned this pull request May 15, 2024
@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

if (reader.ReadILOpcode() is not ILOpcode.callvirt and not ILOpcode.call)
return false;

// We don't actually mind if this is not Object.GetType

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If it is an arbitrary call, can it return a type that happens to be equal to the other type?

Or is the idea that this case will fail the CanReferenceConstructedTypeOrCanonicalFormOfType check below? Ie the other argument can be anything. We are just skipping the specific common patterns here to keep things simple.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should be okay with any value loaded from a local or parameter. So also any value a method call could return.

We just don't have facilities to accept any value, so only a couple recognized patterns are allowed. Allowing any instance method call is less work than also checking if it's object.GetType.

if (knownType.IsCanonicalDefinitionType(CanonicalFormKind.Any))
return false;

if (_devirtualizationManager.CanReferenceConstructedTypeOrCanonicalFormOfType(knownType))

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.

Do we need to call convert ConvertToCanonForm before calling CanReferenceConstructedTypeOrCanonicalFormOfType? Or is the type guaranteed to be normalized somehow?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should convert to canon. Good catch.

@jkotas

Copy link
Copy Markdown
Member

I have run this under debugger on this simple test:

using System;
static class Program
{
static void Main(string[] args)
{
if (typeof(MyType) == args.GetType())
Console.WriteLine(42);
}
}
static class MyType
{
}

I would expect the substitution to trigger for it, but it is not happening. It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

Yes, you need to flip them to the more common pattern.

The problem is in the IL scanner. IL scanner only does the "downgrade result of typeof to necessary MethodTable" for a limited set of IL patterns as well and this one is not it. So we end up with "constructed MethodTable is needed" in the scanning phase, and this can no longer get optimized away.

// We expect pattern:
//
// ldtoken Foo
// call GetTypeFromHandle
// ldtoken Bar
// call GetTypeFromHandle
// call Equals
//
// We check for both ldtoken cases
if((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+6);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}
elseif((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.ldtoken
&&_basicBlocks[_currentOffset+10]==null
&&(ILOpcode)_ilBytes[_currentOffset+10]==ILOpcode.call
&&methodToken==ReadILTokenAt(_currentOffset+11)
&&_basicBlocks[_currentOffset+15]==null
&&(ILOpcode)_ilBytes[_currentOffset+15]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+16);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}

We really need some better facilities to analyze IL in C#, but also I don't know if I want us to build a "proper" IL importer in C#.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

(I plan to look into at least sharing this code between scanner and substitutions in some way.)

@jkotas

Copy link
Copy Markdown
Member

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

It's the tests that are part of this PR. We also have hits in corelib, for example:

if(attributeType==typeof(DecimalConstantAttribute))
{
returnGetRawDecimalConstant(attributeData);
}
elseif(attributeType.IsSubclassOf(typeof(CustomConstantAttribute)))
{
if(attributeType==typeof(DateTimeConstantAttribute))
{
returnGetRawDateTimeConstant(attributeData);
}
returnGetRawConstant(attributeData);
}

(The above will also be a real saving once we can do this optimization in the scanner - this is the only places that boxes DateTime and Decimal and that's a 100 kB saving on an app that uses reflection. It doesn't kick in right now, because the scanner will see we box DateTime/decimal and that destroys our opportunity to get rid of it because DateTime/decimal is referenced in typeof comparisons in other spots.)

@jkotas

Copy link
Copy Markdown
Member

It's the tests that are part of this PR.

I have extracted the test into a small program:

using System;
using System.Runtime.CompilerServices;
static class Program
{
static void Main(string[] args)
{
Type someType = GetTheType();
if (someType == typeof(Never3))
{
Console.WriteLine(42);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static Type GetTheType() => null;
}
class Never3
{
}

I have compiled the test in release mode (the test is under #if !DEBUG). Roslyn optimized out the someType local variable and the IL looks like this:

 IL_0000: call class [System.Runtime]System.Type Program::GetTheType()
IL_0005: ldtoken MyType
IL_000a: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
IL_000f: call bool [System.Runtime]System.Type::op_Equality(class [System.Runtime]System.Type,
class [System.Runtime]System.Type)

It fails the pattern match in TryExpandTypeEquality_TokenOther very early since the ldloc that the pattern match is looking for is gone. What am I missing?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

The tests are all failing in #102374 so the optimization here works. I agree that for the local case this is pretty fragile. This is another case where the expectation is that this will mostly come from a parameter in real world code. Loading it from a local was just equally cheap in the pattern match so I just allowed it. But parameter is the main use case.

@jkotas

jkotas commented May 17, 2024

Copy link
Copy Markdown
Member

I have figured out one of the mysteries:

The dotnet/runtime build sets DebugSymbols property to true globally. DebugSymbols does not actually do what its name suggests. The (portable) symbols are generated regardless of whether this property is true or false. What this property actually does is that it disables C# peephole IL optimizations. The C# peephole IL optimizations break the IL patterns used by the tests added in this PR. Setting the DebugSymbols to false makes the tests fail as demonstrated by #102391 . It would be nice to fix the pattern match and/or the test to work with DebugSymbols set to false.

The ordinary user projects out there do not set DebugSymbols property. I have done my quick ad-hoc test using an ordinary project and it is why it did not work for me. I will look into deleting the DebugSymbols setting so that we build and test our bits using the same settings as our users.

@jkotas

jkotas commented May 18, 2024

Copy link
Copy Markdown
Member

Yes, you need to flip them to the more common pattern.

Ok, this was the other part of the mystery. if (t == typeof(Never)) works as expected, if (typeof(Never) == t) does not work as expected. The code added in this PR handles it, but the pre-existing ldtoken handling in the scanner does not as you have pointed out.

{
Debug.Assert(type.NormalizeInstantiation() == type);
Debug.Assert(ConstructedEETypeNode.CreationAllowed(type));
return _constructedMethodTables.Contains(type);

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.

Should we also assert that we are only adding normalizations into _constructedMethodTables when it is populated?

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

Thanks

@MichalStrehovsky
MichalStrehovsky merged commit e0bd776 into dotnet:mainJun 19, 2024
@MichalStrehovsky
MichalStrehovsky deleted the deadtypeofbranches branch June 19, 2024 14:21
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 19, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 24, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 1, 2024
This fixes the problem discussed at #102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jul 1, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 18, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in #102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 20, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

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

Eliminate dead branches around typeof comparisons - #102248

Merged
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches
Jun 19, 2024
Merged

Eliminate dead branches around typeof comparisons#102248
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

RyuJIT will already do dead branch elimination for typeof(X) == typeof(Y) patterns, but we couldn't do elimination around foo == typeof(X). This fixes that using whole program knowledge - if we never saw a constructed MT for X, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.

I'm doing this in SubstitutedILProvider instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. SubstitutedILProvider is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).

Cc @dotnet/ilc-contrib

RyuJIT will already do dead branch elimination for `typeof(X) == typeof(Y)` patterns, but we couldn't do elimination around `foo == typeof(X)`. This fixes that using whole program knowledge - if we never saw a constructed `MT` for `X`, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.
I'm doing this in `SubstitutedILProvider` instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. `SubstitutedILProvider` is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@github-actionsgithub-actionsBot mentioned this pull request May 15, 2024
@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

if (reader.ReadILOpcode() is not ILOpcode.callvirt and not ILOpcode.call)
return false;

// We don't actually mind if this is not Object.GetType

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If it is an arbitrary call, can it return a type that happens to be equal to the other type?

Or is the idea that this case will fail the CanReferenceConstructedTypeOrCanonicalFormOfType check below? Ie the other argument can be anything. We are just skipping the specific common patterns here to keep things simple.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should be okay with any value loaded from a local or parameter. So also any value a method call could return.

We just don't have facilities to accept any value, so only a couple recognized patterns are allowed. Allowing any instance method call is less work than also checking if it's object.GetType.

if (knownType.IsCanonicalDefinitionType(CanonicalFormKind.Any))
return false;

if (_devirtualizationManager.CanReferenceConstructedTypeOrCanonicalFormOfType(knownType))

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.

Do we need to call convert ConvertToCanonForm before calling CanReferenceConstructedTypeOrCanonicalFormOfType? Or is the type guaranteed to be normalized somehow?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should convert to canon. Good catch.

@jkotas

Copy link
Copy Markdown
Member

I have run this under debugger on this simple test:

using System;
static class Program
{
static void Main(string[] args)
{
if (typeof(MyType) == args.GetType())
Console.WriteLine(42);
}
}
static class MyType
{
}

I would expect the substitution to trigger for it, but it is not happening. It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

Yes, you need to flip them to the more common pattern.

The problem is in the IL scanner. IL scanner only does the "downgrade result of typeof to necessary MethodTable" for a limited set of IL patterns as well and this one is not it. So we end up with "constructed MethodTable is needed" in the scanning phase, and this can no longer get optimized away.

// We expect pattern:
//
// ldtoken Foo
// call GetTypeFromHandle
// ldtoken Bar
// call GetTypeFromHandle
// call Equals
//
// We check for both ldtoken cases
if((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+6);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}
elseif((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.ldtoken
&&_basicBlocks[_currentOffset+10]==null
&&(ILOpcode)_ilBytes[_currentOffset+10]==ILOpcode.call
&&methodToken==ReadILTokenAt(_currentOffset+11)
&&_basicBlocks[_currentOffset+15]==null
&&(ILOpcode)_ilBytes[_currentOffset+15]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+16);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}

We really need some better facilities to analyze IL in C#, but also I don't know if I want us to build a "proper" IL importer in C#.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

(I plan to look into at least sharing this code between scanner and substitutions in some way.)

@jkotas

Copy link
Copy Markdown
Member

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

It's the tests that are part of this PR. We also have hits in corelib, for example:

if(attributeType==typeof(DecimalConstantAttribute))
{
returnGetRawDecimalConstant(attributeData);
}
elseif(attributeType.IsSubclassOf(typeof(CustomConstantAttribute)))
{
if(attributeType==typeof(DateTimeConstantAttribute))
{
returnGetRawDateTimeConstant(attributeData);
}
returnGetRawConstant(attributeData);
}

(The above will also be a real saving once we can do this optimization in the scanner - this is the only places that boxes DateTime and Decimal and that's a 100 kB saving on an app that uses reflection. It doesn't kick in right now, because the scanner will see we box DateTime/decimal and that destroys our opportunity to get rid of it because DateTime/decimal is referenced in typeof comparisons in other spots.)

@jkotas

Copy link
Copy Markdown
Member

It's the tests that are part of this PR.

I have extracted the test into a small program:

using System;
using System.Runtime.CompilerServices;
static class Program
{
static void Main(string[] args)
{
Type someType = GetTheType();
if (someType == typeof(Never3))
{
Console.WriteLine(42);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static Type GetTheType() => null;
}
class Never3
{
}

I have compiled the test in release mode (the test is under #if !DEBUG). Roslyn optimized out the someType local variable and the IL looks like this:

 IL_0000: call class [System.Runtime]System.Type Program::GetTheType()
IL_0005: ldtoken MyType
IL_000a: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
IL_000f: call bool [System.Runtime]System.Type::op_Equality(class [System.Runtime]System.Type,
class [System.Runtime]System.Type)

It fails the pattern match in TryExpandTypeEquality_TokenOther very early since the ldloc that the pattern match is looking for is gone. What am I missing?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

The tests are all failing in #102374 so the optimization here works. I agree that for the local case this is pretty fragile. This is another case where the expectation is that this will mostly come from a parameter in real world code. Loading it from a local was just equally cheap in the pattern match so I just allowed it. But parameter is the main use case.

@jkotas

jkotas commented May 17, 2024

Copy link
Copy Markdown
Member

I have figured out one of the mysteries:

The dotnet/runtime build sets DebugSymbols property to true globally. DebugSymbols does not actually do what its name suggests. The (portable) symbols are generated regardless of whether this property is true or false. What this property actually does is that it disables C# peephole IL optimizations. The C# peephole IL optimizations break the IL patterns used by the tests added in this PR. Setting the DebugSymbols to false makes the tests fail as demonstrated by #102391 . It would be nice to fix the pattern match and/or the test to work with DebugSymbols set to false.

The ordinary user projects out there do not set DebugSymbols property. I have done my quick ad-hoc test using an ordinary project and it is why it did not work for me. I will look into deleting the DebugSymbols setting so that we build and test our bits using the same settings as our users.

@jkotas

jkotas commented May 18, 2024

Copy link
Copy Markdown
Member

Yes, you need to flip them to the more common pattern.

Ok, this was the other part of the mystery. if (t == typeof(Never)) works as expected, if (typeof(Never) == t) does not work as expected. The code added in this PR handles it, but the pre-existing ldtoken handling in the scanner does not as you have pointed out.

{
Debug.Assert(type.NormalizeInstantiation() == type);
Debug.Assert(ConstructedEETypeNode.CreationAllowed(type));
return _constructedMethodTables.Contains(type);

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.

Should we also assert that we are only adding normalizations into _constructedMethodTables when it is populated?

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

Thanks

@MichalStrehovsky
MichalStrehovsky merged commit e0bd776 into dotnet:mainJun 19, 2024
@MichalStrehovsky
MichalStrehovsky deleted the deadtypeofbranches branch June 19, 2024 14:21
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 19, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 24, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 1, 2024
This fixes the problem discussed at #102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jul 1, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 18, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in #102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 20, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

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

Eliminate dead branches around typeof comparisons - #102248

Merged
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches
Jun 19, 2024
Merged

Eliminate dead branches around typeof comparisons#102248
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

RyuJIT will already do dead branch elimination for typeof(X) == typeof(Y) patterns, but we couldn't do elimination around foo == typeof(X). This fixes that using whole program knowledge - if we never saw a constructed MT for X, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.

I'm doing this in SubstitutedILProvider instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. SubstitutedILProvider is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).

Cc @dotnet/ilc-contrib

RyuJIT will already do dead branch elimination for `typeof(X) == typeof(Y)` patterns, but we couldn't do elimination around `foo == typeof(X)`. This fixes that using whole program knowledge - if we never saw a constructed `MT` for `X`, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.
I'm doing this in `SubstitutedILProvider` instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. `SubstitutedILProvider` is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@github-actionsgithub-actionsBot mentioned this pull request May 15, 2024
@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

if (reader.ReadILOpcode() is not ILOpcode.callvirt and not ILOpcode.call)
return false;

// We don't actually mind if this is not Object.GetType

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If it is an arbitrary call, can it return a type that happens to be equal to the other type?

Or is the idea that this case will fail the CanReferenceConstructedTypeOrCanonicalFormOfType check below? Ie the other argument can be anything. We are just skipping the specific common patterns here to keep things simple.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should be okay with any value loaded from a local or parameter. So also any value a method call could return.

We just don't have facilities to accept any value, so only a couple recognized patterns are allowed. Allowing any instance method call is less work than also checking if it's object.GetType.

if (knownType.IsCanonicalDefinitionType(CanonicalFormKind.Any))
return false;

if (_devirtualizationManager.CanReferenceConstructedTypeOrCanonicalFormOfType(knownType))

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.

Do we need to call convert ConvertToCanonForm before calling CanReferenceConstructedTypeOrCanonicalFormOfType? Or is the type guaranteed to be normalized somehow?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should convert to canon. Good catch.

@jkotas

Copy link
Copy Markdown
Member

I have run this under debugger on this simple test:

using System;
static class Program
{
static void Main(string[] args)
{
if (typeof(MyType) == args.GetType())
Console.WriteLine(42);
}
}
static class MyType
{
}

I would expect the substitution to trigger for it, but it is not happening. It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

Yes, you need to flip them to the more common pattern.

The problem is in the IL scanner. IL scanner only does the "downgrade result of typeof to necessary MethodTable" for a limited set of IL patterns as well and this one is not it. So we end up with "constructed MethodTable is needed" in the scanning phase, and this can no longer get optimized away.

// We expect pattern:
//
// ldtoken Foo
// call GetTypeFromHandle
// ldtoken Bar
// call GetTypeFromHandle
// call Equals
//
// We check for both ldtoken cases
if((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+6);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}
elseif((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.ldtoken
&&_basicBlocks[_currentOffset+10]==null
&&(ILOpcode)_ilBytes[_currentOffset+10]==ILOpcode.call
&&methodToken==ReadILTokenAt(_currentOffset+11)
&&_basicBlocks[_currentOffset+15]==null
&&(ILOpcode)_ilBytes[_currentOffset+15]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+16);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}

We really need some better facilities to analyze IL in C#, but also I don't know if I want us to build a "proper" IL importer in C#.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

(I plan to look into at least sharing this code between scanner and substitutions in some way.)

@jkotas

Copy link
Copy Markdown
Member

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

It's the tests that are part of this PR. We also have hits in corelib, for example:

if(attributeType==typeof(DecimalConstantAttribute))
{
returnGetRawDecimalConstant(attributeData);
}
elseif(attributeType.IsSubclassOf(typeof(CustomConstantAttribute)))
{
if(attributeType==typeof(DateTimeConstantAttribute))
{
returnGetRawDateTimeConstant(attributeData);
}
returnGetRawConstant(attributeData);
}

(The above will also be a real saving once we can do this optimization in the scanner - this is the only places that boxes DateTime and Decimal and that's a 100 kB saving on an app that uses reflection. It doesn't kick in right now, because the scanner will see we box DateTime/decimal and that destroys our opportunity to get rid of it because DateTime/decimal is referenced in typeof comparisons in other spots.)

@jkotas

Copy link
Copy Markdown
Member

It's the tests that are part of this PR.

I have extracted the test into a small program:

using System;
using System.Runtime.CompilerServices;
static class Program
{
static void Main(string[] args)
{
Type someType = GetTheType();
if (someType == typeof(Never3))
{
Console.WriteLine(42);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static Type GetTheType() => null;
}
class Never3
{
}

I have compiled the test in release mode (the test is under #if !DEBUG). Roslyn optimized out the someType local variable and the IL looks like this:

 IL_0000: call class [System.Runtime]System.Type Program::GetTheType()
IL_0005: ldtoken MyType
IL_000a: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
IL_000f: call bool [System.Runtime]System.Type::op_Equality(class [System.Runtime]System.Type,
class [System.Runtime]System.Type)

It fails the pattern match in TryExpandTypeEquality_TokenOther very early since the ldloc that the pattern match is looking for is gone. What am I missing?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

The tests are all failing in #102374 so the optimization here works. I agree that for the local case this is pretty fragile. This is another case where the expectation is that this will mostly come from a parameter in real world code. Loading it from a local was just equally cheap in the pattern match so I just allowed it. But parameter is the main use case.

@jkotas

jkotas commented May 17, 2024

Copy link
Copy Markdown
Member

I have figured out one of the mysteries:

The dotnet/runtime build sets DebugSymbols property to true globally. DebugSymbols does not actually do what its name suggests. The (portable) symbols are generated regardless of whether this property is true or false. What this property actually does is that it disables C# peephole IL optimizations. The C# peephole IL optimizations break the IL patterns used by the tests added in this PR. Setting the DebugSymbols to false makes the tests fail as demonstrated by #102391 . It would be nice to fix the pattern match and/or the test to work with DebugSymbols set to false.

The ordinary user projects out there do not set DebugSymbols property. I have done my quick ad-hoc test using an ordinary project and it is why it did not work for me. I will look into deleting the DebugSymbols setting so that we build and test our bits using the same settings as our users.

@jkotas

jkotas commented May 18, 2024

Copy link
Copy Markdown
Member

Yes, you need to flip them to the more common pattern.

Ok, this was the other part of the mystery. if (t == typeof(Never)) works as expected, if (typeof(Never) == t) does not work as expected. The code added in this PR handles it, but the pre-existing ldtoken handling in the scanner does not as you have pointed out.

{
Debug.Assert(type.NormalizeInstantiation() == type);
Debug.Assert(ConstructedEETypeNode.CreationAllowed(type));
return _constructedMethodTables.Contains(type);

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.

Should we also assert that we are only adding normalizations into _constructedMethodTables when it is populated?

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

Thanks

@MichalStrehovsky
MichalStrehovsky merged commit e0bd776 into dotnet:mainJun 19, 2024
@MichalStrehovsky
MichalStrehovsky deleted the deadtypeofbranches branch June 19, 2024 14:21
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 19, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 24, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 1, 2024
This fixes the problem discussed at #102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jul 1, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 18, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in #102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 20, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

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

Eliminate dead branches around typeof comparisons - #102248

Merged
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches
Jun 19, 2024
Merged

Eliminate dead branches around typeof comparisons#102248
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

RyuJIT will already do dead branch elimination for typeof(X) == typeof(Y) patterns, but we couldn't do elimination around foo == typeof(X). This fixes that using whole program knowledge - if we never saw a constructed MT for X, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.

I'm doing this in SubstitutedILProvider instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. SubstitutedILProvider is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).

Cc @dotnet/ilc-contrib

RyuJIT will already do dead branch elimination for `typeof(X) == typeof(Y)` patterns, but we couldn't do elimination around `foo == typeof(X)`. This fixes that using whole program knowledge - if we never saw a constructed `MT` for `X`, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.
I'm doing this in `SubstitutedILProvider` instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. `SubstitutedILProvider` is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@github-actionsgithub-actionsBot mentioned this pull request May 15, 2024
@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

if (reader.ReadILOpcode() is not ILOpcode.callvirt and not ILOpcode.call)
return false;

// We don't actually mind if this is not Object.GetType

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If it is an arbitrary call, can it return a type that happens to be equal to the other type?

Or is the idea that this case will fail the CanReferenceConstructedTypeOrCanonicalFormOfType check below? Ie the other argument can be anything. We are just skipping the specific common patterns here to keep things simple.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should be okay with any value loaded from a local or parameter. So also any value a method call could return.

We just don't have facilities to accept any value, so only a couple recognized patterns are allowed. Allowing any instance method call is less work than also checking if it's object.GetType.

if (knownType.IsCanonicalDefinitionType(CanonicalFormKind.Any))
return false;

if (_devirtualizationManager.CanReferenceConstructedTypeOrCanonicalFormOfType(knownType))

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.

Do we need to call convert ConvertToCanonForm before calling CanReferenceConstructedTypeOrCanonicalFormOfType? Or is the type guaranteed to be normalized somehow?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should convert to canon. Good catch.

@jkotas

Copy link
Copy Markdown
Member

I have run this under debugger on this simple test:

using System;
static class Program
{
static void Main(string[] args)
{
if (typeof(MyType) == args.GetType())
Console.WriteLine(42);
}
}
static class MyType
{
}

I would expect the substitution to trigger for it, but it is not happening. It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

Yes, you need to flip them to the more common pattern.

The problem is in the IL scanner. IL scanner only does the "downgrade result of typeof to necessary MethodTable" for a limited set of IL patterns as well and this one is not it. So we end up with "constructed MethodTable is needed" in the scanning phase, and this can no longer get optimized away.

// We expect pattern:
//
// ldtoken Foo
// call GetTypeFromHandle
// ldtoken Bar
// call GetTypeFromHandle
// call Equals
//
// We check for both ldtoken cases
if((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+6);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}
elseif((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.ldtoken
&&_basicBlocks[_currentOffset+10]==null
&&(ILOpcode)_ilBytes[_currentOffset+10]==ILOpcode.call
&&methodToken==ReadILTokenAt(_currentOffset+11)
&&_basicBlocks[_currentOffset+15]==null
&&(ILOpcode)_ilBytes[_currentOffset+15]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+16);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}

We really need some better facilities to analyze IL in C#, but also I don't know if I want us to build a "proper" IL importer in C#.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

(I plan to look into at least sharing this code between scanner and substitutions in some way.)

@jkotas

Copy link
Copy Markdown
Member

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

It's the tests that are part of this PR. We also have hits in corelib, for example:

if(attributeType==typeof(DecimalConstantAttribute))
{
returnGetRawDecimalConstant(attributeData);
}
elseif(attributeType.IsSubclassOf(typeof(CustomConstantAttribute)))
{
if(attributeType==typeof(DateTimeConstantAttribute))
{
returnGetRawDateTimeConstant(attributeData);
}
returnGetRawConstant(attributeData);
}

(The above will also be a real saving once we can do this optimization in the scanner - this is the only places that boxes DateTime and Decimal and that's a 100 kB saving on an app that uses reflection. It doesn't kick in right now, because the scanner will see we box DateTime/decimal and that destroys our opportunity to get rid of it because DateTime/decimal is referenced in typeof comparisons in other spots.)

@jkotas

Copy link
Copy Markdown
Member

It's the tests that are part of this PR.

I have extracted the test into a small program:

using System;
using System.Runtime.CompilerServices;
static class Program
{
static void Main(string[] args)
{
Type someType = GetTheType();
if (someType == typeof(Never3))
{
Console.WriteLine(42);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static Type GetTheType() => null;
}
class Never3
{
}

I have compiled the test in release mode (the test is under #if !DEBUG). Roslyn optimized out the someType local variable and the IL looks like this:

 IL_0000: call class [System.Runtime]System.Type Program::GetTheType()
IL_0005: ldtoken MyType
IL_000a: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
IL_000f: call bool [System.Runtime]System.Type::op_Equality(class [System.Runtime]System.Type,
class [System.Runtime]System.Type)

It fails the pattern match in TryExpandTypeEquality_TokenOther very early since the ldloc that the pattern match is looking for is gone. What am I missing?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

The tests are all failing in #102374 so the optimization here works. I agree that for the local case this is pretty fragile. This is another case where the expectation is that this will mostly come from a parameter in real world code. Loading it from a local was just equally cheap in the pattern match so I just allowed it. But parameter is the main use case.

@jkotas

jkotas commented May 17, 2024

Copy link
Copy Markdown
Member

I have figured out one of the mysteries:

The dotnet/runtime build sets DebugSymbols property to true globally. DebugSymbols does not actually do what its name suggests. The (portable) symbols are generated regardless of whether this property is true or false. What this property actually does is that it disables C# peephole IL optimizations. The C# peephole IL optimizations break the IL patterns used by the tests added in this PR. Setting the DebugSymbols to false makes the tests fail as demonstrated by #102391 . It would be nice to fix the pattern match and/or the test to work with DebugSymbols set to false.

The ordinary user projects out there do not set DebugSymbols property. I have done my quick ad-hoc test using an ordinary project and it is why it did not work for me. I will look into deleting the DebugSymbols setting so that we build and test our bits using the same settings as our users.

@jkotas

jkotas commented May 18, 2024

Copy link
Copy Markdown
Member

Yes, you need to flip them to the more common pattern.

Ok, this was the other part of the mystery. if (t == typeof(Never)) works as expected, if (typeof(Never) == t) does not work as expected. The code added in this PR handles it, but the pre-existing ldtoken handling in the scanner does not as you have pointed out.

{
Debug.Assert(type.NormalizeInstantiation() == type);
Debug.Assert(ConstructedEETypeNode.CreationAllowed(type));
return _constructedMethodTables.Contains(type);

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.

Should we also assert that we are only adding normalizations into _constructedMethodTables when it is populated?

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

Thanks

@MichalStrehovsky
MichalStrehovsky merged commit e0bd776 into dotnet:mainJun 19, 2024
@MichalStrehovsky
MichalStrehovsky deleted the deadtypeofbranches branch June 19, 2024 14:21
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 19, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 24, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 1, 2024
This fixes the problem discussed at #102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jul 1, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 18, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in #102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 20, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@MichalStrehovsky@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Eliminate dead branches around typeof comparisons by MichalStrehovsky · Pull Request #102248 · dotnet/runtime · GitHub
Skip to content

Eliminate dead branches around typeof comparisons - #102248

Merged
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches
Jun 19, 2024
Merged

Eliminate dead branches around typeof comparisons#102248
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

RyuJIT will already do dead branch elimination for typeof(X) == typeof(Y) patterns, but we couldn't do elimination around foo == typeof(X). This fixes that using whole program knowledge - if we never saw a constructed MT for X, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.

I'm doing this in SubstitutedILProvider instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. SubstitutedILProvider is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).

Cc @dotnet/ilc-contrib

RyuJIT will already do dead branch elimination for `typeof(X) == typeof(Y)` patterns, but we couldn't do elimination around `foo == typeof(X)`. This fixes that using whole program knowledge - if we never saw a constructed `MT` for `X`, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.
I'm doing this in `SubstitutedILProvider` instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. `SubstitutedILProvider` is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@github-actionsgithub-actionsBot mentioned this pull request May 15, 2024
@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

if (reader.ReadILOpcode() is not ILOpcode.callvirt and not ILOpcode.call)
return false;

// We don't actually mind if this is not Object.GetType

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If it is an arbitrary call, can it return a type that happens to be equal to the other type?

Or is the idea that this case will fail the CanReferenceConstructedTypeOrCanonicalFormOfType check below? Ie the other argument can be anything. We are just skipping the specific common patterns here to keep things simple.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should be okay with any value loaded from a local or parameter. So also any value a method call could return.

We just don't have facilities to accept any value, so only a couple recognized patterns are allowed. Allowing any instance method call is less work than also checking if it's object.GetType.

if (knownType.IsCanonicalDefinitionType(CanonicalFormKind.Any))
return false;

if (_devirtualizationManager.CanReferenceConstructedTypeOrCanonicalFormOfType(knownType))

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.

Do we need to call convert ConvertToCanonForm before calling CanReferenceConstructedTypeOrCanonicalFormOfType? Or is the type guaranteed to be normalized somehow?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should convert to canon. Good catch.

@jkotas

Copy link
Copy Markdown
Member

I have run this under debugger on this simple test:

using System;
static class Program
{
static void Main(string[] args)
{
if (typeof(MyType) == args.GetType())
Console.WriteLine(42);
}
}
static class MyType
{
}

I would expect the substitution to trigger for it, but it is not happening. It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

Yes, you need to flip them to the more common pattern.

The problem is in the IL scanner. IL scanner only does the "downgrade result of typeof to necessary MethodTable" for a limited set of IL patterns as well and this one is not it. So we end up with "constructed MethodTable is needed" in the scanning phase, and this can no longer get optimized away.

// We expect pattern:
//
// ldtoken Foo
// call GetTypeFromHandle
// ldtoken Bar
// call GetTypeFromHandle
// call Equals
//
// We check for both ldtoken cases
if((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+6);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}
elseif((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.ldtoken
&&_basicBlocks[_currentOffset+10]==null
&&(ILOpcode)_ilBytes[_currentOffset+10]==ILOpcode.call
&&methodToken==ReadILTokenAt(_currentOffset+11)
&&_basicBlocks[_currentOffset+15]==null
&&(ILOpcode)_ilBytes[_currentOffset+15]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+16);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}

We really need some better facilities to analyze IL in C#, but also I don't know if I want us to build a "proper" IL importer in C#.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

(I plan to look into at least sharing this code between scanner and substitutions in some way.)

@jkotas

Copy link
Copy Markdown
Member

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

It's the tests that are part of this PR. We also have hits in corelib, for example:

if(attributeType==typeof(DecimalConstantAttribute))
{
returnGetRawDecimalConstant(attributeData);
}
elseif(attributeType.IsSubclassOf(typeof(CustomConstantAttribute)))
{
if(attributeType==typeof(DateTimeConstantAttribute))
{
returnGetRawDateTimeConstant(attributeData);
}
returnGetRawConstant(attributeData);
}

(The above will also be a real saving once we can do this optimization in the scanner - this is the only places that boxes DateTime and Decimal and that's a 100 kB saving on an app that uses reflection. It doesn't kick in right now, because the scanner will see we box DateTime/decimal and that destroys our opportunity to get rid of it because DateTime/decimal is referenced in typeof comparisons in other spots.)

@jkotas

Copy link
Copy Markdown
Member

It's the tests that are part of this PR.

I have extracted the test into a small program:

using System;
using System.Runtime.CompilerServices;
static class Program
{
static void Main(string[] args)
{
Type someType = GetTheType();
if (someType == typeof(Never3))
{
Console.WriteLine(42);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static Type GetTheType() => null;
}
class Never3
{
}

I have compiled the test in release mode (the test is under #if !DEBUG). Roslyn optimized out the someType local variable and the IL looks like this:

 IL_0000: call class [System.Runtime]System.Type Program::GetTheType()
IL_0005: ldtoken MyType
IL_000a: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
IL_000f: call bool [System.Runtime]System.Type::op_Equality(class [System.Runtime]System.Type,
class [System.Runtime]System.Type)

It fails the pattern match in TryExpandTypeEquality_TokenOther very early since the ldloc that the pattern match is looking for is gone. What am I missing?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

The tests are all failing in #102374 so the optimization here works. I agree that for the local case this is pretty fragile. This is another case where the expectation is that this will mostly come from a parameter in real world code. Loading it from a local was just equally cheap in the pattern match so I just allowed it. But parameter is the main use case.

@jkotas

jkotas commented May 17, 2024

Copy link
Copy Markdown
Member

I have figured out one of the mysteries:

The dotnet/runtime build sets DebugSymbols property to true globally. DebugSymbols does not actually do what its name suggests. The (portable) symbols are generated regardless of whether this property is true or false. What this property actually does is that it disables C# peephole IL optimizations. The C# peephole IL optimizations break the IL patterns used by the tests added in this PR. Setting the DebugSymbols to false makes the tests fail as demonstrated by #102391 . It would be nice to fix the pattern match and/or the test to work with DebugSymbols set to false.

The ordinary user projects out there do not set DebugSymbols property. I have done my quick ad-hoc test using an ordinary project and it is why it did not work for me. I will look into deleting the DebugSymbols setting so that we build and test our bits using the same settings as our users.

@jkotas

jkotas commented May 18, 2024

Copy link
Copy Markdown
Member

Yes, you need to flip them to the more common pattern.

Ok, this was the other part of the mystery. if (t == typeof(Never)) works as expected, if (typeof(Never) == t) does not work as expected. The code added in this PR handles it, but the pre-existing ldtoken handling in the scanner does not as you have pointed out.

{
Debug.Assert(type.NormalizeInstantiation() == type);
Debug.Assert(ConstructedEETypeNode.CreationAllowed(type));
return _constructedMethodTables.Contains(type);

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.

Should we also assert that we are only adding normalizations into _constructedMethodTables when it is populated?

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

Thanks

@MichalStrehovsky
MichalStrehovsky merged commit e0bd776 into dotnet:mainJun 19, 2024
@MichalStrehovsky
MichalStrehovsky deleted the deadtypeofbranches branch June 19, 2024 14:21
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 19, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 24, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 1, 2024
This fixes the problem discussed at #102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jul 1, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 18, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in #102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 20, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

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

Eliminate dead branches around typeof comparisons - #102248

Merged
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches
Jun 19, 2024
Merged

Eliminate dead branches around typeof comparisons#102248
MichalStrehovsky merged 4 commits into
dotnet:mainfrom
MichalStrehovsky:deadtypeofbranches

Conversation

@MichalStrehovsky

Copy link
Copy Markdown
Member

RyuJIT will already do dead branch elimination for typeof(X) == typeof(Y) patterns, but we couldn't do elimination around foo == typeof(X). This fixes that using whole program knowledge - if we never saw a constructed MT for X, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.

I'm doing this in SubstitutedILProvider instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. SubstitutedILProvider is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).

Cc @dotnet/ilc-contrib

RyuJIT will already do dead branch elimination for `typeof(X) == typeof(Y)` patterns, but we couldn't do elimination around `foo == typeof(X)`. This fixes that using whole program knowledge - if we never saw a constructed `MT` for `X`, the comparison is not going to be true. Because it needs whole program, we still scan this dead branch so in the end this doesn't save much. We can eventually do better.
I'm doing this in `SubstitutedILProvider` instead of in RyuJIT: this is because we currently only reap a small benefit from this optimization due to it only happening during compilation phase. We need to do this during scanning as well. I think I can extend it to scannig. But the extension will require the optimization to 100% guaranteed happen during codegen. We cannot rely on whether RyuJIT will feel like it. `SubstitutedILProvider` is our way to ensure the optimization will happen no matter what - the IL from the branch will be gone and RyuJIT can at most remove the comparison (we don't mind much if it's left).
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@github-actionsgithub-actionsBot mentioned this pull request May 15, 2024
@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

if (reader.ReadILOpcode() is not ILOpcode.callvirt and not ILOpcode.call)
return false;

// We don't actually mind if this is not Object.GetType

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If it is an arbitrary call, can it return a type that happens to be equal to the other type?

Or is the idea that this case will fail the CanReferenceConstructedTypeOrCanonicalFormOfType check below? Ie the other argument can be anything. We are just skipping the specific common patterns here to keep things simple.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should be okay with any value loaded from a local or parameter. So also any value a method call could return.

We just don't have facilities to accept any value, so only a couple recognized patterns are allowed. Allowing any instance method call is less work than also checking if it's object.GetType.

if (knownType.IsCanonicalDefinitionType(CanonicalFormKind.Any))
return false;

if (_devirtualizationManager.CanReferenceConstructedTypeOrCanonicalFormOfType(knownType))

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.

Do we need to call convert ConvertToCanonForm before calling CanReferenceConstructedTypeOrCanonicalFormOfType? Or is the type guaranteed to be normalized somehow?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we should convert to canon. Good catch.

@jkotas

Copy link
Copy Markdown
Member

I have run this under debugger on this simple test:

using System;
static class Program
{
static void Main(string[] args)
{
if (typeof(MyType) == args.GetType())
Console.WriteLine(42);
}
}
static class MyType
{
}

I would expect the substitution to trigger for it, but it is not happening. It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

It never hits breakpoint at this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 . Is that expected?

Yes, you need to flip them to the more common pattern.

The problem is in the IL scanner. IL scanner only does the "downgrade result of typeof to necessary MethodTable" for a limited set of IL patterns as well and this one is not it. So we end up with "constructed MethodTable is needed" in the scanning phase, and this can no longer get optimized away.

// We expect pattern:
//
// ldtoken Foo
// call GetTypeFromHandle
// ldtoken Bar
// call GetTypeFromHandle
// call Equals
//
// We check for both ldtoken cases
if((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+6);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}
elseif((ILOpcode)_ilBytes[_currentOffset+5]==ILOpcode.ldtoken
&&_basicBlocks[_currentOffset+10]==null
&&(ILOpcode)_ilBytes[_currentOffset+10]==ILOpcode.call
&&methodToken==ReadILTokenAt(_currentOffset+11)
&&_basicBlocks[_currentOffset+15]==null
&&(ILOpcode)_ilBytes[_currentOffset+15]==ILOpcode.call)
{
methodToken=ReadILTokenAt(_currentOffset+16);
method=(MethodDesc)_methodIL.GetObject(methodToken);
isTypeEquals=IsTypeEquals(method);
}

We really need some better facilities to analyze IL in C#, but also I don't know if I want us to build a "proper" IL importer in C#.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

(I plan to look into at least sharing this code between scanner and substitutions in some way.)

@jkotas

Copy link
Copy Markdown
Member

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Could you please share a program that hits this line https://github.com/dotnet/runtime/pull/102248/files#diff-7c5a8ad684ce4e7f583b3bc392219bb15fc17e8400b172a2ae32d5301b7cdd0bR1027 in the compiler? Or is this WIP and this path is not reachable yet?

It's the tests that are part of this PR. We also have hits in corelib, for example:

if(attributeType==typeof(DecimalConstantAttribute))
{
returnGetRawDecimalConstant(attributeData);
}
elseif(attributeType.IsSubclassOf(typeof(CustomConstantAttribute)))
{
if(attributeType==typeof(DateTimeConstantAttribute))
{
returnGetRawDateTimeConstant(attributeData);
}
returnGetRawConstant(attributeData);
}

(The above will also be a real saving once we can do this optimization in the scanner - this is the only places that boxes DateTime and Decimal and that's a 100 kB saving on an app that uses reflection. It doesn't kick in right now, because the scanner will see we box DateTime/decimal and that destroys our opportunity to get rid of it because DateTime/decimal is referenced in typeof comparisons in other spots.)

@jkotas

Copy link
Copy Markdown
Member

It's the tests that are part of this PR.

I have extracted the test into a small program:

using System;
using System.Runtime.CompilerServices;
static class Program
{
static void Main(string[] args)
{
Type someType = GetTheType();
if (someType == typeof(Never3))
{
Console.WriteLine(42);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static Type GetTheType() => null;
}
class Never3
{
}

I have compiled the test in release mode (the test is under #if !DEBUG). Roslyn optimized out the someType local variable and the IL looks like this:

 IL_0000: call class [System.Runtime]System.Type Program::GetTheType()
IL_0005: ldtoken MyType
IL_000a: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
IL_000f: call bool [System.Runtime]System.Type::op_Equality(class [System.Runtime]System.Type,
class [System.Runtime]System.Type)

It fails the pattern match in TryExpandTypeEquality_TokenOther very early since the ldloc that the pattern match is looking for is gone. What am I missing?

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

@MichalStrehovsky

Copy link
Copy Markdown
MemberAuthor

Weird, I don't know how the test would pass without it. I've submitted #102374 with just the test because I don't want to switch branches locally right now.

The tests are all failing in #102374 so the optimization here works. I agree that for the local case this is pretty fragile. This is another case where the expectation is that this will mostly come from a parameter in real world code. Loading it from a local was just equally cheap in the pattern match so I just allowed it. But parameter is the main use case.

@jkotas

jkotas commented May 17, 2024

Copy link
Copy Markdown
Member

I have figured out one of the mysteries:

The dotnet/runtime build sets DebugSymbols property to true globally. DebugSymbols does not actually do what its name suggests. The (portable) symbols are generated regardless of whether this property is true or false. What this property actually does is that it disables C# peephole IL optimizations. The C# peephole IL optimizations break the IL patterns used by the tests added in this PR. Setting the DebugSymbols to false makes the tests fail as demonstrated by #102391 . It would be nice to fix the pattern match and/or the test to work with DebugSymbols set to false.

The ordinary user projects out there do not set DebugSymbols property. I have done my quick ad-hoc test using an ordinary project and it is why it did not work for me. I will look into deleting the DebugSymbols setting so that we build and test our bits using the same settings as our users.

@jkotas

jkotas commented May 18, 2024

Copy link
Copy Markdown
Member

Yes, you need to flip them to the more common pattern.

Ok, this was the other part of the mystery. if (t == typeof(Never)) works as expected, if (typeof(Never) == t) does not work as expected. The code added in this PR handles it, but the pre-existing ldtoken handling in the scanner does not as you have pointed out.

{
Debug.Assert(type.NormalizeInstantiation() == type);
Debug.Assert(ConstructedEETypeNode.CreationAllowed(type));
return _constructedMethodTables.Contains(type);

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.

Should we also assert that we are only adding normalizations into _constructedMethodTables when it is populated?

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

Thanks

@MichalStrehovsky
MichalStrehovsky merged commit e0bd776 into dotnet:mainJun 19, 2024
@MichalStrehovsky
MichalStrehovsky deleted the deadtypeofbranches branch June 19, 2024 14:21
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 19, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 24, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
This fixes the problem discussed at dotnet#102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jun 28, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 1, 2024
This fixes the problem discussed at #102248 (comment). Now we call into the same code from both substitutions and scanner.
MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this pull request Jul 1, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in dotnet#102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
MichalStrehovsky added a commit that referenced this pull request Jul 18, 2024
Before this PR, we were somewhat able to eliminate dead typeof checks such as:
```csharp
if (someType == typeof(Foo)
{
ExpensiveMethod();
}
```
This work was done in #102248.
However, the optimization only happened during codegen. This meant that when building the whole program view, we'd still look at `ExpensiveMethod` and whatever damage this caused to the whole program view was permanent.
With this PR, the scanner now becomes aware of the optimization we do during codegen and tries to defer injecting dependencies until we will need them.
With this change, we detect the conditional branch, and generate whatever dependencies from the basic block as conditional. That way scanning can fully skip scanning `ExpensiveMethod` and the subsequent optimization will ensure the missed scanning will not cause issues at codegen time.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 20, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@MichalStrehovsky@jkotas