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

new lint to detect inefficient iter().any() #13817

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

lapla-cogito
Copy link
Contributor

@lapla-cogito lapla-cogito commented Dec 12, 2024

fix #13353

Using contains() for numeric slices are more efficient than using iter().any().

changelog: [slice_iter_any]: new lint

@rustbot
Copy link
Collaborator

rustbot commented Dec 12, 2024

r? @Manishearth

rustbot has assigned @Manishearth.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Dec 12, 2024
@lapla-cogito lapla-cogito force-pushed the contains_for_u8i8 branch 2 times, most recently from acb27cb to 8eb9d35 Compare December 12, 2024 01:30
@lapla-cogito
Copy link
Contributor Author

lapla-cogito commented Dec 12, 2024

On second thought, I thought it would be more appropriate to place the lint under the methods directory. Therefore, I am sorry, but I'm going to convert to draft this PR and modify it. now fixed

@lapla-cogito lapla-cogito marked this pull request as draft December 12, 2024 02:35
@lapla-cogito lapla-cogito marked this pull request as ready for review December 12, 2024 02:59
CHANGELOG.md Outdated Show resolved Hide resolved
/// Checks for usage of `iter().any()` on slices of `u8` or `i8` and suggests using `contains()` instead.
///
/// ### Why is this bad?
/// `iter().any()` on slices of `u8` or `i8` is optimized to use `memchr`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't you mean .contains()? Anyway, I think this is too specific, on my machine it seems to use compiler intrinsics and make no calls to memchr.

You're right about performances though: I benchmarked both .iter().any(==) and .contains() and the latter runs more than 10 times faster on large areas.

Copy link
Contributor Author

@lapla-cogito lapla-cogito Dec 13, 2024

Choose a reason for hiding this comment

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

Oh, I had assumed from the implementation that memchr would be used in any environment (including my local environment). Also, this godbolt does so as far as the assembly is concerned: https://rust.godbolt.org/z/Kxrzr81Me

However, if it may differ depending on the environment, the description should be indeed modified, so I'll make a change. Could you please tell me for reference, what's the environment you have checked that?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the memchr itself has been replaced by an intrinsic. I'm using the latest nightly compiler on x86_64.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you! In any case, it looks like this lint description should be modified.

Copy link
Member

Choose a reason for hiding this comment

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

I think the memchr() exists for larger integer types too, yes?

Copy link
Member

Choose a reason for hiding this comment

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

oh, nope, it doesn't. It could be extended, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I found that contains() is now faster for certain types (u16, u32, u64, i16, i32, i64, f32, f64, usize, isize) compared to before (see: rust-lang/rust#130991). Therefore, this performance lint should be extended to cover these types starting from Rust 1.84.0 and I'll make a change for this.

let vec: Vec<u32> = vec![1, 2, 3, 4, 5, 6];
let values = &vec[..];
let _ = values.iter().any(|&v| v == 4);
// no error, because it's not a slice of u8/i8
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't it be more readable to use .contains() here too? I thought that was what @Manishearth was suggesting.


let values: [u8; 6] = [3, 14, 15, 92, 6, 5];
let _ = values.iter().any(|&v| v == 10);
// no error, because it's an array
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't it be more efficient to lint there, even though this is an array?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The optimization doesn't seem to work for arrays. Because of the implementation I mentioned in this.

Copy link
Contributor

Choose a reason for hiding this comment

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

But isn't it clearer anyway to use .contains() rather than .any(==)?

Copy link
Contributor Author

@lapla-cogito lapla-cogito Dec 13, 2024

Choose a reason for hiding this comment

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

If you are suggesting that clippy should modify this code style, you may be right. I'll try to make changes.

Copy link
Contributor Author

@lapla-cogito lapla-cogito Dec 13, 2024

Choose a reason for hiding this comment

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

However, after thinking about it, what this lint should do is to suggest performance improvements for u8 and i8 slices, and it seems appropriate to make this as a separate lint. What do you think? If this is a good idea, I'll implement this as a new lint in another PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't a lint which is more efficient for some types (u8/i8) and not less efficient for some others deserve to be in the performance category? And by the way, I see the same 10+ performance boost in u32 as well.

It looks like a second lint would also cover this one, I'm not sure two lints are needed. I'll let others weigh in.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see the same 10+ performance boost in u32 as well

I didn't check about it. Thank you very much. If so, it seems more reasonable to combine them into one as a single lint.

Copy link
Contributor

Choose a reason for hiding this comment

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

In case you don't have one, here is the one I used, for testing func1 and func2, the two versions I wanted to compare.

@lapla-cogito lapla-cogito force-pushed the contains_for_u8i8 branch 2 times, most recently from fe5f2c4 to 041f76b Compare December 14, 2024 01:49
@lapla-cogito lapla-cogito force-pushed the contains_for_u8i8 branch 3 times, most recently from 2b9d742 to b3837dc Compare December 14, 2024 02:10
@lapla-cogito lapla-cogito changed the title new lint to use contains() instead of iter().any() for u8 and i8 slices new lints to detect inefficient iter().any() Dec 14, 2024
ty::Ref(_, inner_type, _) if inner_type.is_slice() => {
// check if the receiver is a u8/i8 slice
if let ty::Slice(slice_type) = inner_type.kind()
&& (slice_type.to_string() == "u8" || slice_type.to_string() == "i8")
Copy link
Contributor Author

@lapla-cogito lapla-cogito Dec 14, 2024

Choose a reason for hiding this comment

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

In my environment, I could only see the speedup for the u8 and i8 slices (about 5~7x) while @samueltardieu says he has been able to confirm this with other types of slices, so the changes in this PR are only for these types.
At least the speedups for the u8 and i8 slices are correct for reasons that come from the Rust implementation, I think.

Copy link
Member

@Manishearth Manishearth left a comment

Choose a reason for hiding this comment

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

A thing I'm unsure about is the mixing of two lints like this. I'm going to ask on Zulip if we should be doing two lints here.

https://rust-lang.zulipchat.com/#narrow/channel/257328-clippy/topic/iter.2Eany.28.29.20lint.3A.20one.20or.20two.20lints.3F

&& let Some((name, recv, _, _, _)) = method_call(recv)
&& name == "iter"
{
let ref_type = cx.typeck_results().expr_ty(recv);
Copy link
Member

Choose a reason for hiding this comment

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

issue: Probably should be expr_ty_adjusted to handle autoderef. Add a test that ensures this works on a vector vec.iter().any(...).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you! I changed to use expr_ty_adjusted and added a test for this in e964cbc.

Copy link
Contributor Author

@lapla-cogito lapla-cogito left a comment

Choose a reason for hiding this comment

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

@Manishearth TBH, I added style lint (unnecessary_iter_any) through the review process, which I consider unnecessary at least for this change.
Certainly there will be some cases where replacing iter().any() with contains() will be more readable, but I think it could also lead to false positives. Not only that, I found that adding the unnecessary_iter_any lint required a lot of modifications in the existing code base of Clippy.
Therefore, I think it is appropriate to limit this change to adding slice_iter_any lint for slices of numeric slices. What do you think?

edit: What I meant is e964cbc. If the changes that follow this cource are acceptable, I'll squash the previous commits as appropriate.

@lapla-cogito lapla-cogito changed the title new lints to detect inefficient iter().any() new lint to detect inefficient iter().any() Dec 22, 2024
Comment on lines +53 to +66
fn can_replace_with_contains(op: Spanned<BinOpKind>, lhs: &Expr<'_>, rhs: &Expr<'_>) -> bool {
matches!(
(op.node, &lhs.kind, &rhs.kind),
(
BinOpKind::Eq,
ExprKind::Path(_) | ExprKind::Unary(_, _),
ExprKind::Lit(_) | ExprKind::Path(_)
) | (
BinOpKind::Eq,
ExprKind::Lit(_),
ExprKind::Path(_) | ExprKind::Unary(_, _)
)
)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For example, the following code uses == inside closures, but cannot simply be replaced by contains():

let _ = values.iter().any(|&v| v % 2 == 0);

Therefore, this function exclude such cases.

@Manishearth
Copy link
Member

What do you think?

I think that we can add a single numeric-only lint to the nursery but we should have the clippy team figure out whether we wish to

  1. add two lints (one perf, one style)
  2. add a single perf lint with a config for how expansive it is
  3. add a single perf lint that is expansive
  4. add a single perf lint that is numeric-only

before the numeric lint is moved out of the nursery. Currently 2 and 3 are options that would be made harder by adding a numeric-only lint outside of the nursery. Clippy is allowed to expand lints after release but when it does so it can be annoying to people so we try to limit that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Suggest faster .contains() instead of .iter().any() for [u8] and [i8] slices
4 participants