-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
RuntimeInformation
causes unexpected allocations
#101536
Comments
Tagging subscribers to this area: @dotnet/gc |
There are number of background threads running in your app: finalizer thread, tiered JIT thread, diagnostics (event pipe) thread, ... . All these threads can wake up, do work and allocate as side-effect. The allocations you are seeing are likely from these background threads waking up and allocating. When I run your repro under debugger (that also affects the timing), I see some allocations from finalizers running on the finalizer thread while your benchmark is running in the foreground. Adding/removing RuntimeInformation.FrameworkDescription call is changing the timing and the work that some of these background threads need to do. For example, adding the call can introduce more work for the tiered JIT thread. |
@jkotas Sure, I can believe that we may get some random allocations from background threads we cannot control, but I don't believe that's what's happening here. If I measure it repeatedly, I see the same results repeatedly. public static void Main()
{
var bench = new Bench();
var action = bench.Benchmark1;
Warmup(action);
// Switch these lines to observe 0 allocated bytes.
var frameworkDescription = RuntimeInformation.FrameworkDescription;
//var frameworkDescription = "";
Console.WriteLine($"allocated bytes: {MeasureAllocations(action)}");
Console.WriteLine($"allocated bytes: {MeasureAllocations(action)}");
Console.WriteLine($"allocated bytes: {MeasureAllocations(action)}");
Console.WriteLine($"allocated bytes: {MeasureAllocations(action)}");
Console.WriteLine($"allocated bytes: {MeasureAllocations(action)}");
Console.WriteLine($"allocated bytes: {MeasureAllocations(action)}");
Console.WriteLine($"allocated bytes: {MeasureAllocations(action)}");
Console.WriteLine($"allocated bytes: {MeasureAllocations(action)}");
Console.WriteLine($"allocated bytes: {MeasureAllocations(action)}");
Console.WriteLine($"allocated bytes: {MeasureAllocations(action)}");
GC.KeepAlive(frameworkDescription);
}
static long MeasureAllocations(Action action)
{
var initial = GetAllocatedBytes();
action();
var final = GetAllocatedBytes();
return final - initial;
} allocated bytes: 336
allocated bytes: 336
allocated bytes: 336
allocated bytes: 336
allocated bytes: 336
allocated bytes: 336
allocated bytes: 336
allocated bytes: 336
allocated bytes: 336
allocated bytes: 336 Without allocated bytes: 0
allocated bytes: 0
allocated bytes: 0
allocated bytes: 0
allocated bytes: 0
allocated bytes: 0
allocated bytes: 0
allocated bytes: 0
allocated bytes: 0
allocated bytes: 0 |
|
(It is not the only alllocation that ArrayPool cleanup does.) |
Hm, that makes sense. Can the allocations be eliminated? Or do you have any suggestions to avoid the issue? |
I suppose it may be possible to eliminate it by somehow, but I am sure whether it is worth the troubles. Many of the background runtime activities can allocate for number of different reasons. It does not make sense to try to eliminate every single one of them to make the benchmark.net alllocation analyzer reliable. If you do not want the background activities to interfere, the best way is to disable them: disable tiered JIT, disable event source, hang the finalizer by creating a finalizer with infinite sleep, etc. |
If you are okay with the overhead, The native You can filter out allocation that does not happen on the thread of interest. We have a sample test case here to show how it could be done. |
I don't know what other allocations there are, but I suppose a low effort reduction there would be to use a struct enumerator for the ConditionalWeakTable (internal only for safety, of course).
Hanging the finalizer thread is certainly an interesting idea. I'll try to see if I can work that into our tests. |
Tagging subscribers to this area: @mangod9 |
Description
While debugging and attempting to fix flaky allocation measurements in BenchmarkDotNet (dotnet/BenchmarkDotNet#2562), I found that
System.Runtime.InteropServices.RuntimeInformation
causes allocations during aGC.Collect()
.I only observe this behavior in
net8.0
runtime.net48
runtime usingMicrosoft.DotNet.PlatformAbstractions
nuget package does not reproduce (orAppDomain.MonitoringTotalAllocatedMemorySize
just doesn't observe it, sinceGC.GetTotalAllocatedBytes
is not available innet48
).Reproduction Steps
Create a Console application and run this code in Release with net8.0.
Expected behavior
Console prints
allocated bytes: 0
.Actual behavior
Console prints
allocated bytes: 336
(sometimes it prints 48 or 672).Regression?
No response
Known Workarounds
No response
Configuration
Windows 10 x64
.Net 8.0
Other information
No response
The text was updated successfully, but these errors were encountered: