
* Add benchmark * Update rules/S1215/csharp/rule.adoc Co-authored-by: Sebastien Marichal <sebastien.marichal@sonarsource.com> * Review --------- Co-authored-by: Sebastien Marichal <sebastien.marichal@sonarsource.com>
110 lines
5.0 KiB
Plaintext
110 lines
5.0 KiB
Plaintext
== Why is this an issue?
|
|
|
|
https://learn.microsoft.com/en-us/dotnet/api/system.gc.collect[GC.Collect] is a method that forces or suggests to the https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/[garbage collector] to run a collection of objects in the managed heap that are no longer being used and free their memory.
|
|
|
|
Calling `GC.Collect` is rarely necessary and can significantly affect application performance. That's because it is a https://en.wikipedia.org/wiki/Tracing_garbage_collection[tracing garbage collector] and needs to examine _every object in memory_ for cleanup and analyze all reachable objects from every application's root (static fields, local variables on thread stacks, etc.).
|
|
|
|
To perform tracing and memory releasing correctly, the garbage collection https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/latency[may] need to block all threads currently in execution. That is why, as a general rule, the https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/performance#troubleshoot-performance-issues[performance implications] of calling `GC.Collect` far outweigh the benefits.
|
|
|
|
This rule raises an issue when any overload of `Collect` is invoked.
|
|
|
|
[source,csharp]
|
|
----
|
|
static void Main(string[] args)
|
|
{
|
|
// ...
|
|
GC.Collect(); // Noncompliant
|
|
GC.Collect(2, GCCollectionMode.Optimized); // Noncompliant
|
|
}
|
|
----
|
|
|
|
There may be exceptions to this rule: for example, you've just triggered some event that is unique in the run of your program that caused a lot of long-lived objects to die, and you want to release their memory.
|
|
|
|
== Resources
|
|
|
|
=== Documentation
|
|
|
|
* https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/[Garbage collection]
|
|
* https://learn.microsoft.com/en-us/dotnet/api/system.gc.collect[GC.Collect]
|
|
* https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/latency[Garbage collection latency modes]
|
|
* https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/performance#troubleshoot-performance-issues[Garbage collection troubleshoot performance issues]
|
|
|
|
=== Benchmarks
|
|
|
|
Each .NET runtime features distinct implementations, modes, and configurations for its garbage collector.
|
|
The benchmark below illustrates how invoking `GC.Collect()` can have opposite effects across different runtimes.
|
|
|
|
[options="header"]
|
|
|===
|
|
| Runtime | Collect | Mean | Standard Deviation | Allocated
|
|
| .NET 9.0 | False | 659.2 ms | 15.69 ms | 205.95 MB
|
|
| .NET 9.0 | True | 888.8 ms | 15.34 ms | 205.95 MB
|
|
| | | | |
|
|
| .NET Framework 4.8.1 | False | 545.7 ms | 19.49 ms | 228.8 MB
|
|
| .NET Framework 4.8.1 | True | 484.8 ms | 11.79 ms | 228.8 MB
|
|
|===
|
|
|
|
==== Glossary
|
|
|
|
* Collect - if `True`, `GC.Collect()` is called in the middle of the allocation heavy `Benchmark()` method
|
|
* https://en.wikipedia.org/wiki/Arithmetic_mean[Mean]
|
|
* https://en.wikipedia.org/wiki/Standard_deviation[Standard Deviation]
|
|
* https://github.com/dotnet/BenchmarkDotNet/blob/master/docs/articles/configs/diagnosers.md[Allocated]
|
|
|
|
The results were generated by running the following snippet with https://github.com/dotnet/BenchmarkDotNet[BenchmarkDotNet]:
|
|
|
|
[source,csharp]
|
|
----
|
|
class Tree
|
|
{
|
|
public List<Tree> Children = new();
|
|
}
|
|
|
|
private void AppendToTree(Tree tree, int childsPerTree, int depth)
|
|
{
|
|
if (depth == 0)
|
|
{
|
|
return;
|
|
}
|
|
for (int i = 0; i < childsPerTree; i++)
|
|
{
|
|
var child = new Tree();
|
|
tree.Children.Add(child);
|
|
AppendToTree(child, childsPerTree, depth - 1);
|
|
}
|
|
}
|
|
|
|
[Benchmark]
|
|
[Arguments(true)]
|
|
[Arguments(false)]
|
|
public void Benchmark(bool collect)
|
|
{
|
|
var tree = new Tree();
|
|
AppendToTree(tree, 8, 7); // Create 8^7 Tree objects (2.097.152 objects) linked via List<Tree> Children
|
|
GC.Collect();
|
|
GC.Collect(); // Move the objects to generation 2
|
|
AppendToTree(new Tree(), 8, 6); // Add some more memory preasure (8^6 262.144 objects) which can be collected right after this call
|
|
tree = null; // Remove all references to the tree and its content. This freees up 8^7 Tree objects (2.097.152 objects)
|
|
if (collect)
|
|
{
|
|
GC.Collect(); // Force GC to run and block until it finishes
|
|
}
|
|
AppendToTree(new Tree(), 3, 10); // Do some more allocations (3^10 = 59.049)
|
|
AppendToTree(new Tree(), 4, 7); // 4^10 = 1.048.576
|
|
AppendToTree(new Tree(), 5, 7); // 5^7 = 78.125
|
|
GC.Collect(); // Collect all the memory allocated in this method
|
|
}
|
|
----
|
|
|
|
Hardware configuration:
|
|
|
|
[source]
|
|
----
|
|
BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.5247/22H2/2022Update)
|
|
Intel Core Ultra 7 165H, 1 CPU, 22 logical and 16 physical cores
|
|
[Host] : .NET Framework 4.8.1 (4.8.9282.0), X64 RyuJIT VectorSize=256
|
|
.NET 9.0 : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2
|
|
.NET Framework 4.8.1 : .NET Framework 4.8.1 (4.8.9282.0), X64 RyuJIT VectorSize=256
|
|
----
|
|
|
|
include::rspecator.adoc[] |