-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
Prevent compiler stack overflow for deeply recursive code #55617
Conversation
(rust_highfive has picked a reviewer for you, use r? to override) |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
src/librustc/Cargo.toml
Outdated
@@ -34,6 +34,7 @@ byteorder = { version = "1.1", features = ["i128"]} | |||
chalk-engine = { version = "0.8.0", default-features=false } | |||
rustc_fs_util = { path = "../librustc_fs_util" } | |||
smallvec = { version = "0.6.5", features = ["union"] } | |||
stacker = "0.1.3" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure stacker
is necessarily rustc-ready right now, but that doesn't mean that it can't be! Some issues I can think of are:
- It only has support for x86 platforms basically, and only Windows/Mac/Linux. It should be easy enough to "add support" for other platforms by basically doing nothing. Full support could be added over time as necessary
- I don't think
stacker
does anything with guard pages, but ideally it'd also be sure to allocate guard pages for larger segments to protect agains accidental stack overflow - I'm not entirely sure how well panics and such work? It should be relatively easy to
catch_unwind
andresume_unwind
though when necessary (just needs to be done)
These are all pretty minor, but I'd want to be sure to handle them before merging if possible!
src/librustc_driver/lib.rs
Outdated
@@ -1460,6 +1460,8 @@ fn parse_crate_attrs<'a>(sess: &'a Session, input: &Input) -> PResult<'a, Vec<as | |||
} | |||
} | |||
|
|||
const STACK_SIZE: usize = 4 * 1024 * 1024; // 4MB |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe make this dependent on whether stacker
actually works?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How's that relevant? When stacker doesn't work, these values don't matter, because they don't do anything
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought this was the default thread stack size, unrelated to stacker. My bad if that's not the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm gonna rename the constant to be clearer about this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh you're right, I was looking at the wrong value. But this change is just for crater as noted by @nagisa in #55617 (comment)
We'll bump it back after crater succeeds
Let's do a perf run. |
🔒 Merge conflict This pull request and the master branch diverged in a way that cannot be automatically merged. Please rebase on top of the latest master branch, and let the reviewer approve again. How do I rebase?Assuming
You may also read Git Rebasing to Resolve Conflicts by Drew Blessing for a short tutorial. Please avoid the "Resolve conflicts" button on GitHub. It uses Sometimes step 4 will complete without asking for resolution. This is usually due to difference between how Error message
|
@bors try |
Prevent compiler stack overflow for deeply recursive code I was unable to write a test that 1. runs in under 1s 2. overflows on my machine without this patch The following reproduces the issue, but I don't think it's sensible to include a test that takes 30s to compile. We can now easily squash newly appearing overflows by the strategic insertion of calls to `ensure_sufficient_stack`. ```rust // compile-pass #![recursion_limit="1000000"] macro_rules! chain { (EE $e:expr) => {$e.sin()}; (RECURSE $i:ident $e:expr) => {chain!($i chain!($i chain!($i chain!($i $e))))}; (Z $e:expr) => {chain!(RECURSE EE $e)}; (Y $e:expr) => {chain!(RECURSE Z $e)}; (X $e:expr) => {chain!(RECURSE Y $e)}; (A $e:expr) => {chain!(RECURSE X $e)}; (B $e:expr) => {chain!(RECURSE A $e)}; (C $e:expr) => {chain!(RECURSE B $e)}; // causes overflow on x86_64 linux // less than 1 second until overflow on test machine // after overflow has been fixed, takes 30s to compile :/ (D $e:expr) => {chain!(RECURSE C $e)}; (E $e:expr) => {chain!(RECURSE D $e)}; (F $e:expr) => {chain!(RECURSE E $e)}; // more than 10 seconds (G $e:expr) => {chain!(RECURSE F $e)}; (H $e:expr) => {chain!(RECURSE G $e)}; (I $e:expr) => {chain!(RECURSE H $e)}; (J $e:expr) => {chain!(RECURSE I $e)}; (K $e:expr) => {chain!(RECURSE J $e)}; (L $e:expr) => {chain!(RECURSE L $e)}; } fn main() { let x = chain!(D 42.0_f32); } ``` fixes #55471 fixes #41884 fixes #40161 fixes #34844 fixes #32594 cc @alexcrichton @rust-lang/compiler I looked at all code that checks the recursion limit and inserted stack growth calls where appropriate.
☀️ Test successful - status-travis |
@rust-timer build 2b10b3d |
Success: Queued 2b10b3d with parent f90aab7, comparison URL. |
Finished benchmarking try commit 2b10b3d |
Improvements for ctfe stress tests (spurious?), regressions up to 3% for everything else except the |
Makes sense that the |
Well, this is an operation we now run on every single query (not every call, just every evaluation). There are loads of queries. It seems logical that this introduces some regression that we can't get rid of. |
But
|
I feel like the actual stack check shouldn't be noticeable compared to hashing and looking up a key in a hashmap. Maybe we're growing the stack more often than we need to? |
Looking at the stacker implementation, a possible issue might be that we're sitting somewhere close to the stack limit and regularly go over it and below it again. This will allocate and deallocate a new stack every time. Maybe retaining the last allocation would help to reduce the performance impact? |
My initial proposal separately mentioned that we should only grow the stack
and never bother redicing its size. I made that call with performance in
mind. I didn't realise stacker was deallocating stack fragments, though now
that I think about it, *that* is the obvious implementation for the stacker
approach.
…On Wed, Nov 7, 2018, 19:15 Nikita Popov ***@***.*** wrote:
Looking at the stacker implementation, a possible issue might be that
we're sitting somewhere close to the stack limit and regularly go over it
and below it again. This will allocate and deallocate a new stack every
time. Maybe retaining the last allocation would help to reduce the
performance impact?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#55617 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AApc0ssfA1jZIb2KMI_ZkxHe12s2Eb0yks5usxU-gaJpZM4YL3aW>
.
|
@bors retry (yield) |
⌛ Testing commit 935a05f with merge 1e4ad8ae1ae7c74ced296fd2e6380a81a5f6357c... |
@bors retry yield |
⌛ Testing commit 935a05f with merge 698a0e16dbbb8fa45c5f0dd8c298ad8e98d14f80... |
@bors retry yield |
⌛ Testing commit 935a05f with merge 18e1bdf136f4f0a269ac64e89c73020ad8b882a6... |
@bors retry yield |
☀️ Test successful - checks-actions, checks-azure |
cc @XAMPPRocky I tagged this with relnotes-perf but it's not really perf so much as "hey this fixes a longstanding problem, would be cool to mention" |
rustdoc's `main()` immediately spawns a thread, M, with a large stack (16MiB or 32MiB) on which it runs `main_args()`. `main_args()` does a small amount of options processing and then calls `setup_callbacks_and_run_in_default_thread_pool_with_globals()`, which spawns it own thread, and M is not used further. So, thread M seems unnecessary. However, it does serve a purpose: if the options processing in `main_args()` panics, that panic is caught when M is joined. So M can't simply be removed. However, `main_options()`, which is called by `main_args()`, has a `catch_fatal_errors()` call within it. We can move that call to `main()` and change it to the very similar `catch_with_exit_code()`. With that in place, M can be removed, and panics from options processing will still be caught appropriately. Even better, this makes rustdoc's `main()` match rustc's `main()`, which also uses `catch_with_exit_code()`. (Also note that the use of a 16MiB/32MiB stack was eliminated from rustc in rust-lang#55617.)
I was unable to write a test that
The following reproduces the issue, but I don't think it's sensible to include a test that takes 30s to compile. We can now easily squash newly appearing overflows by the strategic insertion of calls to
ensure_sufficient_stack
.fixes #55471
fixes #41884
fixes #40161
fixes #34844
fixes #32594
cc @alexcrichton @rust-lang/compiler
I looked at all code that checks the recursion limit and inserted stack growth calls where appropriate.