Skip to content

Commit c864594

Browse files
committed
Added AggressiveOptimization to methods involved in measuring allocations.
Warm up allocation measurement before taking actual measurement. Moved a potential allocation offender to after the `GcStats.ReadFinal()` call. Changed some `RuntimeInformation` properties to static readonly fields. Removed enable monitoring in Engine (GcStats handles it).
1 parent a24d689 commit c864594

File tree

3 files changed

+35
-37
lines changed

3 files changed

+35
-37
lines changed

Diff for: src/BenchmarkDotNet/Engines/Engine.cs

+8-15
Original file line numberDiff line numberDiff line change
@@ -212,12 +212,12 @@ private ClockSpan Measure(Action<long> action, long invokeCount)
212212
return clock.GetElapsed();
213213
}
214214

215+
// Make sure tier0 jit doesn't cause any unexpected allocations in this method.
216+
[MethodImpl(CodeGenHelper.AggressiveOptimizationOption)]
215217
private (GcStats, ThreadingStats, double) GetExtraStats(IterationData data)
216218
{
217-
// we enable monitoring after main target run, for this single iteration which is executed at the end
218-
// so even if we enable AppDomain monitoring in separate process
219-
// it does not matter, because we have already obtained the results!
220-
EnableMonitoring();
219+
// Warm up the GetAllocatedBytes function before starting the actual measurement.
220+
DeadCodeEliminationHelper.KeepAliveWithoutBoxing(GcStats.ReadInitial());
221221

222222
IterationSetupAction(); // we run iteration setup first, so even if it allocates, it is not included in the results
223223

@@ -228,8 +228,8 @@ private ClockSpan Measure(Action<long> action, long invokeCount)
228228

229229
WorkloadAction(data.InvokeCount / data.UnrollFactor);
230230

231-
exceptionsStats.Stop();
232231
var finalGcStats = GcStats.ReadFinal();
232+
exceptionsStats.Stop(); // this method might (de)allocate
233233
var finalThreadingStats = ThreadingStats.ReadFinal();
234234

235235
IterationCleanupAction(); // we run iteration cleanup after collecting GC stats
@@ -267,7 +267,9 @@ private void GcCollect()
267267
ForceGcCollect();
268268
}
269269

270-
private static void ForceGcCollect()
270+
// Make sure tier0 jit doesn't cause any unexpected allocations in this method.
271+
[MethodImpl(CodeGenHelper.AggressiveOptimizationOption)]
272+
internal static void ForceGcCollect()
271273
{
272274
GC.Collect();
273275
GC.WaitForPendingFinalizers();
@@ -278,15 +280,6 @@ private static void ForceGcCollect()
278280

279281
public void WriteLine() => Host.WriteLine();
280282

281-
private static void EnableMonitoring()
282-
{
283-
if (RuntimeInformation.IsOldMono) // Monitoring is not available in Mono, see http://stackoverflow.com/questions/40234948/how-to-get-the-number-of-allocated-bytes-in-mono
284-
return;
285-
286-
if (RuntimeInformation.IsFullFramework)
287-
AppDomain.MonitoringIsEnabled = true;
288-
}
289-
290283
[UsedImplicitly]
291284
public static class Signals
292285
{

Diff for: src/BenchmarkDotNet/Engines/GcStats.cs

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Reflection;
3+
using System.Runtime.CompilerServices;
34
using BenchmarkDotNet.Columns;
45
using BenchmarkDotNet.Jobs;
56
using BenchmarkDotNet.Portability;
@@ -106,6 +107,8 @@ public int GetCollectionsCount(int generation)
106107
return AllocatedBytes <= AllocationQuantum ? 0L : AllocatedBytes;
107108
}
108109

110+
// Make sure tier0 jit doesn't cause any unexpected allocations in this method.
111+
[MethodImpl(CodeGenHelper.AggressiveOptimizationOption)]
109112
public static GcStats ReadInitial()
110113
{
111114
// this will force GC.Collect, so we want to do this before collecting collections counts
@@ -119,6 +122,8 @@ public static GcStats ReadInitial()
119122
0);
120123
}
121124

125+
// Make sure tier0 jit doesn't cause any unexpected allocations in this method.
126+
[MethodImpl(CodeGenHelper.AggressiveOptimizationOption)]
122127
public static GcStats ReadFinal()
123128
{
124129
return new GcStats(
@@ -136,6 +141,8 @@ public static GcStats ReadFinal()
136141
public static GcStats FromForced(int forcedFullGarbageCollections)
137142
=> new GcStats(forcedFullGarbageCollections, forcedFullGarbageCollections, forcedFullGarbageCollections, 0, 0);
138143

144+
// Make sure tier0 jit doesn't cause any unexpected allocations in this method.
145+
[MethodImpl(CodeGenHelper.AggressiveOptimizationOption)]
139146
private static long? GetAllocatedBytes()
140147
{
141148
// we have no tests for WASM and don't want to risk introducing a new bug (https://github.com/dotnet/BenchmarkDotNet/issues/2226)
@@ -145,7 +152,7 @@ public static GcStats FromForced(int forcedFullGarbageCollections)
145152
// "This instance Int64 property returns the number of bytes that have been allocated by a specific
146153
// AppDomain. The number is accurate as of the last garbage collection." - CLR via C#
147154
// so we enforce GC.Collect here just to make sure we get accurate results
148-
GC.Collect();
155+
Engine.ForceGcCollect();
149156

150157
#if NET6_0_OR_GREATER
151158
return GC.GetTotalAllocatedBytes(precise: true);
@@ -218,9 +225,7 @@ private static long CalculateAllocationQuantumSize()
218225
break;
219226
}
220227

221-
GC.Collect();
222-
GC.WaitForPendingFinalizers();
223-
GC.Collect();
228+
Engine.ForceGcCollect();
224229

225230
result = GC.GetTotalMemory(false);
226231
var tmp = new object();

Diff for: src/BenchmarkDotNet/Portability/RuntimeInformation.cs

+18-18
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
using BenchmarkDotNet.Extensions;
1313
using BenchmarkDotNet.Helpers;
1414
using BenchmarkDotNet.Portability.Cpu;
15-
using JetBrains.Annotations;
1615
using Microsoft.Win32;
1716
using static System.Runtime.InteropServices.RuntimeInformation;
1817
using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment;
@@ -25,46 +24,47 @@ internal static class RuntimeInformation
2524
internal const string ReleaseConfigurationName = "RELEASE";
2625
internal const string Unknown = "?";
2726

27+
// Many of these checks allocate and/or are expensive to compute. We store the results in static readonly fields to keep Engine non-allocating.
28+
// Static readonly fields are used instead of properties to avoid an extra getter method call that might not be tier1 jitted.
29+
// This class is internal, so we don't need to expose these as properties.
30+
2831
/// <summary>
2932
/// returns true for both the old (implementation of .NET Framework) and new Mono (.NET 6+ flavour)
3033
/// </summary>
31-
public static bool IsMono { get; } = Type.GetType("Mono.RuntimeStructs") != null; // it allocates a lot of memory, we need to check it once in order to keep Engine non-allocating!
34+
public static readonly bool IsMono = Type.GetType("Mono.RuntimeStructs") != null;
3235

33-
public static bool IsOldMono { get; } = Type.GetType("Mono.Runtime") != null;
36+
public static readonly bool IsOldMono = Type.GetType("Mono.Runtime") != null;
3437

35-
public static bool IsNewMono { get; } = IsMono && !IsOldMono;
38+
public static readonly bool IsNewMono = IsMono && !IsOldMono;
3639

37-
public static bool IsFullFramework =>
40+
public static readonly bool IsFullFramework =
3841
#if NET6_0_OR_GREATER
42+
// This could be const, but we want to avoid unreachable code warnings.
3943
false;
4044
#else
4145
FrameworkDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase);
4246
#endif
4347

44-
[PublicAPI]
45-
public static bool IsNetNative => FrameworkDescription.StartsWith(".NET Native", StringComparison.OrdinalIgnoreCase);
48+
public static readonly bool IsNetNative = FrameworkDescription.StartsWith(".NET Native", StringComparison.OrdinalIgnoreCase);
4649

47-
public static bool IsNetCore
48-
=> ((Environment.Version.Major >= 5) || FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase))
50+
public static readonly bool IsNetCore =
51+
((Environment.Version.Major >= 5) || FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase))
4952
&& !string.IsNullOrEmpty(typeof(object).Assembly.Location);
5053

51-
public static bool IsNativeAOT
52-
=> Environment.Version.Major >= 5
54+
public static readonly bool IsNativeAOT =
55+
Environment.Version.Major >= 5
5356
&& string.IsNullOrEmpty(typeof(object).Assembly.Location) // it's merged to a single .exe and .Location returns null
5457
&& !IsWasm; // Wasm also returns "" for assembly locations
5558

5659
#if NET6_0_OR_GREATER
5760
[System.Runtime.Versioning.SupportedOSPlatformGuard("browser")]
58-
#endif
59-
public static bool IsWasm =>
60-
#if NET6_0_OR_GREATER
61-
OperatingSystem.IsBrowser();
61+
public static readonly bool IsWasm = OperatingSystem.IsBrowser();
6262
#else
63-
IsOSPlatform(OSPlatform.Create("BROWSER"));
63+
public static readonly bool IsWasm = IsOSPlatform(OSPlatform.Create("BROWSER"));
6464
#endif
6565

6666
#if NETSTANDARD2_0
67-
public static bool IsAot { get; } = IsAotMethod(); // This allocates, so we only want to call it once statically.
67+
public static readonly bool IsAot = IsAotMethod();
6868

6969
private static bool IsAotMethod()
7070
{
@@ -82,7 +82,7 @@ private static bool IsAotMethod()
8282
return false;
8383
}
8484
#else
85-
public static bool IsAot => !System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeCompiled;
85+
public static readonly bool IsAot = !System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeCompiled;
8686
#endif
8787

8888
public static bool IsRunningInContainer => string.Equals(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"), "true");

0 commit comments

Comments
 (0)