Skip to content

Commit d5f7b9f

Browse files
authored
Memory Randomization (#1587)
* add initial implementation of experimental mode that allocates random-size array between iterations and calls global setup after it * don't remove outliers if Memory Randomization is enabled * introduce an attribute that allows to enable MemoryRandomization per class * rename ForceAllocations to ForceGcCleanups to make it clear what it does * add a call to [GlobalCleanup] before [GlobalSetu] allocate sth on LOH too * add stack memory randomization * keep the stack memory alive for the iteration period
1 parent aef9cbe commit d5f7b9f

File tree

8 files changed

+113
-4
lines changed

8 files changed

+113
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using BenchmarkDotNet.Attributes;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
8+
namespace BenchmarkDotNet.Samples
9+
{
10+
public class IntroMemoryRandomization
11+
{
12+
[Params(512 * 4)]
13+
public int Size;
14+
15+
private int[] _array;
16+
private int[] _destination;
17+
18+
[GlobalSetup]
19+
public void Setup()
20+
{
21+
_array = new int[Size];
22+
_destination = new int[Size];
23+
}
24+
25+
[Benchmark]
26+
public void Array() => System.Array.Copy(_array, _destination, Size);
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using BenchmarkDotNet.Jobs;
2+
using Perfolizer.Mathematics.OutlierDetection;
3+
4+
namespace BenchmarkDotNet.Attributes
5+
{
6+
/// <summary>
7+
/// specifies whether Engine should allocate some random-sized memory between iterations
8+
/// <remarks>it makes [GlobalCleanup] and [GlobalSetup] methods to be executed after every iteration</remarks>
9+
/// </summary>
10+
public class MemoryRandomizationAttribute : JobMutatorConfigBaseAttribute
11+
{
12+
public MemoryRandomizationAttribute(bool enable = true, OutlierMode outlierMode = OutlierMode.DontRemove)
13+
: base(Job.Default.WithMemoryRandomization(enable).WithOutlierMode(outlierMode))
14+
{
15+
}
16+
}
17+
}

src/BenchmarkDotNet/ConsoleArguments/CommandLineOptions.cs

+3
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ public class CommandLineOptions
168168
[Option("envVars", Required = false, HelpText = "Colon separated environment variables (key:value)")]
169169
public IEnumerable<string> EnvironmentVariables { get; set; }
170170

171+
[Option("memoryRandomization", Required = false, HelpText = "Specifies whether Engine should allocate some random-sized memory between iterations. It makes [GlobalCleanup] and [GlobalSetup] methods to be executed after every iteration.")]
172+
public bool MemoryRandomization { get; set; }
173+
171174
[Option("wasmEngine", Required = false, HelpText = "Full path to a java script engine used to run the benchmarks, used by Wasm toolchain.")]
172175
public FileInfo WasmJavascriptEngine { get; set; }
173176

src/BenchmarkDotNet/ConsoleArguments/ConfigParser.cs

+2
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ private static Job GetBaseJob(CommandLineOptions options, IConfig globalConfig)
281281
baseJob = baseJob.WithPlatform(options.Platform.Value);
282282
if (options.RunOncePerIteration)
283283
baseJob = baseJob.RunOncePerIteration();
284+
if (options.MemoryRandomization)
285+
baseJob = baseJob.WithMemoryRandomization();
284286

285287
if (options.EnvironmentVariables.Any())
286288
{

src/BenchmarkDotNet/Engines/Engine.cs

+39-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.Globalization;
45
using System.Linq;
6+
using System.Runtime.CompilerServices;
57
using BenchmarkDotNet.Characteristics;
68
using BenchmarkDotNet.Jobs;
79
using BenchmarkDotNet.Portability;
@@ -33,16 +35,18 @@ public class Engine : IEngine
3335
[PublicAPI] public string BenchmarkName { get; }
3436

3537
private IClock Clock { get; }
36-
private bool ForceAllocations { get; }
38+
private bool ForceGcCleanups { get; }
3739
private int UnrollFactor { get; }
3840
private RunStrategy Strategy { get; }
3941
private bool EvaluateOverhead { get; }
4042
private int InvocationCount { get; }
43+
private bool MemoryRandomization { get; }
4144

4245
private readonly EnginePilotStage pilotStage;
4346
private readonly EngineWarmupStage warmupStage;
4447
private readonly EngineActualStage actualStage;
4548
private readonly bool includeExtraStats;
49+
private readonly Random random;
4650

4751
internal Engine(
4852
IHost host,
@@ -70,15 +74,18 @@ internal Engine(
7074
Resolver = resolver;
7175

7276
Clock = targetJob.ResolveValue(InfrastructureMode.ClockCharacteristic, Resolver);
73-
ForceAllocations = targetJob.ResolveValue(GcMode.ForceCharacteristic, Resolver);
77+
ForceGcCleanups = targetJob.ResolveValue(GcMode.ForceCharacteristic, Resolver);
7478
UnrollFactor = targetJob.ResolveValue(RunMode.UnrollFactorCharacteristic, Resolver);
7579
Strategy = targetJob.ResolveValue(RunMode.RunStrategyCharacteristic, Resolver);
7680
EvaluateOverhead = targetJob.ResolveValue(AccuracyMode.EvaluateOverheadCharacteristic, Resolver);
7781
InvocationCount = targetJob.ResolveValue(RunMode.InvocationCountCharacteristic, Resolver);
82+
MemoryRandomization = targetJob.ResolveValue(RunMode.MemoryRandomizationCharacteristic, Resolver);
7883

7984
warmupStage = new EngineWarmupStage(this);
8085
pilotStage = new EnginePilotStage(this);
8186
actualStage = new EngineActualStage(this);
87+
88+
random = new Random(12345); // we are using constant seed to try to get repeatable results
8289
}
8390

8491
public void Dispose()
@@ -146,6 +153,7 @@ public Measurement RunIteration(IterationData data)
146153
int unrollFactor = data.UnrollFactor;
147154
long totalOperations = invokeCount * OperationsPerInvoke;
148155
bool isOverhead = data.IterationMode == IterationMode.Overhead;
156+
bool randomizeMemory = !isOverhead && MemoryRandomization;
149157
var action = isOverhead ? OverheadAction : WorkloadAction;
150158

151159
if (!isOverhead)
@@ -156,6 +164,8 @@ public Measurement RunIteration(IterationData data)
156164
if (EngineEventSource.Log.IsEnabled())
157165
EngineEventSource.Log.IterationStart(data.IterationMode, data.IterationStage, totalOperations);
158166

167+
Span<byte> stackMemory = randomizeMemory ? stackalloc byte[random.Next(32)] : Span<byte>.Empty;
168+
159169
// Measure
160170
var clock = Clock.Start();
161171
action(invokeCount / unrollFactor);
@@ -167,12 +177,17 @@ public Measurement RunIteration(IterationData data)
167177
if (!isOverhead)
168178
IterationCleanupAction();
169179

180+
if (randomizeMemory)
181+
RandomizeManagedHeapMemory();
182+
170183
GcCollect();
171184

172185
// Results
173186
var measurement = new Measurement(0, data.IterationMode, data.IterationStage, data.Index, totalOperations, clockSpan.GetNanoseconds());
174187
WriteLine(measurement.ToString());
175188

189+
Consume(stackMemory);
190+
176191
return measurement;
177192
}
178193

@@ -201,9 +216,30 @@ public Measurement RunIteration(IterationData data)
201216
return (gcStats, threadingStats);
202217
}
203218

219+
[MethodImpl(MethodImplOptions.NoInlining)]
220+
private void Consume(in Span<byte> _) { }
221+
222+
private void RandomizeManagedHeapMemory()
223+
{
224+
// invoke global cleanup before global setup
225+
GlobalCleanupAction?.Invoke();
226+
227+
var gen0object = new byte[random.Next(32)];
228+
var lohObject = new byte[85 * 1024 + random.Next(32)];
229+
230+
// we expect the key allocations to happen in global setup (not ctor)
231+
// so we call it while keeping the random-size objects alive
232+
GlobalSetupAction?.Invoke();
233+
234+
GC.KeepAlive(gen0object);
235+
GC.KeepAlive(lohObject);
236+
237+
// we don't enforce GC.Collects here as engine does it later anyway
238+
}
239+
204240
private void GcCollect()
205241
{
206-
if (!ForceAllocations)
242+
if (!ForceGcCleanups)
207243
return;
208244

209245
ForceGcCollect();

src/BenchmarkDotNet/Engines/EngineResolver.cs

+7
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,15 @@ private EngineResolver()
3333
Register(AccuracyMode.MinIterationTimeCharacteristic, () => TimeInterval.Millisecond * 500);
3434
Register(AccuracyMode.MinInvokeCountCharacteristic, () => 4);
3535
Register(AccuracyMode.EvaluateOverheadCharacteristic, () => true);
36+
Register(RunMode.MemoryRandomizationCharacteristic, () => false);
3637
Register(AccuracyMode.OutlierModeCharacteristic, job =>
3738
{
39+
// if Memory Randomization was enabled and the benchmark is truly multimodal
40+
// removing outliers could remove some values that are not actually outliers
41+
// see https://github.com/dotnet/BenchmarkDotNet/pull/1587#issue-516837573 for example
42+
if (job.ResolveValue(RunMode.MemoryRandomizationCharacteristic, this))
43+
return OutlierMode.DontRemove;
44+
3845
var strategy = job.ResolveValue(RunMode.RunStrategyCharacteristic, this);
3946
switch (strategy)
4047
{

src/BenchmarkDotNet/Jobs/JobExtensions.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,17 @@ public static Job WithHeapAffinitizeMask(this Job job, int heapAffinitizeMask) =
210210
/// </summary>
211211
public static Job WithPowerPlan(this Job job, Guid powerPlanGuid) => job.WithCore(j => j.Environment.PowerPlanMode = powerPlanGuid);
212212

213-
214213
/// <summary>
215214
/// ensures that BenchmarkDotNet does not enforce any power plan
216215
/// </summary>
217216
public static Job DontEnforcePowerPlan(this Job job) => job.WithCore(j => j.Environment.PowerPlanMode = Guid.Empty);
218217

218+
/// <summary>
219+
/// specifies whether Engine should allocate some random-sized memory between iterations
220+
/// <remarks>it makes [GlobalCleanup] and [GlobalSetup] methods to be executed after every iteration</remarks>
221+
/// </summary>
222+
public static Job WithMemoryRandomization(this Job job, bool enable = true) => job.WithCore(j => j.Run.MemoryRandomization = enable);
223+
219224
// Infrastructure
220225
[EditorBrowsable(EditorBrowsableState.Never)]
221226
[Obsolete("This method will soon be removed, please start using .WithToolchain instead")]

src/BenchmarkDotNet/Jobs/RunMode.cs

+11
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public sealed class RunMode : JobMode<RunMode>
2121
public static readonly Characteristic<int> WarmupCountCharacteristic = CreateCharacteristic<int>(nameof(WarmupCount));
2222
public static readonly Characteristic<int> MinWarmupIterationCountCharacteristic = CreateCharacteristic<int>(nameof(MinWarmupIterationCount));
2323
public static readonly Characteristic<int> MaxWarmupIterationCountCharacteristic = CreateCharacteristic<int>(nameof(MaxWarmupIterationCount));
24+
public static readonly Characteristic<bool> MemoryRandomizationCharacteristic = CreateCharacteristic<bool>(nameof(MemoryRandomization));
2425

2526
public static readonly RunMode Dry = new RunMode(nameof(Dry))
2627
{
@@ -180,5 +181,15 @@ public int MaxWarmupIterationCount
180181
get { return MaxWarmupIterationCountCharacteristic[this]; }
181182
set { MaxWarmupIterationCountCharacteristic[this] = value; }
182183
}
184+
185+
/// <summary>
186+
/// specifies whether Engine should allocate some random-sized memory between iterations
187+
/// <remarks>it makes [GlobalCleanup] and [GlobalSetup] methods to be executed after every iteration</remarks>
188+
/// </summary>
189+
public bool MemoryRandomization
190+
{
191+
get => MemoryRandomizationCharacteristic[this];
192+
set => MemoryRandomizationCharacteristic[this] = value;
193+
}
183194
}
184195
}

0 commit comments

Comments
 (0)