Skip to content

vec_init_then_push should only trigger if there are no further pushes #7071

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

Closed
jyn514 opened this issue Apr 12, 2021 · 13 comments · Fixed by #8699
Closed

vec_init_then_push should only trigger if there are no further pushes #7071

jyn514 opened this issue Apr 12, 2021 · 13 comments · Fixed by #8699
Labels
C-bug Category: Clippy is not doing the correct thing E-medium Call for participation: Medium difficulty level problem and requires some initial experience. I-false-positive Issue: The lint was triggered on code it shouldn't have

Comments

@jyn514
Copy link
Member

jyn514 commented Apr 12, 2021

Lint name: vec_init_then_push

I tried this code:

    let mut arg_strings: Vec<Box<str>> = Vec::new();
    arg_strings.push(name.to_owned().into_boxed_str());
    for arg in args {
        arg_strings.push(
            arg.as_ref()
                .to_os_string()
                .into_string()
                .unwrap()
                .into_boxed_str(),
        );
    }

I expected to see this happen: The lint doesn't trigger, because using vec![] and then immediately pushing doesn't improve the code any, it uses two different styles of initializing the vector.

Instead, this happened:

warning: calls to `push` immediately after creation
   --> tests/mock/clitools.rs:613:5
    |
613 | /     let mut arg_strings: Vec<Box<str>> = Vec::new();
614 | |     arg_strings.push(name.to_owned().into_boxed_str());
    | |_______________________________________________________^ help: consider using the `vec![]` macro: `let mut arg_strings: Vec<Box<str>> = vec![..];`

cc rust-lang/rustup#2718, @kinnison

Meta

  • cargo clippy -V: clippy 0.1.53 (dae9d6a 2021-04-09)
  • rustc -v: rustc 1.53.0-nightly (dae9d6a 2021-04-09)
@jyn514 jyn514 added C-bug Category: Clippy is not doing the correct thing I-false-positive Issue: The lint was triggered on code it shouldn't have labels Apr 12, 2021
@giraffate giraffate added the E-medium Call for participation: Medium difficulty level problem and requires some initial experience. label Apr 13, 2021
@Jarcho
Copy link
Contributor

Jarcho commented Apr 13, 2021

This isn't really a false positive. Could be

    let mut arg_strings: Vec<Box<str>> = vec![name.to_owned().into_boxed_str()];
    for arg in args {
        arg_strings.push(
            arg.as_ref()
                .to_os_string()
                .into_string()
                .unwrap()
                .into_boxed_str(),
        );
    }

@kinnison
Copy link
Contributor

What about if you have code of the form:

let mut foo = Vec::new();
foo.push("bar".to_string());
for x in 0..9 {
    foo.push(format!("baz {}", x));
}
foo.push("wibble".to_string());
.....

The pattern here is clear as pushes, but if you move the first push into a vec![] then the pattern is less clean. Also vec![] essentially just does the new+push pair clippy is wanting to replace, with no performance benefit to be drawn.

The example in the lint shows the removal of mut as a result of doing this work, and realistically I think the lint should only fire if it'd be possible to remove the mut as a result of the refactor, or at least if there are zero push calls as a result.

@Jarcho
Copy link
Contributor

Jarcho commented Apr 15, 2021

Really Vec::with_capacity should be used in this case from a performance perspective. It would also stop this lint from triggering. That's a whole different discussion though.

In this case I guess if there's only one or two items being pushed before a loop pushing items (or extend, and maybe others) it might be better to leave it as is.

@camsteffen
Copy link
Contributor

I think the original request is reasonable. The lint is really only valuable if you can put all the elements in vec!.

@Jarcho
Copy link
Contributor

Jarcho commented May 11, 2021

From a performance perspective it's always better to use the macro with the current Vec implementation. A default Vec starts with no capacity and increases by powers of two, starting at 1. So even the macro with a single item avoids that first capacity check.

@uazu
Copy link

uazu commented May 17, 2021

That's not true (that it increases from a starting capacity of 1). I checked the source. It starts with zero, and then jumps to a capacity of 8 for u8, or 4 for anything else under 1KB. I have a situation where I create a Vec, add one item and then add several more in a loop. The clippy suggestion to switch to vec! will produce less-efficient code, since (checking the source) the vec! creates a box (of size one) which is then converted into a Vec of capacity one.

So my original code creates an empty Vec, then allocates space for 4 items, and so on. Switching to vec!, it would allocate space for 1 item, then jump to 4 and so on. So that's one extra unnecessary malloc/free with the clippy suggestion.

I agree with the original request: Using vec! should only be suggested when nothing else will be added to the Vec later on. Or if you see more than 4 pushes immediately after the Vec creation (with current std library implementation), it would also be worthwhile switching to vec! even if there are pushes later on. Otherwise it becomes an annoying clippy lint that has to be disabled. Thanks

@uazu
Copy link

uazu commented May 17, 2021

Here's a playground link that demonstrates the problem. Assuming that the push of 2 occurs later, the clippy suggestion costs an extra malloc/free.

@camsteffen
Copy link
Contributor

@uazu Good point, that adds some nuance.

Or if you see more than 4 pushes immediately after the Vec creation (with current std library implementation)

I don't think we should be dependent on the std implementation to that level of detail.

@Jarcho
Copy link
Contributor

Jarcho commented May 17, 2021

Looks like I was looking at old sources.

@camsteffen
Copy link
Contributor

Here's an idea. We can split into two lints.

  1. Vec::new is followed by a constant number of pushes - should use vec!
  2. Vec::new is followed by a dynamic number of pushes - should use Vec::with_capacity

@uazu
Copy link

uazu commented May 18, 2021

Yes, case 1 seems like a clear win. Also using vec! in this case won't over-allocate (i.e. it will allocate exactly the right number, unlike pushing to a Vec).

Case 2 depends. Sometimes it's not possible to determine the required capacity in advance. Can clippy detect when the capacity could have been determined by the coder? That seems like quite an involved analysis. Sometimes Vec's behaviour is exactly what you want, i.e. optimised for short lists, but can still handle big lists if required, i.e. it scales smoothly.

@camsteffen
Copy link
Contributor

Can clippy detect when the capacity could have been determined by the coder? That seems like quite an involved analysis.

I think it is theoretically always possible to calculate the number of elements pushed within the function before pushing. But oftentimes it is too complex and not worth it. If the Vec is returned or passed mutably to another function, all bets are off. The really simple cases may be better written using collect.

Sometimes Vec's behaviour is exactly what you want

Very true. That means a lot of false positives for case 2. It would definitely not be warn-by-default. But I'm starting to think we should not lint those cases at all - which just goes back to the original request...

@rbtcollins
Copy link

I think it is theoretically always possible to calculate the number of elements pushed within the function before pushing.

@camsteffen only if the content is coming from a ExactSizeIterator or similarly capable source. For instance, an iterator over stdin lines() stopping at the first blank line isn't able to estimate the size at all without buffering (presumably in another vector, giving this issue again :P).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-bug Category: Clippy is not doing the correct thing E-medium Call for participation: Medium difficulty level problem and requires some initial experience. I-false-positive Issue: The lint was triggered on code it shouldn't have
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants