Skip to content

Commit 4116428

Browse files
committed
Clear from consumer.
Added survived memory tests.
1 parent c55939d commit 4116428

File tree

5 files changed

+171
-3
lines changed

5 files changed

+171
-3
lines changed

src/BenchmarkDotNet/Engines/Consumer.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ private static readonly HashSet<Type> SupportedTypes
3333
private string stringHolder;
3434
private object objectHolder;
3535

36+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
37+
[PublicAPI]
38+
public void Clear()
39+
{
40+
Volatile.Write(ref stringHolder, null);
41+
Volatile.Write(ref objectHolder, null);
42+
}
43+
3644
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3745
[PublicAPI]
3846
public void Consume(byte byteValue) => byteHolder = byteValue;

src/BenchmarkDotNet/Engines/Engine.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ private Func<long> GetTotalBytesFunc()
130130
}
131131
}
132132

133+
private static void ResetSurvived()
134+
{
135+
survivedBytes = 0;
136+
survivedBytesMeasured = false;
137+
}
138+
133139
public void Dispose()
134140
{
135141
try
@@ -284,6 +290,7 @@ private double MeasureAction(Action<long> action, long arg)
284290
GcStats gcStats = (finalGcStats - initialGcStats).WithTotalOperationsAndSurvivedBytes(data.InvokeCount * OperationsPerInvoke, survivedBytes);
285291
ThreadingStats threadingStats = (finalThreadingStats - initialThreadingStats).WithTotalOperations(data.InvokeCount * OperationsPerInvoke);
286292

293+
ResetSurvived();
287294
return (gcStats, threadingStats);
288295
}
289296

src/BenchmarkDotNet/Templates/BenchmarkType.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
{
122122
consumer.Consume(overheadDelegate($PassArguments$));@Unroll@
123123
}
124+
consumer.Clear(); // Necessary for survived memory diagnoser.
124125
}
125126

126127
private void OverheadActionNoUnroll(System.Int64 invokeCount)
@@ -130,6 +131,7 @@
130131
{
131132
consumer.Consume(overheadDelegate($PassArguments$));
132133
}
134+
consumer.Clear(); // Necessary for survived memory diagnoser.
133135
}
134136

135137
private void WorkloadActionUnroll(System.Int64 invokeCount)
@@ -139,6 +141,7 @@
139141
{
140142
consumer.Consume(workloadDelegate($PassArguments$)$ConsumeField$);@Unroll@
141143
}
144+
consumer.Clear(); // Necessary for survived memory diagnoser.
142145
}
143146

144147
private void WorkloadActionNoUnroll(System.Int64 invokeCount)
@@ -148,6 +151,7 @@
148151
{
149152
consumer.Consume(workloadDelegate($PassArguments$)$ConsumeField$);
150153
}
154+
consumer.Clear(); // Necessary for survived memory diagnoser.
151155
}
152156

153157
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]

src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/ConsumableConsumeEmitter.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,5 +122,13 @@ protected override void EmitActionAfterCallOverride(ILGenerator ilBuilder)
122122
}
123123
}
124124
}
125+
126+
protected override void EmitActionAfterLoopOverride(ILGenerator ilBuilder)
127+
{
128+
var clearMethod = typeof(Consumer).GetMethod(nameof(Consumer.Clear));
129+
ilBuilder.Emit(OpCodes.Ldarg_0);
130+
ilBuilder.Emit(OpCodes.Ldfld, consumerField);
131+
ilBuilder.Emit(OpCodes.Callvirt, clearMethod);
132+
}
125133
}
126134
}

tests/BenchmarkDotNet.IntegrationTests/MemoryDiagnoserTests.cs

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using BenchmarkDotNet.Columns;
1010
using BenchmarkDotNet.Configs;
1111
using BenchmarkDotNet.Diagnosers;
12+
using BenchmarkDotNet.Engines;
1213
using BenchmarkDotNet.Extensions;
1314
using BenchmarkDotNet.IntegrationTests.Xunit;
1415
using BenchmarkDotNet.Jobs;
@@ -19,6 +20,7 @@
1920
using BenchmarkDotNet.Tests.XUnit;
2021
using BenchmarkDotNet.Toolchains;
2122
using BenchmarkDotNet.Toolchains.CoreRt;
23+
using BenchmarkDotNet.Toolchains.CsProj;
2224
using BenchmarkDotNet.Toolchains.InProcess.Emit;
2325
using Xunit;
2426
using Xunit.Abstractions;
@@ -69,6 +71,64 @@ public void MemoryDiagnoserIsAccurate(IToolchain toolchain)
6971
});
7072
}
7173

74+
public class AccurateSurvived
75+
{
76+
[Benchmark] public byte[] EightBytesArray() => new byte[8];
77+
[Benchmark] public byte[] SixtyFourBytesArray() => new byte[64];
78+
[Benchmark] public Task<int> AllocateTask() => Task.FromResult(default(int));
79+
80+
81+
public byte[] bytes8;
82+
public byte[] bytes64;
83+
public Task<int> task;
84+
85+
[GlobalSetup(Targets = new string[] { nameof(EightBytesArrayNoAllocate), nameof(SixtyFourBytesArrayNoAllocate) })]
86+
public void SetupNoAllocate()
87+
{
88+
bytes8 = new byte[8];
89+
bytes64 = new byte[64];
90+
}
91+
92+
[Benchmark] public byte[] EightBytesArrayNoAllocate() => bytes8;
93+
[Benchmark] public byte[] SixtyFourBytesArrayNoAllocate() => bytes64;
94+
95+
96+
[Benchmark] public void EightBytesArraySurvive() => bytes8 = new byte[8];
97+
[Benchmark] public void SixtyFourBytesArraySurvive() => bytes64 = new byte[64];
98+
[Benchmark] public void AllocateTaskSurvive() => task = Task.FromResult(default(int));
99+
100+
101+
[Benchmark] public void EightBytesArrayAllocateNoSurvive() => DeadCodeEliminationHelper.KeepAliveWithoutBoxing(new byte[8]);
102+
[Benchmark] public void SixtyFourBytesArrayAllocateNoSurvive() => DeadCodeEliminationHelper.KeepAliveWithoutBoxing(new byte[64]);
103+
[Benchmark] public void TaskAllocateNoSurvive() => DeadCodeEliminationHelper.KeepAliveWithoutBoxing(Task.FromResult(default(int)));
104+
}
105+
106+
[Theory, MemberData(nameof(GetToolchains))]
107+
[Trait(Constants.Category, Constants.BackwardCompatibilityCategory)]
108+
public void MemoryDiagnoserSurvivedIsAccurate(IToolchain toolchain)
109+
{
110+
long objectAllocationOverhead = IntPtr.Size * 2; // pointer to method table + object header word
111+
long arraySizeOverhead = IntPtr.Size; // array length
112+
113+
AssertSurvived(toolchain, typeof(AccurateSurvived), new Dictionary<string, long>
114+
{
115+
{ nameof(AccurateSurvived.EightBytesArray), 0 },
116+
{ nameof(AccurateSurvived.SixtyFourBytesArray), 0 },
117+
{ nameof(AccurateSurvived.AllocateTask), 0 },
118+
119+
{ nameof(AccurateSurvived.EightBytesArrayNoAllocate), 0 },
120+
{ nameof(AccurateSurvived.SixtyFourBytesArrayNoAllocate), 0 },
121+
122+
{ nameof(AccurateSurvived.EightBytesArraySurvive), 8 + objectAllocationOverhead + arraySizeOverhead },
123+
{ nameof(AccurateSurvived.SixtyFourBytesArraySurvive), 64 + objectAllocationOverhead + arraySizeOverhead },
124+
{ nameof(AccurateSurvived.AllocateTaskSurvive), CalculateRequiredSpace<Task<int>>() },
125+
126+
{ nameof(AccurateSurvived.EightBytesArrayAllocateNoSurvive), 0 },
127+
{ nameof(AccurateSurvived.SixtyFourBytesArrayAllocateNoSurvive), 0 },
128+
{ nameof(AccurateSurvived.TaskAllocateNoSurvive), 0 },
129+
});
130+
}
131+
72132
public class AllocatingGlobalSetupAndCleanup
73133
{
74134
private List<int> list;
@@ -102,6 +162,16 @@ public void MemoryDiagnoserDoesNotIncludeAllocationsFromSetupAndCleanup(IToolcha
102162
});
103163
}
104164

165+
[Theory, MemberData(nameof(GetToolchains))]
166+
[Trait(Constants.Category, Constants.BackwardCompatibilityCategory)]
167+
public void MemoryDiagnoserDoesNotIncludeSurvivedFromSetupAndCleanup(IToolchain toolchain)
168+
{
169+
AssertSurvived(toolchain, typeof(AllocatingGlobalSetupAndCleanup), new Dictionary<string, long>
170+
{
171+
{ nameof(AllocatingGlobalSetupAndCleanup.AllocateNothing), 0 }
172+
});
173+
}
174+
105175
public class NoAllocationsAtAll
106176
{
107177
[Benchmark] public void EmptyMethod() { }
@@ -117,6 +187,16 @@ public void EngineShouldNotInterfereAllocationResults(IToolchain toolchain)
117187
});
118188
}
119189

190+
[Theory, MemberData(nameof(GetToolchains))]
191+
[Trait(Constants.Category, Constants.BackwardCompatibilityCategory)]
192+
public void EngineShouldNotInterfereSurvivedResults(IToolchain toolchain)
193+
{
194+
AssertSurvived(toolchain, typeof(NoAllocationsAtAll), new Dictionary<string, long>
195+
{
196+
{ nameof(NoAllocationsAtAll.EmptyMethod), 0 }
197+
});
198+
}
199+
120200
public class NoBoxing
121201
{
122202
[Benchmark] public ValueTuple<int> ReturnsValueType() => new ValueTuple<int>(0);
@@ -132,10 +212,29 @@ public void EngineShouldNotIntroduceBoxing(IToolchain toolchain)
132212
});
133213
}
134214

215+
[Theory, MemberData(nameof(GetToolchains))]
216+
[Trait(Constants.Category, Constants.BackwardCompatibilityCategory)]
217+
public void EngineShouldNotIntroduceBoxingSurvived(IToolchain toolchain)
218+
{
219+
AssertSurvived(toolchain, typeof(NoBoxing), new Dictionary<string, long>
220+
{
221+
{ nameof(NoBoxing.ReturnsValueType), 0 }
222+
});
223+
}
224+
135225
public class NonAllocatingAsynchronousBenchmarks
136226
{
137227
private readonly Task<int> completedTaskOfT = Task.FromResult(default(int)); // we store it in the field, because Task<T> is reference type so creating it allocates heap memory
138228

229+
[GlobalSetup]
230+
public void Setup()
231+
{
232+
// Run once to set static memory.
233+
DeadCodeEliminationHelper.KeepAliveWithoutBoxing(CompletedTask());
234+
DeadCodeEliminationHelper.KeepAliveWithoutBoxing(CompletedTaskOfT());
235+
DeadCodeEliminationHelper.KeepAliveWithoutBoxing(CompletedValueTaskOfT());
236+
}
237+
139238
[Benchmark] public Task CompletedTask() => Task.CompletedTask;
140239

141240
[Benchmark] public Task<int> CompletedTaskOfT() => completedTaskOfT;
@@ -155,6 +254,18 @@ public void AwaitingTasksShouldNotInterfereAllocationResults(IToolchain toolchai
155254
});
156255
}
157256

257+
[Theory, MemberData(nameof(GetToolchains))]
258+
[Trait(Constants.Category, Constants.BackwardCompatibilityCategory)]
259+
public void AwaitingTasksShouldNotInterfereSurvivedResults(IToolchain toolchain)
260+
{
261+
AssertSurvived(toolchain, typeof(NonAllocatingAsynchronousBenchmarks), new Dictionary<string, long>
262+
{
263+
{ nameof(NonAllocatingAsynchronousBenchmarks.CompletedTask), 0 },
264+
{ nameof(NonAllocatingAsynchronousBenchmarks.CompletedTaskOfT), 0 },
265+
{ nameof(NonAllocatingAsynchronousBenchmarks.CompletedValueTaskOfT), 0 }
266+
});
267+
}
268+
158269
public class WithOperationsPerInvokeBenchmarks
159270
{
160271
[Benchmark(OperationsPerInvoke = 4)]
@@ -257,7 +368,7 @@ public void MemoryDiagnoserIsAccurateForMultiThreadedBenchmarks(IToolchain toolc
257368

258369
private void AssertAllocations(IToolchain toolchain, Type benchmarkType, Dictionary<string, long> benchmarksAllocationsValidators)
259370
{
260-
var config = CreateConfig(toolchain);
371+
var config = CreateConfig(toolchain, MemoryDiagnoser.Default);
261372
var benchmarks = BenchmarkConverter.TypeToBenchmarks(benchmarkType, config);
262373

263374
var summary = BenchmarkRunner.Run(benchmarks);
@@ -285,7 +396,37 @@ private void AssertAllocations(IToolchain toolchain, Type benchmarkType, Diction
285396
}
286397
}
287398

288-
private IConfig CreateConfig(IToolchain toolchain)
399+
private void AssertSurvived(IToolchain toolchain, Type benchmarkType, Dictionary<string, long> benchmarkSurvivedValidators)
400+
{
401+
// Core has survived memory measurement problems.
402+
// See https://github.com/dotnet/runtime/issues/45446
403+
if (toolchain is CsProjCoreToolchain || toolchain is CoreRtToolchain) // CoreRt actually does measure accurately in a normal benchmark run, but doesn't with the specific version used in these tests.
404+
return;
405+
406+
var config = CreateConfig(toolchain, MemoryDiagnoser.WithSurvived);
407+
var benchmarks = BenchmarkConverter.TypeToBenchmarks(benchmarkType, config);
408+
409+
var summary = BenchmarkRunner.Run(benchmarks);
410+
411+
foreach (var benchmarkSurvivedValidator in benchmarkSurvivedValidators)
412+
{
413+
// CoreRT is missing some of the CoreCLR threading/task related perf improvements, so sizeof(Task<int>) calculated for CoreCLR < sizeof(Task<int>) on CoreRT
414+
// see https://github.com/dotnet/corert/issues/5705 for more
415+
if (benchmarkSurvivedValidator.Key == nameof(AccurateSurvived.AllocateTaskSurvive) && toolchain is CoreRtToolchain)
416+
continue;
417+
418+
var survivedBenchmarks = benchmarks.BenchmarksCases.Where(benchmark => benchmark.Descriptor.WorkloadMethodDisplayInfo == benchmarkSurvivedValidator.Key).ToArray();
419+
420+
foreach (var benchmark in survivedBenchmarks)
421+
{
422+
var benchmarkReport = summary.Reports.Single(report => report.BenchmarkCase == benchmark);
423+
424+
Assert.Equal(benchmarkSurvivedValidator.Value, benchmarkReport.GcStats.SurvivedBytes);
425+
}
426+
}
427+
}
428+
429+
private IConfig CreateConfig(IToolchain toolchain, MemoryDiagnoser memoryDiagnoser)
289430
=> ManualConfig.CreateEmpty()
290431
.AddJob(Job.ShortRun
291432
.WithEvaluateOverhead(false) // no need to run idle for this test
@@ -294,7 +435,7 @@ private IConfig CreateConfig(IToolchain toolchain)
294435
.WithGcForce(false)
295436
.WithToolchain(toolchain))
296437
.AddColumnProvider(DefaultColumnProviders.Instance)
297-
.AddDiagnoser(MemoryDiagnoser.Default)
438+
.AddDiagnoser(memoryDiagnoser)
298439
.AddLogger(toolchain.IsInProcess ? ConsoleLogger.Default : new OutputLogger(output)); // we can't use OutputLogger for the InProcess toolchains because it allocates memory on the same thread
299440

300441
// note: don't copy, never use in production systems (it should work but I am not 100% sure)

0 commit comments

Comments
 (0)