Skip to content

Commit ebafbfc

Browse files
committed
Auto merge of #10827 - y21:large-stack-frames, r=dswij
new lint: `large_stack_frames` This implements a lint that looks for functions that use a lot of stack space. It uses the MIR because conveniently every temporary gets its own local and I think it maps best to stack space used in a function. It's probably going to be quite inaccurate in release builds, but at least for debug builds where opts are less aggressive on LLVM's side I think this is accurate "enough". (This does not work for generic functions yet. Not sure if I should try to get it working in this PR or if it could land without it for now and be added in a followup PR.) I also put it under the nursery category because this probably needs more work... changelog: new lint: [`large_stack_frames`]
2 parents a3b185b + 6ad7c6f commit ebafbfc

File tree

9 files changed

+264
-0
lines changed

9 files changed

+264
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4883,6 +4883,7 @@ Released 2018-09-13
48834883
[`large_futures`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_futures
48844884
[`large_include_file`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_include_file
48854885
[`large_stack_arrays`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_stack_arrays
4886+
[`large_stack_frames`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_stack_frames
48864887
[`large_types_passed_by_value`]: https://rust-lang.github.io/rust-clippy/master/index.html#large_types_passed_by_value
48874888
[`len_without_is_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#len_without_is_empty
48884889
[`len_zero`]: https://rust-lang.github.io/rust-clippy/master/index.html#len_zero

book/src/lint_configuration.md

+10
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,16 @@ The maximum allowed size for arrays on the stack
338338
* [`large_const_arrays`](https://rust-lang.github.io/rust-clippy/master/index.html#large_const_arrays)
339339

340340

341+
## `stack-size-threshold`
342+
The maximum allowed stack size for functions in bytes
343+
344+
**Default Value:** `512000` (`u64`)
345+
346+
---
347+
**Affected lints:**
348+
* [`large_stack_frames`](https://rust-lang.github.io/rust-clippy/master/index.html#large_stack_frames)
349+
350+
341351
## `vec-box-size-threshold`
342352
The size of the boxed type in bytes, where boxing in a `Vec` is allowed
343353

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
228228
crate::large_futures::LARGE_FUTURES_INFO,
229229
crate::large_include_file::LARGE_INCLUDE_FILE_INFO,
230230
crate::large_stack_arrays::LARGE_STACK_ARRAYS_INFO,
231+
crate::large_stack_frames::LARGE_STACK_FRAMES_INFO,
231232
crate::len_zero::COMPARISON_TO_EMPTY_INFO,
232233
crate::len_zero::LEN_WITHOUT_IS_EMPTY_INFO,
233234
crate::len_zero::LEN_ZERO_INFO,
+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
use std::ops::AddAssign;
2+
3+
use clippy_utils::diagnostics::span_lint_and_note;
4+
use clippy_utils::fn_has_unsatisfiable_preds;
5+
use rustc_hir::def_id::LocalDefId;
6+
use rustc_hir::intravisit::FnKind;
7+
use rustc_hir::Body;
8+
use rustc_hir::FnDecl;
9+
use rustc_lint::{LateContext, LateLintPass};
10+
use rustc_session::declare_tool_lint;
11+
use rustc_session::impl_lint_pass;
12+
use rustc_span::Span;
13+
14+
declare_clippy_lint! {
15+
/// ### What it does
16+
/// Checks for functions that use a lot of stack space.
17+
///
18+
/// This often happens when constructing a large type, such as an array with a lot of elements,
19+
/// or constructing *many* smaller-but-still-large structs, or copying around a lot of large types.
20+
///
21+
/// This lint is a more general version of [`large_stack_arrays`](https://rust-lang.github.io/rust-clippy/master/#large_stack_arrays)
22+
/// that is intended to look at functions as a whole instead of only individual array expressions inside of a function.
23+
///
24+
/// ### Why is this bad?
25+
/// The stack region of memory is very limited in size (usually *much* smaller than the heap) and attempting to
26+
/// use too much will result in a stack overflow and crash the program.
27+
/// To avoid this, you should consider allocating large types on the heap instead (e.g. by boxing them).
28+
///
29+
/// Keep in mind that the code path to construction of large types does not even need to be reachable;
30+
/// it purely needs to *exist* inside of the function to contribute to the stack size.
31+
/// For example, this causes a stack overflow even though the branch is unreachable:
32+
/// ```rust,ignore
33+
/// fn main() {
34+
/// if false {
35+
/// let x = [0u8; 10000000]; // 10 MB stack array
36+
/// black_box(&x);
37+
/// }
38+
/// }
39+
/// ```
40+
///
41+
/// ### Known issues
42+
/// False positives. The stack size that clippy sees is an estimated value and can be vastly different
43+
/// from the actual stack usage after optimizations passes have run (especially true in release mode).
44+
/// Modern compilers are very smart and are able to optimize away a lot of unnecessary stack allocations.
45+
/// In debug mode however, it is usually more accurate.
46+
///
47+
/// This lint works by summing up the size of all variables that the user typed, variables that were
48+
/// implicitly introduced by the compiler for temporaries, function arguments and the return value,
49+
/// and comparing them against a (configurable, but high-by-default).
50+
///
51+
/// ### Example
52+
/// This function creates four 500 KB arrays on the stack. Quite big but just small enough to not trigger `large_stack_arrays`.
53+
/// However, looking at the function as a whole, it's clear that this uses a lot of stack space.
54+
/// ```rust
55+
/// struct QuiteLargeType([u8; 500_000]);
56+
/// fn foo() {
57+
/// // ... some function that uses a lot of stack space ...
58+
/// let _x1 = QuiteLargeType([0; 500_000]);
59+
/// let _x2 = QuiteLargeType([0; 500_000]);
60+
/// let _x3 = QuiteLargeType([0; 500_000]);
61+
/// let _x4 = QuiteLargeType([0; 500_000]);
62+
/// }
63+
/// ```
64+
///
65+
/// Instead of doing this, allocate the arrays on the heap.
66+
/// This currently requires going through a `Vec` first and then converting it to a `Box`:
67+
/// ```rust
68+
/// struct NotSoLargeType(Box<[u8]>);
69+
///
70+
/// fn foo() {
71+
/// let _x1 = NotSoLargeType(vec![0; 500_000].into_boxed_slice());
72+
/// // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Now heap allocated.
73+
/// // The size of `NotSoLargeType` is 16 bytes.
74+
/// // ...
75+
/// }
76+
/// ```
77+
#[clippy::version = "1.71.0"]
78+
pub LARGE_STACK_FRAMES,
79+
nursery,
80+
"checks for functions that allocate a lot of stack space"
81+
}
82+
83+
pub struct LargeStackFrames {
84+
maximum_allowed_size: u64,
85+
}
86+
87+
impl LargeStackFrames {
88+
#[must_use]
89+
pub fn new(size: u64) -> Self {
90+
Self {
91+
maximum_allowed_size: size,
92+
}
93+
}
94+
}
95+
96+
impl_lint_pass!(LargeStackFrames => [LARGE_STACK_FRAMES]);
97+
98+
#[derive(Copy, Clone)]
99+
enum Space {
100+
Used(u64),
101+
Overflow,
102+
}
103+
104+
impl Space {
105+
pub fn exceeds_limit(self, limit: u64) -> bool {
106+
match self {
107+
Self::Used(used) => used > limit,
108+
Self::Overflow => true,
109+
}
110+
}
111+
}
112+
113+
impl AddAssign<u64> for Space {
114+
fn add_assign(&mut self, rhs: u64) {
115+
if let Self::Used(lhs) = self {
116+
match lhs.checked_add(rhs) {
117+
Some(sum) => *self = Self::Used(sum),
118+
None => *self = Self::Overflow,
119+
}
120+
}
121+
}
122+
}
123+
124+
impl<'tcx> LateLintPass<'tcx> for LargeStackFrames {
125+
fn check_fn(
126+
&mut self,
127+
cx: &LateContext<'tcx>,
128+
_: FnKind<'tcx>,
129+
_: &'tcx FnDecl<'tcx>,
130+
_: &'tcx Body<'tcx>,
131+
span: Span,
132+
local_def_id: LocalDefId,
133+
) {
134+
let def_id = local_def_id.to_def_id();
135+
// Building MIR for `fn`s with unsatisfiable preds results in ICE.
136+
if fn_has_unsatisfiable_preds(cx, def_id) {
137+
return;
138+
}
139+
140+
let mir = cx.tcx.optimized_mir(def_id);
141+
let param_env = cx.tcx.param_env(def_id);
142+
143+
let mut frame_size = Space::Used(0);
144+
145+
for local in &mir.local_decls {
146+
if let Ok(layout) = cx.tcx.layout_of(param_env.and(local.ty)) {
147+
frame_size += layout.size.bytes();
148+
}
149+
}
150+
151+
if frame_size.exceeds_limit(self.maximum_allowed_size) {
152+
span_lint_and_note(
153+
cx,
154+
LARGE_STACK_FRAMES,
155+
span,
156+
"this function allocates a large amount of stack space",
157+
None,
158+
"allocating large amounts of stack space can overflow the stack",
159+
);
160+
}
161+
}
162+
}

clippy_lints/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ mod large_enum_variant;
168168
mod large_futures;
169169
mod large_include_file;
170170
mod large_stack_arrays;
171+
mod large_stack_frames;
171172
mod len_zero;
172173
mod let_if_seq;
173174
mod let_underscore;
@@ -1042,6 +1043,8 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
10421043
min_ident_chars_threshold,
10431044
})
10441045
});
1046+
let stack_size_threshold = conf.stack_size_threshold;
1047+
store.register_late_pass(move |_| Box::new(large_stack_frames::LargeStackFrames::new(stack_size_threshold)));
10451048
// add lints here, do not remove this comment, it's used in `new_lint`
10461049
}
10471050

clippy_lints/src/utils/conf.rs

+4
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,10 @@ define_Conf! {
387387
///
388388
/// The maximum allowed size for arrays on the stack
389389
(array_size_threshold: u64 = 512_000),
390+
/// Lint: LARGE_STACK_FRAMES.
391+
///
392+
/// The maximum allowed stack size for functions in bytes
393+
(stack_size_threshold: u64 = 512_000),
390394
/// Lint: VEC_BOX.
391395
///
392396
/// The size of the boxed type in bytes, where boxing in a `Vec` is allowed

tests/ui-toml/toml_unknown_key/conf_unknown_key.stderr

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ error: error reading Clippy's configuration file: unknown field `foobar`, expect
4444
semicolon-inside-block-ignore-singleline
4545
semicolon-outside-block-ignore-multiline
4646
single-char-binding-names-threshold
47+
stack-size-threshold
4748
standard-macro-braces
4849
suppress-restriction-lint-in-const
4950
third-party
@@ -109,6 +110,7 @@ error: error reading Clippy's configuration file: unknown field `barfoo`, expect
109110
semicolon-inside-block-ignore-singleline
110111
semicolon-outside-block-ignore-multiline
111112
single-char-binding-names-threshold
113+
stack-size-threshold
112114
standard-macro-braces
113115
suppress-restriction-lint-in-const
114116
third-party

tests/ui/large_stack_frames.rs

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#![allow(unused, incomplete_features)]
2+
#![warn(clippy::large_stack_frames)]
3+
#![feature(unsized_locals)]
4+
5+
use std::hint::black_box;
6+
7+
fn generic<T: Default>() {
8+
let x = T::default();
9+
black_box(&x);
10+
}
11+
12+
fn unsized_local() {
13+
let x: dyn std::fmt::Display = *(Box::new(1) as Box<dyn std::fmt::Display>);
14+
black_box(&x);
15+
}
16+
17+
struct ArrayDefault<const N: usize>([u8; N]);
18+
19+
impl<const N: usize> Default for ArrayDefault<N> {
20+
fn default() -> Self {
21+
Self([0; N])
22+
}
23+
}
24+
25+
fn many_small_arrays() {
26+
let x = [0u8; 500_000];
27+
let x2 = [0u8; 500_000];
28+
let x3 = [0u8; 500_000];
29+
let x4 = [0u8; 500_000];
30+
let x5 = [0u8; 500_000];
31+
black_box((&x, &x2, &x3, &x4, &x5));
32+
}
33+
34+
fn large_return_value() -> ArrayDefault<1_000_000> {
35+
Default::default()
36+
}
37+
38+
fn large_fn_arg(x: ArrayDefault<1_000_000>) {
39+
black_box(&x);
40+
}
41+
42+
fn main() {
43+
generic::<ArrayDefault<1_000_000>>();
44+
}

tests/ui/large_stack_frames.stderr

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
error: this function allocates a large amount of stack space
2+
--> $DIR/large_stack_frames.rs:25:1
3+
|
4+
LL | / fn many_small_arrays() {
5+
LL | | let x = [0u8; 500_000];
6+
LL | | let x2 = [0u8; 500_000];
7+
LL | | let x3 = [0u8; 500_000];
8+
... |
9+
LL | | black_box((&x, &x2, &x3, &x4, &x5));
10+
LL | | }
11+
| |_^
12+
|
13+
= note: allocating large amounts of stack space can overflow the stack
14+
= note: `-D clippy::large-stack-frames` implied by `-D warnings`
15+
16+
error: this function allocates a large amount of stack space
17+
--> $DIR/large_stack_frames.rs:34:1
18+
|
19+
LL | / fn large_return_value() -> ArrayDefault<1_000_000> {
20+
LL | | Default::default()
21+
LL | | }
22+
| |_^
23+
|
24+
= note: allocating large amounts of stack space can overflow the stack
25+
26+
error: this function allocates a large amount of stack space
27+
--> $DIR/large_stack_frames.rs:38:1
28+
|
29+
LL | / fn large_fn_arg(x: ArrayDefault<1_000_000>) {
30+
LL | | black_box(&x);
31+
LL | | }
32+
| |_^
33+
|
34+
= note: allocating large amounts of stack space can overflow the stack
35+
36+
error: aborting due to 3 previous errors
37+

0 commit comments

Comments
 (0)