Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[mono][interp] Defer compilation in bblocks with unitialized stack #108731

Merged
merged 5 commits into from
Nov 27, 2024

Conversation

BrzVlad
Copy link
Member

@BrzVlad BrzVlad commented Oct 9, 2024

Each basic block will have an emit state, not emitted, emitting or emitted. When we reach a new basic block, we will emit code into it only if the stack state is initialized (the stack state of a bblock can be initialized either from the state of the previous bblocks, if it is fallthrough, or from branching from another bblock with initialized state). If we encounter a bblock that doesn't have the state initialized we set a flag so we will retry codegen in an attempt to emit new bblocks.

Once we finish emitting code, we remove all bblocks in not emitted state.

Before this change, when encountering a bblock with unitialized stack, we assumed by chance that it had an empty stack, which is incorrect according to the spec. Also, in some cases we could simply crash, even if the block was indeed having an empty stack.

Copy link
Contributor

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

Each basic block will have an emit state, not emitted, emitting or emitted. When we reach a new basic block, we will emit code into it only if the stack state is initialized (the stack state of a bblock can be initialized either from the state of the previous bblocks, if it is fallthrough, or from branching from another bblock with initialized state). If we encounter a bblock that doesn't have the state initialized we set a flag so we will retry codegen in an attempt to emit new bblocks.

Once we finish emitting code, we remove all bblocks in not emitted state.
…ed ranges

Following the change to only emit code in bblocks once we reach them with an initialized stack state, we have the side effect of not processing IL code in dead bblocks. This means that offset_to_bb might actually be null for some IL offsets, so we need to iterate over following il offsets until we find a mapped bblock.
@BrzVlad BrzVlad force-pushed the fix-interp-bb-stack branch from 9d8dbad to f601a5b Compare November 3, 2024 17:08
@BrzVlad BrzVlad changed the title [mono][interp] testing [mono][interp] Defer compilation in bblocks with unitialized stack Nov 3, 2024
@BrzVlad BrzVlad marked this pull request as ready for review November 4, 2024 08:59
@BrzVlad BrzVlad requested a review from kotlarmilos as a code owner November 4, 2024 08:59
g_assert (bb);
// If the bblock is detected as dead while traversing the IL code, the mapping for
// it is cleared. We can skip it.
if (!bb)
Copy link
Member

Choose a reason for hiding this comment

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

I’m concerned about relaxing the condition here. If bblock is null due to a bug, it might be incorrectly processed here. Could we explicitly annotate a bblock as dead instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

We do have a dead field for basic blocks, but the existing pattern is that dead bblocks are no longer linked to live bblocks, they are not reachable. In addition to being a different pattern, having them still exist in the td->offset_to_bb mapping turned out to complicate code in other places. This condition here is used only for exception clause ranges so I would say the scope is limited enough so we don't risk serious bugs.

@lewing
Copy link
Member

lewing commented Dec 4, 2024

@BrzVlad looks like a big regression on wasm dotnet/perf-autofiling-issues#45939

BrzVlad added a commit to BrzVlad/runtime that referenced this pull request Dec 6, 2024
Many methods in the BCL, especially hwintrins related, contain a lot of code that is detected as dead during compilation. On mono, inlining happens during IL import and a lot of optimizations are run as later passes. This exposed the issue where we have a lot of dead code bloat from inlining, with optimizations running on it.

A simple solution for this problem was tracking jump counts for each bblock (dotnet#97514), which are initialized when bblocks are first created, before IL import stage. Then a small set of IL import level optimizations were added, in order to reduce the jump targets of each bblock. As we were further importing IL, if we reached a bblock with 0 jump targets, we would disable inlining into it, in order to reduce code bloat. Disabling code emit altogether was too challenging. Another limitation of this approach was that we would fail to detect dead code if it was part of a loop. The results were good however, by reducing mem usage in `System.Numerics.Tensor.Tests` from 6GB to 600MB.

For an unrelated issue, the order in which we generate bblocks was redesigned in order to account for bblock stack state initialization in weird control flow scenarios (dotnet#108731). This was achieved by deferring IL import into bblock that was not yet reached from other live bblocks. A side effect of this is that we no longer generate code at all in unreachable bblocks, completely superseding the previous approach while addressing both the problems of inlining into loops or generating IR for dead IL. In the previously mentioned test suite, this further reduces the memory usage to 300MB.

Remnants of the unnecessary `no_inlining` approach still lingered in the code, leading to disabling of inline optimization in some reachable code. This triggered a significant performance regression.
BrzVlad added a commit to BrzVlad/runtime that referenced this pull request Dec 6, 2024
Many methods in the BCL, especially hwintrins related, contain a lot of code that is detected as dead during compilation. On mono, inlining happens during IL import and a lot of optimizations are run as later passes. This exposed the issue where we have a lot of dead code bloat from inlining, with optimizations later running on it.

A simple solution for this problem was tracking jump counts for each bblock (dotnet#97514), which are initialized when bblocks are first created, before IL import stage. Then a small set of IL import level optimizations were added, in order to reduce the jump targets of each bblock. As we were further importing IL, if we reached a bblock with 0 jump targets, we would disable inlining into it, in order to reduce code bloat. Disabling code emit altogether was too challenging. Another limitation of this approach was that we would fail to detect dead code if it was part of a loop. The results were good however, by reducing mem usage in `System.Numerics.Tensor.Tests` from 6GB to 600MB.

For an unrelated issue, the order in which we generate bblocks was redesigned in order to account for bblock stack state initialization in weird control flow scenarios (dotnet#108731). This was achieved by deferring IL import into bblocks that were not yet reached from other live bblocks. A side effect of this is that we no longer generate code at all in unreachable bblocks, completely superseding the previous approach while addressing both the problems of inlining into loops or generating IR for dead IL. In the previously mentioned test suite, this further reduced the memory usage to 300MB.

Remnants of the unnecessary `no_inlining` approach still lingered in the code, leading to disabling of inline optimization in some reachable code. This triggered a significant performance regression which this PR addresses.
mikelle-rogers pushed a commit to mikelle-rogers/runtime that referenced this pull request Dec 10, 2024
…otnet#108731)

* [mono][interp] Add bblock start verbose logging

* [mono][interp] Minor fixes around IL offsets with inlining

* [mono][interp] Defer compilation in bblocks with unitialized stack

Each basic block will have an emit state, not emitted, emitting or emitted. When we reach a new basic block, we will emit code into it only if the stack state is initialized (the stack state of a bblock can be initialized either from the state of the previous bblocks, if it is fallthrough, or from branching from another bblock with initialized state). If we encounter a bblock that doesn't have the state initialized we set a flag so we will retry codegen in an attempt to emit new bblocks.

Once we finish emitting code, we remove all bblocks in not emitted state.

* [mono][interp] Fix obtaining of native offsets when computing protected ranges

Following the change to only emit code in bblocks once we reach them with an initialized stack state, we have the side effect of not processing IL code in dead bblocks. This means that offset_to_bb might actually be null for some IL offsets, so we need to iterate over following il offsets until we find a mapped bblock.

---------

Co-authored-by: Larry Ewing <lewing@microsoft.com>
BrzVlad added a commit that referenced this pull request Dec 10, 2024
…0468)

Many methods in the BCL, especially hwintrins related, contain a lot of code that is detected as dead during compilation. On mono, inlining happens during IL import and a lot of optimizations are run as later passes. This exposed the issue where we have a lot of dead code bloat from inlining, with optimizations later running on it.

A simple solution for this problem was tracking jump counts for each bblock (#97514), which are initialized when bblocks are first created, before IL import stage. Then a small set of IL import level optimizations were added, in order to reduce the jump targets of each bblock. As we were further importing IL, if we reached a bblock with 0 jump targets, we would disable inlining into it, in order to reduce code bloat. Disabling code emit altogether was too challenging. Another limitation of this approach was that we would fail to detect dead code if it was part of a loop. The results were good however, by reducing mem usage in `System.Numerics.Tensor.Tests` from 6GB to 600MB.

For an unrelated issue, the order in which we generate bblocks was redesigned in order to account for bblock stack state initialization in weird control flow scenarios (#108731). This was achieved by deferring IL import into bblocks that were not yet reached from other live bblocks. A side effect of this is that we no longer generate code at all in unreachable bblocks, completely superseding the previous approach while addressing both the problems of inlining into loops or generating IR for dead IL. In the previously mentioned test suite, this further reduced the memory usage to 300MB.

Remnants of the unnecessary `no_inlining` approach still lingered in the code, leading to disabling of inline optimization in some reachable code. This triggered a significant performance regression which this PR addresses.
hez2010 pushed a commit to hez2010/runtime that referenced this pull request Dec 14, 2024
…net#110468)

Many methods in the BCL, especially hwintrins related, contain a lot of code that is detected as dead during compilation. On mono, inlining happens during IL import and a lot of optimizations are run as later passes. This exposed the issue where we have a lot of dead code bloat from inlining, with optimizations later running on it.

A simple solution for this problem was tracking jump counts for each bblock (dotnet#97514), which are initialized when bblocks are first created, before IL import stage. Then a small set of IL import level optimizations were added, in order to reduce the jump targets of each bblock. As we were further importing IL, if we reached a bblock with 0 jump targets, we would disable inlining into it, in order to reduce code bloat. Disabling code emit altogether was too challenging. Another limitation of this approach was that we would fail to detect dead code if it was part of a loop. The results were good however, by reducing mem usage in `System.Numerics.Tensor.Tests` from 6GB to 600MB.

For an unrelated issue, the order in which we generate bblocks was redesigned in order to account for bblock stack state initialization in weird control flow scenarios (dotnet#108731). This was achieved by deferring IL import into bblocks that were not yet reached from other live bblocks. A side effect of this is that we no longer generate code at all in unreachable bblocks, completely superseding the previous approach while addressing both the problems of inlining into loops or generating IR for dead IL. In the previously mentioned test suite, this further reduced the memory usage to 300MB.

Remnants of the unnecessary `no_inlining` approach still lingered in the code, leading to disabling of inline optimization in some reachable code. This triggered a significant performance regression which this PR addresses.
@github-actions github-actions bot locked and limited conversation to collaborators Jan 4, 2025
Sign up for free to 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.

3 participants