Skip to content

Commit 91027de

Browse files
committed
Added memory diagnoser option to measure survived memory from the first benchmark run.
1 parent 0d30991 commit 91027de

File tree

16 files changed

+309
-47
lines changed

16 files changed

+309
-47
lines changed

src/BenchmarkDotNet/Attributes/MemoryDiagnoserAttribute.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ public class MemoryDiagnoserAttribute : Attribute, IConfigSource
1010
public IConfig Config { get; }
1111

1212
/// <param name="displayGenColumns">Display Garbage Collections per Generation columns (Gen 0, Gen 1, Gen 2). True by default.</param>
13-
public MemoryDiagnoserAttribute(bool displayGenColumns = true)
13+
/// <param name="includeSurvived">If true, monitoring will be enabled and survived memory will be measured on the first benchmark run.</param>
14+
public MemoryDiagnoserAttribute(bool displayGenColumns = true, bool includeSurvived = false)
1415
{
15-
Config = ManualConfig.CreateEmpty().AddDiagnoser(new MemoryDiagnoser(new MemoryDiagnoserConfig(displayGenColumns)));
16+
Config = ManualConfig.CreateEmpty().AddDiagnoser(new MemoryDiagnoser(new MemoryDiagnoserConfig(displayGenColumns, includeSurvived)));
1617
}
1718
}
1819
}

src/BenchmarkDotNet/Code/CodeGenerator.cs

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ internal static string Generate(BuildPartition buildPartition)
6363
.Replace("$PassArguments$", passArguments)
6464
.Replace("$EngineFactoryType$", GetEngineFactoryTypeName(benchmark))
6565
.Replace("$MeasureExtraStats$", buildInfo.Config.HasExtraStatsDiagnoser() ? "true" : "false")
66+
.Replace("$MeasureSurvivedMemory$", buildInfo.Config.HasSurvivedMemoryDiagnoser() ? "true" : "false")
6667
.Replace("$DisassemblerEntryMethodName$", DisassemblerConstants.DisassemblerEntryMethodName)
6768
.Replace("$WorkloadMethodCall$", provider.GetWorkloadMethodCall(passArguments))
6869
.RemoveRedundantIfDefines(compilationId);

src/BenchmarkDotNet/Configs/ImmutableConfig.cs

+2
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ internal ImmutableConfig(
111111

112112
public bool HasMemoryDiagnoser() => diagnosers.OfType<MemoryDiagnoser>().Any();
113113

114+
public bool HasSurvivedMemoryDiagnoser() => diagnosers.Any(diagnoser => diagnoser is MemoryDiagnoser md && md.Config.IncludeSurvived);
115+
114116
public bool HasThreadingDiagnoser() => diagnosers.Contains(ThreadingDiagnoser.Default);
115117

116118
public bool HasExceptionDiagnoser() => diagnosers.Contains(ExceptionDiagnoser.Default);

src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs

+3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ public bool UseDisassemblyDiagnoser
6262
[Option('a', "artifacts", Required = false, HelpText = "Valid path to accessible directory")]
6363
public DirectoryInfo ArtifactsDirectory { get; set; }
6464

65+
[Option("memorySurvived", Required = false, Default = false, HelpText = "Measures survived memory.")]
66+
public bool UseSurvivedMemoryDiagnoser { get; set; }
67+
6568
[Option("outliers", Required = false, Default = OutlierMode.RemoveUpper, HelpText = "DontRemove/RemoveUpper/RemoveLower/RemoveAll")]
6669
public OutlierMode Outliers { get; set; }
6770

src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,11 @@ private static IConfig CreateConfig(CommandLineOptions options, IConfig globalCo
343343
.Select(counterName => (HardwareCounter)Enum.Parse(typeof(HardwareCounter), counterName, ignoreCase: true))
344344
.ToArray());
345345

346-
if (options.UseMemoryDiagnoser)
346+
if (options.UseSurvivedMemoryDiagnoser)
347+
config.AddDiagnoser(new MemoryDiagnoser(new MemoryDiagnoserConfig(includeSurvived: true)));
348+
else if (options.UseMemoryDiagnoser)
347349
config.AddDiagnoser(MemoryDiagnoser.Default);
350+
348351
if (options.UseThreadingDiagnoser)
349352
config.AddDiagnoser(ThreadingDiagnoser.Default);
350353
if (options.UseExceptionDiagnoser)

src/BenchmarkDotNet/Diagnosers/MemoryDiagnoser.cs

+21
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using BenchmarkDotNet.Reports;
99
using BenchmarkDotNet.Running;
1010
using BenchmarkDotNet.Validators;
11+
using Perfolizer.Metrology;
1112

1213
namespace BenchmarkDotNet.Diagnosers
1314
{
@@ -42,6 +43,26 @@ public IEnumerable<Metric> ProcessResults(DiagnoserResults diagnoserResults)
4243
}
4344

4445
yield return new Metric(AllocatedMemoryMetricDescriptor.Instance, diagnoserResults.GcStats.GetBytesAllocatedPerOperation(diagnoserResults.BenchmarkCase) ?? double.NaN);
46+
47+
if (Config.IncludeSurvived)
48+
{
49+
yield return new Metric(SurvivedMemoryMetricDescriptor.Instance, diagnoserResults.GcStats.SurvivedBytes ?? double.NaN);
50+
}
51+
}
52+
53+
private class SurvivedMemoryMetricDescriptor : IMetricDescriptor
54+
{
55+
internal static readonly IMetricDescriptor Instance = new SurvivedMemoryMetricDescriptor();
56+
57+
public string Id => "Survived Memory";
58+
public string DisplayName => "Survived";
59+
public string Legend => "Memory survived after the first operation (managed only, inclusive, 1KB = 1024B)";
60+
public string NumberFormat => "N0";
61+
public UnitType UnitType => UnitType.Size;
62+
public string Unit => SizeUnit.B.Abbreviation;
63+
public bool TheGreaterTheBetter => false;
64+
public int PriorityInCategory { get; } = AllocatedMemoryMetricDescriptor.Instance.PriorityInCategory + 1;
65+
public bool GetIsAvailable(Metric metric) => true;
4566
}
4667

4768
private class GarbageCollectionsMetricDescriptor : IMetricDescriptor

src/BenchmarkDotNet/Diagnosers/MemoryDiagnoserConfig.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ namespace BenchmarkDotNet.Diagnosers
55
public class MemoryDiagnoserConfig
66
{
77
/// <param name="displayGenColumns">Display Garbage Collections per Generation columns (Gen 0, Gen 1, Gen 2). True by default.</param>
8+
/// <param name="includeSurvived">If true, monitoring will be enabled and survived memory will be measured on the first benchmark run.</param>
89
[PublicAPI]
9-
public MemoryDiagnoserConfig(bool displayGenColumns = true)
10+
public MemoryDiagnoserConfig(bool displayGenColumns = true, bool includeSurvived = false)
1011
{
1112
DisplayGenColumns = displayGenColumns;
13+
IncludeSurvived = includeSurvived;
1214
}
1315

1416
public bool DisplayGenColumns { get; }
17+
public bool IncludeSurvived { get; }
1518
}
1619
}

src/BenchmarkDotNet/Engines/Engine.cs

+31-7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class Engine : IEngine
1919

2020
[PublicAPI] public IHost Host { get; }
2121
[PublicAPI] public Action<long> WorkloadAction { get; }
22+
[PublicAPI] public Action<long> WorkloadActionNoUnroll { get; }
2223
[PublicAPI] public Action Dummy1Action { get; }
2324
[PublicAPI] public Action Dummy2Action { get; }
2425
[PublicAPI] public Action Dummy3Action { get; }
@@ -44,19 +45,22 @@ public class Engine : IEngine
4445
private readonly EnginePilotStage pilotStage;
4546
private readonly EngineWarmupStage warmupStage;
4647
private readonly EngineActualStage actualStage;
47-
private readonly bool includeExtraStats;
4848
private readonly Random random;
49+
private readonly bool includeExtraStats, includeSurvivedMemory;
50+
51+
private long? survivedBytes;
52+
private bool survivedBytesMeasured;
4953

5054
internal Engine(
5155
IHost host,
5256
IResolver resolver,
53-
Action dummy1Action, Action dummy2Action, Action dummy3Action, Action<long> overheadAction, Action<long> workloadAction, Job targetJob,
57+
Action dummy1Action, Action dummy2Action, Action dummy3Action, Action<long> overheadAction, Action<long> workloadAction, Action<long> workloadActionNoUnroll, Job targetJob,
5458
Action globalSetupAction, Action globalCleanupAction, Action iterationSetupAction, Action iterationCleanupAction, long operationsPerInvoke,
55-
bool includeExtraStats, string benchmarkName)
59+
bool includeExtraStats, bool includeSurvivedMemory, string benchmarkName)
5660
{
57-
5861
Host = host;
5962
OverheadAction = overheadAction;
63+
WorkloadActionNoUnroll = workloadActionNoUnroll;
6064
Dummy1Action = dummy1Action;
6165
Dummy2Action = dummy2Action;
6266
Dummy3Action = dummy3Action;
@@ -69,6 +73,7 @@ internal Engine(
6973
OperationsPerInvoke = operationsPerInvoke;
7074
this.includeExtraStats = includeExtraStats;
7175
BenchmarkName = benchmarkName;
76+
this.includeSurvivedMemory = includeSurvivedMemory;
7277

7378
Resolver = resolver;
7479

@@ -86,6 +91,14 @@ internal Engine(
8691
random = new Random(12345); // we are using constant seed to try to get repeatable results
8792
}
8893

94+
internal Engine WithInitialData(Engine other)
95+
{
96+
// Copy the survived bytes from the other engine so we only measure it once.
97+
survivedBytes = other.survivedBytes;
98+
survivedBytesMeasured = other.survivedBytesMeasured;
99+
return this;
100+
}
101+
89102
public void Dispose()
90103
{
91104
try
@@ -168,6 +181,17 @@ public Measurement RunIteration(IterationData data)
168181

169182
Span<byte> stackMemory = randomizeMemory ? stackalloc byte[random.Next(32)] : Span<byte>.Empty;
170183

184+
bool needsSurvivedMeasurement = includeSurvivedMemory && !isOverhead && !survivedBytesMeasured;
185+
if (needsSurvivedMeasurement && GcStats.InitTotalBytes())
186+
{
187+
// Measure survived bytes for only the first invocation.
188+
survivedBytesMeasured = true;
189+
long beforeBytes = GcStats.GetTotalBytes();
190+
WorkloadActionNoUnroll(1);
191+
long afterBytes = GcStats.GetTotalBytes();
192+
survivedBytes = afterBytes - beforeBytes;
193+
}
194+
171195
// Measure
172196
var clock = Clock.Start();
173197
action(invokeCount / unrollFactor);
@@ -218,8 +242,8 @@ public Measurement RunIteration(IterationData data)
218242
IterationCleanupAction(); // we run iteration cleanup after collecting GC stats
219243

220244
var totalOperationsCount = data.InvokeCount * OperationsPerInvoke;
221-
GcStats gcStats = (finalGcStats - initialGcStats).WithTotalOperations(totalOperationsCount);
222-
ThreadingStats threadingStats = (finalThreadingStats - initialThreadingStats).WithTotalOperations(data.InvokeCount * OperationsPerInvoke);
245+
GcStats gcStats = (finalGcStats - initialGcStats).WithTotalOperationsAndSurvivedBytes(totalOperationsCount, survivedBytes);
246+
ThreadingStats threadingStats = (finalThreadingStats - initialThreadingStats).WithTotalOperations(totalOperationsCount);
223247

224248
return (gcStats, threadingStats, exceptionsStats.ExceptionsCount / (double)totalOperationsCount);
225249
}
@@ -253,7 +277,7 @@ private void GcCollect()
253277
ForceGcCollect();
254278
}
255279

256-
private static void ForceGcCollect()
280+
internal static void ForceGcCollect()
257281
{
258282
GC.Collect();
259283
GC.WaitForPendingFinalizers();

src/BenchmarkDotNet/Engines/EngineFactory.cs

+6-2
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,12 @@ public IEngine CreateReadyToRun(EngineParameters engineParameters)
6969
.WithMinInvokeCount(2) // the minimum is 2 (not the default 4 which can be too much and not 1 which we already know is not enough)
7070
.WithEvaluateOverhead(false); // it's something very time consuming, it overhead is too small compared to total time
7171

72-
return CreateEngine(engineParameters, needsPilot, engineParameters.OverheadActionNoUnroll, engineParameters.WorkloadActionNoUnroll);
72+
return CreateEngine(engineParameters, needsPilot, engineParameters.OverheadActionNoUnroll, engineParameters.WorkloadActionNoUnroll)
73+
.WithInitialData(singleActionEngine);
7374
}
7475

75-
var multiActionEngine = CreateMultiActionEngine(engineParameters);
76+
var multiActionEngine = CreateMultiActionEngine(engineParameters)
77+
.WithInitialData(singleActionEngine);
7678

7779
DeadCodeEliminationHelper.KeepAliveWithoutBoxing(Jit(multiActionEngine, ++jitIndex, invokeCount: defaultUnrollFactor, unrollFactor: defaultUnrollFactor));
7880

@@ -118,13 +120,15 @@ private static Engine CreateEngine(EngineParameters engineParameters, Job job, A
118120
engineParameters.Dummy3Action,
119121
idle,
120122
main,
123+
engineParameters.WorkloadActionNoUnroll,
121124
job,
122125
engineParameters.GlobalSetupAction,
123126
engineParameters.GlobalCleanupAction,
124127
engineParameters.IterationSetupAction,
125128
engineParameters.IterationCleanupAction,
126129
engineParameters.OperationsPerInvoke,
127130
engineParameters.MeasureExtraStats,
131+
engineParameters.MeasureSurvivedMemory,
128132
engineParameters.BenchmarkName);
129133
}
130134
}

src/BenchmarkDotNet/Engines/EngineParameters.cs

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public class EngineParameters
2727
public Action IterationCleanupAction { get; set; }
2828
public bool MeasureExtraStats { get; set; }
2929

30+
public bool MeasureSurvivedMemory { get; set; }
31+
3032
[PublicAPI] public string BenchmarkName { get; set; }
3133

3234
public bool NeedsJitting => TargetJob.ResolveValue(RunMode.RunStrategyCharacteristic, DefaultResolver).NeedsJitting();

0 commit comments

Comments
 (0)