Skip to content

Commit

Permalink
Added AggressiveOptimization to methods involved in measuring alloc…
Browse files Browse the repository at this point in the history
…ations.

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).
  • Loading branch information
timcassell committed Apr 15, 2024
1 parent a24d689 commit c864594
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 37 deletions.
23 changes: 8 additions & 15 deletions src/BenchmarkDotNet/Engines/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,12 +212,12 @@ private ClockSpan Measure(Action<long> action, long invokeCount)
return clock.GetElapsed();
}

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

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

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

WorkloadAction(data.InvokeCount / data.UnrollFactor);

exceptionsStats.Stop();
var finalGcStats = GcStats.ReadFinal();
exceptionsStats.Stop(); // this method might (de)allocate
var finalThreadingStats = ThreadingStats.ReadFinal();

IterationCleanupAction(); // we run iteration cleanup after collecting GC stats
Expand Down Expand Up @@ -267,7 +267,9 @@ private void GcCollect()
ForceGcCollect();
}

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

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

private static void EnableMonitoring()
{
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
return;

if (RuntimeInformation.IsFullFramework)
AppDomain.MonitoringIsEnabled = true;
}

[UsedImplicitly]
public static class Signals
{
Expand Down
13 changes: 9 additions & 4 deletions src/BenchmarkDotNet/Engines/GcStats.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Portability;
Expand Down Expand Up @@ -106,6 +107,8 @@ public int GetCollectionsCount(int generation)
return AllocatedBytes <= AllocationQuantum ? 0L : AllocatedBytes;
}

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

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

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

#if NET6_0_OR_GREATER
return GC.GetTotalAllocatedBytes(precise: true);
Expand Down Expand Up @@ -218,9 +225,7 @@ private static long CalculateAllocationQuantumSize()
break;
}

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Engine.ForceGcCollect();

result = GC.GetTotalMemory(false);
var tmp = new object();
Expand Down
36 changes: 18 additions & 18 deletions src/BenchmarkDotNet/Portability/RuntimeInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
using BenchmarkDotNet.Extensions;
using BenchmarkDotNet.Helpers;
using BenchmarkDotNet.Portability.Cpu;
using JetBrains.Annotations;
using Microsoft.Win32;
using static System.Runtime.InteropServices.RuntimeInformation;
using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment;
Expand All @@ -25,46 +24,47 @@ internal static class RuntimeInformation
internal const string ReleaseConfigurationName = "RELEASE";
internal const string Unknown = "?";

// Many of these checks allocate and/or are expensive to compute. We store the results in static readonly fields to keep Engine non-allocating.
// Static readonly fields are used instead of properties to avoid an extra getter method call that might not be tier1 jitted.
// This class is internal, so we don't need to expose these as properties.

/// <summary>
/// returns true for both the old (implementation of .NET Framework) and new Mono (.NET 6+ flavour)
/// </summary>
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!
public static readonly bool IsMono = Type.GetType("Mono.RuntimeStructs") != null;

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

public static bool IsNewMono { get; } = IsMono && !IsOldMono;
public static readonly bool IsNewMono = IsMono && !IsOldMono;

public static bool IsFullFramework =>
public static readonly bool IsFullFramework =
#if NET6_0_OR_GREATER
// This could be const, but we want to avoid unreachable code warnings.
false;
#else
FrameworkDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase);
#endif

[PublicAPI]
public static bool IsNetNative => FrameworkDescription.StartsWith(".NET Native", StringComparison.OrdinalIgnoreCase);
public static readonly bool IsNetNative = FrameworkDescription.StartsWith(".NET Native", StringComparison.OrdinalIgnoreCase);

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

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

#if NET6_0_OR_GREATER
[System.Runtime.Versioning.SupportedOSPlatformGuard("browser")]
#endif
public static bool IsWasm =>
#if NET6_0_OR_GREATER
OperatingSystem.IsBrowser();
public static readonly bool IsWasm = OperatingSystem.IsBrowser();
#else
IsOSPlatform(OSPlatform.Create("BROWSER"));
public static readonly bool IsWasm = IsOSPlatform(OSPlatform.Create("BROWSER"));
#endif

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

private static bool IsAotMethod()
{
Expand All @@ -82,7 +82,7 @@ private static bool IsAotMethod()
return false;
}
#else
public static bool IsAot => !System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeCompiled;
public static readonly bool IsAot = !System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeCompiled;
#endif

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

0 comments on commit c864594

Please sign in to comment.