Skip to content

Commit

Permalink
NET-567 Modify rule S6605: Remove from SonarWay and update benchmarks (
Browse files Browse the repository at this point in the history
  • Loading branch information
martin-strecker-sonarsource authored Oct 31, 2024
1 parent 86ac8e6 commit 56018c3
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 35 deletions.
2 changes: 1 addition & 1 deletion rules/S6605/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
"ruleSpecification": "RSPEC-6605",
"sqKey": "S6605",
"scope": "All",
"defaultQualityProfiles": ["Sonar way"],
"defaultQualityProfiles": [],
"quickfix": "targeted"
}
104 changes: 70 additions & 34 deletions rules/S6605/resources-dotnet.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,43 @@

=== Documentation

* https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.exists[List<T>.Exists(Predicate<T>)]
* https://learn.microsoft.com/en-us/dotnet/api/system.array.exists[Array.Exists<T>(T[\], Predicate<T>)]
* https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutablelist-1.exists[ImmutableList<T>.Exists(Predicate<T>)]
* https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.any[Enumerable.Any(Predicate<T>)]
* https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/ef/language-reference/linq-to-entities[LINQ to Entities]
* Microsoft Learn - https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.exists[List<T>.Exists(Predicate<T>)]
* Microsoft Learn - https://learn.microsoft.com/en-us/dotnet/api/system.array.exists[Array.Exists<T>(T[\], Predicate<T>)]
* Microsoft Learn - https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutablelist-1.exists[ImmutableList<T>.Exists(Predicate<T>)]
* Microsoft Learn - https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.any[Enumerable.Any(Predicate<T>)]
* Microsoft Learn - https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/ef/language-reference/linq-to-entities[LINQ to Entities]

=== Benchmarks

[options="header"]
|===
| Method | Runtime | Mean | Standard Deviation | Allocated
| Any | .NET 7.0 | 6.670 ms | 0.1413 ms | 40004 B
| Exists | .NET 7.0 | 1.364 ms | 0.0270 ms | 1 B
| Any | .NET Framework 4.6.2 | 5.380 ms | 0.0327 ms | 40128 B
| Exists | .NET Framework 4.6.2 | 1.575 ms | 0.0348 ms | -
| Method | Runtime | Categories | Mean | Standard Deviation | Allocated
| ArrayAny | .NET 8.0 | Array | 1,174.0 ns | 16.44 ns | 32 B
| ArrayExists | .NET 8.0 | Array | 570.6 ns | 7.12 ns | -
| | | | | |
| ArrayAny | .NET 9.0 | Array | 358.5 ns | 5.57 ns | -
| ArrayExists | .NET 9.0 | Array | 581.6 ns | 6.17 ns | -
| | | | | |
| ArrayAny | .NET Framework 4.8.1 | Array | 4,896.0 ns | 102.83 ns | 32 B
| ArrayExists | .NET Framework 4.8.1 | Array | 1,649.4 ns | 29.81 ns | -
| | | | | |
| ImmutableListAny | .NET 8.0 | ImmutableList<T> | 7,859.3 ns | 91.45 ns | 72 B
| ImmutableListExists | .NET 8.0 | ImmutableList<T> | 5,898.1 ns | 81.69 ns | -
| | | | | |
| ImmutableListAny | .NET 9.0 | ImmutableList<T> | 7,748.9 ns | 119.10 ns | 72 B
| ImmutableListExists | .NET 9.0 | ImmutableList<T> | 5,705.0 ns | 31.53 ns | -
| | | | | |
| ImmutableListAny | .NET Framework 4.8.1 | ImmutableList<T> | 45,118.5 ns | 168.72 ns | 72 B
| ImmutableListExists | .NET Framework 4.8.1 | ImmutableList<T> | 41,966.0 ns | 631.59 ns | -
| | | | | |
| ListAny | .NET 8.0 | List<T> | 1,643.5 ns | 13.09 ns | 40 B
| ListExists | .NET 8.0 | List<T> | 726.2 ns | 11.99 ns | -
| | | | | |
| ListAny | .NET 9.0 | List<T> | 398.6 ns | 8.20 ns | -
| ListExists | .NET 9.0 | List<T> | 612.4 ns | 18.73 ns | -
| | | | | |
| ListAny | .NET Framework 4.8.1 | List<T> | 5,621.5 ns | 35.80 ns | 40 B
| ListExists | .NET Framework 4.8.1 | List<T> | 1,748.0 ns | 11.76 ns | -
|===

==== Glossary
Expand All @@ -29,44 +51,58 @@ The results were generated by running the following snippet with https://github.

[source,csharp]
----
private List<int> data;
private readonly Random random = new Random();
// Explicitly cache the delegates to avoid allocations inside the benchmark.
private readonly static Func<int, bool> ConditionFunc = static x => x == -1 * Math.Abs(x);
private readonly static Predicate<int> ConditionPredicate = static x => x == -1 * Math.Abs(x);
private List<int> list;
private ImmutableList<int> immutableList;
private int[] array;
[Params(1_000)]
public int N { get; set; }
[GlobalSetup]
public void Setup() =>
data = Enumerable.Range(0, N).Select(x => 43).ToList();
[Benchmark(Baseline = true)]
public void Any()
public void GlobalSetup()
{
for (var i = 0; i < N; i++)
{
_ = data.Any(x => x % 2 == 0); // Enumerable.Any
}
list = Enumerable.Range(0, N).Select(x => N - x).ToList();
immutableList = ImmutableList.CreateRange(list);
array = list.ToArray();
}
[Benchmark]
public void Exists()
{
for (var i = 0; i < N; i++)
{
_ = data.Exists(x => x % 2 == 0); // List<T>.Exists
}
}
[BenchmarkCategory("List<T>"), Benchmark]
public bool ListAny() =>
list.Any(ConditionFunc);
[BenchmarkCategory("List<T>"), Benchmark(Baseline = true)]
public bool ListExists() =>
list.Exists(ConditionPredicate);
[BenchmarkCategory("ImmutableList<T>"), Benchmark(Baseline = true)]
public bool ImmutableListAny() =>
immutableList.Any(ConditionFunc);
[BenchmarkCategory("ImmutableList<T>"), Benchmark]
public bool ImmutableListExists() =>
immutableList.Exists(ConditionPredicate);
[BenchmarkCategory("Array"), Benchmark(Baseline = true)]
public bool ArrayAny() =>
array.Any(ConditionFunc);
[BenchmarkCategory("Array"), Benchmark]
public bool ArrayExists() =>
Array.Exists(array, ConditionPredicate);
----

Hardware configuration:

[source]
----
BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19045.2846/22H2/2022Update)
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4317/23H2/2023Update/SunValley3)
11th Gen Intel Core i7-11850H 2.50GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.203
[Host] : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2
.NET 7.0 : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2
.NET Framework 4.6.2 : .NET Framework 4.8.1 (4.8.9139.0), X64 RyuJIT VectorSize=256
[Host] : .NET Framework 4.8.1 (4.8.9277.0), X64 RyuJIT VectorSize=256
.NET 8.0 : .NET 8.0.10 (8.0.1024.46610), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
.NET 9.0 : .NET 9.0.0 (9.0.24.47305), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
.NET Framework 4.8.1 : .NET Framework 4.8.1 (4.8.9277.0), X64 RyuJIT VectorSize=256
----
2 changes: 2 additions & 0 deletions rules/S6605/why-dotnet.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Both the `List.Exists` method and `IEnumerable.Any` method can be used to find the first element that satisfies a predicate in a collection. However, `List.Exists` can be faster than `IEnumerable.Any` for `List` objects, as well as requires significantly less memory. For small collections, the performance difference may be negligible, but for large collections, it can be noticeable. The same applies to `ImmutableList` and arrays too.

It is important to enable this rule with caution, as performance outcomes can vary significantly across different runtimes. Notably, the https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-9/#collections[performance improvements in .NET 9] have brought `Any` closer to the performance of collection-specific `Exists` methods in most scenarios.

*Applies to*

* https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.exists[List]
Expand Down

0 comments on commit 56018c3

Please sign in to comment.