Skip to content

Commit 095975f

Browse files
Handle addresses larger than 2GB for 32-bit benchmarks #1469 (#2145)
* Handle addresses larger than 2GB for 32-bit benchmarks #1469 * Apply suggestions from code review Co-authored-by: Adam Sitnik <[email protected]>
1 parent 1f5637c commit 095975f

File tree

11 files changed

+250
-17
lines changed

11 files changed

+250
-17
lines changed

docs/articles/configs/jobs.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ It's a single string characteristic. It allows to name your job. This name will
3333
* `CpuGroups`: Specifies whether garbage collection supports multiple CPU groups
3434
* `Force`: Specifies whether the BenchmarkDotNet's benchmark runner forces full garbage collection after each benchmark invocation
3535
* `AllowVeryLargeObjects`: On 64-bit platforms, enables arrays that are greater than 2 gigabytes (GB) in total size
36+
* `LargeAddressAware`: Specifies that benchmark can handle addresses larger than 2 gigabytes. See also: @BenchmarkDotNet.Samples.IntroLargeAddressAware and [LARGEADDRESSAWARE](https://learn.microsoft.com/cpp/build/reference/largeaddressaware-handle-large-addresses)
37+
* `false`: Benchmark uses the defaults (64-bit: enabled; 32-bit: disabled).
38+
* `true`: Explicitly specify that benchmark can handle addresses larger than 2 gigabytes.
3639
* `EnvironmentVariables`: customized environment variables for target benchmark. See also: @BenchmarkDotNet.Samples.IntroEnvVars
3740

3841
BenchmarkDotNet will use host process environment characteristics for non specified values.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
uid: BenchmarkDotNet.Samples.IntroLargeAddressAware
3+
title: "Sample: IntroLargeAddressAware"
4+
---
5+
6+
## Sample: IntroLargeAddressAware
7+
8+
### Source code
9+
10+
[!code-csharp[IntroLargeAddressAware.cs](../../../samples/BenchmarkDotNet.Samples/IntroLargeAddressAware.cs)]
11+
12+
### Links
13+
14+
* The permanent link to this sample: @BenchmarkDotNet.Samples.IntroLargeAddressAware
15+
16+
---

docs/articles/samples/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
href: IntroJobBaseline.md
6565
- name: IntroJoin
6666
href: IntroJoin.md
67+
- name: IntroLargeAddressAware
68+
href: IntroLargeAddressAware.md
6769
- name: IntroMonitoring
6870
href: IntroMonitoring.md
6971
- name: IntroMultimodal
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System;
2+
using BenchmarkDotNet.Attributes;
3+
using BenchmarkDotNet.Configs;
4+
using BenchmarkDotNet.Environments;
5+
using BenchmarkDotNet.Jobs;
6+
7+
namespace BenchmarkDotNet.Samples
8+
{
9+
[MemoryDiagnoser]
10+
[Config(typeof(Config))]
11+
public class IntroLargeAddressAware
12+
{
13+
private class Config : ManualConfig
14+
{
15+
public Config()
16+
{
17+
AddJob(Job.Default
18+
.WithRuntime(ClrRuntime.Net462)
19+
.WithPlatform(Platform.X86)
20+
.WithLargeAddressAware()
21+
.WithId("Framework"));
22+
}
23+
}
24+
25+
[Benchmark]
26+
public void AllocateMoreThan2GB()
27+
{
28+
const int oneGB = 1024 * 1024 * 1024;
29+
const int halfGB = oneGB / 2;
30+
byte[] bytes1 = new byte[oneGB];
31+
byte[] bytes2 = new byte[oneGB];
32+
byte[] bytes3 = new byte[halfGB];
33+
GC.KeepAlive(bytes1);
34+
GC.KeepAlive(bytes2);
35+
GC.KeepAlive(bytes3);
36+
}
37+
}
38+
}

src/BenchmarkDotNet/Jobs/EnvironmentMode.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public sealed class EnvironmentMode : JobMode<EnvironmentMode>
1616
public static readonly Characteristic<GcMode> GcCharacteristic = CreateCharacteristic<GcMode>(nameof(Gc));
1717
public static readonly Characteristic<IReadOnlyList<EnvironmentVariable>> EnvironmentVariablesCharacteristic = CreateCharacteristic<IReadOnlyList<EnvironmentVariable>>(nameof(EnvironmentVariables));
1818
public static readonly Characteristic<Guid?> PowerPlanModeCharacteristic = CreateCharacteristic<Guid?>(nameof(PowerPlanMode));
19+
public static readonly Characteristic<bool> LargeAddressAwareCharacteristic = CreateCharacteristic<bool>(nameof(LargeAddressAware));
1920

2021
public static readonly EnvironmentMode LegacyJitX86 = new EnvironmentMode(nameof(LegacyJitX86), Jit.LegacyJit, Platform.X86).Freeze();
2122
public static readonly EnvironmentMode LegacyJitX64 = new EnvironmentMode(nameof(LegacyJitX64), Jit.LegacyJit, Platform.X64).Freeze();
@@ -96,6 +97,25 @@ public Guid? PowerPlanMode
9697
set => PowerPlanModeCharacteristic[this] = value;
9798
}
9899

100+
/// <summary>
101+
/// Specifies that benchmark can handle addresses larger than 2 gigabytes.
102+
/// <value>false: Benchmark uses the default (64-bit: enabled; 32-bit:disabled). This is the default.</value>
103+
/// <value>true: Explicitly specify that benchmark can handle addresses larger than 2 gigabytes.</value>
104+
/// </summary>
105+
public bool LargeAddressAware
106+
{
107+
get => LargeAddressAwareCharacteristic[this];
108+
set
109+
{
110+
if (!RuntimeInformation.IsWindows())
111+
{
112+
throw new NotSupportedException("LargeAddressAware is a Windows-specific concept.");
113+
}
114+
115+
LargeAddressAwareCharacteristic[this] = value;
116+
}
117+
}
118+
99119
/// <summary>
100120
/// Adds the specified <paramref name="variable"/> to <see cref="EnvironmentVariables"/>.
101121
/// If <see cref="EnvironmentVariables"/> already contains a variable with the same key,

src/BenchmarkDotNet/Jobs/JobExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ public static class JobExtensions
7676
/// </summary>
7777
public static Job WithGcAllowVeryLargeObjects(this Job job, bool value) => job.WithCore(j => j.Environment.Gc.AllowVeryLargeObjects = value);
7878

79+
/// <summary>
80+
/// Specifies that benchmark can handle addresses larger than 2 gigabytes.
81+
/// </summary>
82+
public static Job WithLargeAddressAware(this Job job, bool value = true) => job.WithCore(j => j.Environment.LargeAddressAware = value);
83+
7984
/// <summary>
8085
/// Put segments that should be deleted on a standby list for future use instead of releasing them back to the OS
8186
/// <remarks>The default is false</remarks>

src/BenchmarkDotNet/Running/BenchmarkPartitioner.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public bool Equals(BenchmarkCase x, BenchmarkCase y)
4141
return false;
4242
if (jobX.Environment.Platform != jobY.Environment.Platform) // platform is set in .csproj
4343
return false;
44+
if (jobX.Environment.LargeAddressAware != jobY.Environment.LargeAddressAware)
45+
return false;
4446
if (AreDifferent(jobX.Infrastructure.BuildConfiguration, jobY.Infrastructure.BuildConfiguration)) // Debug vs Release
4547
return false;
4648
if (AreDifferent(jobX.Infrastructure.Arguments, jobY.Infrastructure.Arguments)) // arguments can be anything (Mono runtime settings or MsBuild parameters)
@@ -74,6 +76,7 @@ public int GetHashCode(BenchmarkCase obj)
7476
hashCode ^= obj.Descriptor.Type.Assembly.Location.GetHashCode();
7577
hashCode ^= (int)job.Environment.Jit;
7678
hashCode ^= (int)job.Environment.Platform;
79+
hashCode ^= job.Environment.LargeAddressAware.GetHashCode();
7780
hashCode ^= job.Environment.Gc.GetHashCode();
7881

7982
if (job.Infrastructure.BuildConfiguration != null)

src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliBuilder.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ public DotNetCliBuilder(string targetFrameworkMoniker, string customDotNetCliPat
2626
}
2727

2828
public BuildResult Build(GenerateResult generateResult, BuildPartition buildPartition, ILogger logger)
29-
=> new DotNetCliCommand(
29+
{
30+
BuildResult buildResult = new DotNetCliCommand(
3031
CustomDotNetCliPath,
3132
string.Empty,
3233
generateResult,
@@ -37,5 +38,12 @@ public BuildResult Build(GenerateResult generateResult, BuildPartition buildPart
3738
logOutput: LogOutput,
3839
retryFailedBuildWithNoDeps: RetryFailedBuildWithNoDeps)
3940
.RestoreThenBuild();
41+
if (buildResult.IsBuildSuccess &&
42+
buildPartition.RepresentativeBenchmarkCase.Job.Environment.LargeAddressAware)
43+
{
44+
LargeAddressAware.SetLargeAddressAware(generateResult.ArtifactsPaths.ExecutablePath);
45+
}
46+
return buildResult;
47+
}
4048
}
4149
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System;
2+
using System.IO;
3+
4+
namespace BenchmarkDotNet.Toolchains
5+
{
6+
// From https://github.com/KirillOsenkov/LargeAddressAware/blob/95e5fc6024438f94325df4bac6ef73c77bf90e71/SetLargeAddressAware/LargeAddressAware.cs
7+
internal class LargeAddressAware
8+
{
9+
10+
public static void SetLargeAddressAware(string filePath)
11+
{
12+
PrepareStream(filePath, (stream, binaryReader) =>
13+
{
14+
var value = binaryReader.ReadInt16();
15+
if ((value & 0x20) == 0)
16+
{
17+
value = (short)(value | 0x20);
18+
stream.Position -= 2;
19+
var binaryWriter = new BinaryWriter(stream);
20+
binaryWriter.Write(value);
21+
binaryWriter.Flush();
22+
}
23+
});
24+
}
25+
26+
private static void PrepareStream(string filePath, Action<Stream, BinaryReader> action)
27+
{
28+
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
29+
{
30+
if (stream.Length < 0x3C)
31+
{
32+
return;
33+
}
34+
35+
var binaryReader = new BinaryReader(stream);
36+
37+
// MZ header
38+
if (binaryReader.ReadInt16() != 0x5A4D)
39+
{
40+
return;
41+
}
42+
43+
stream.Position = 0x3C;
44+
var peHeaderLocation = binaryReader.ReadInt32();
45+
46+
stream.Position = peHeaderLocation;
47+
48+
// PE header
49+
if (binaryReader.ReadInt32() != 0x4550)
50+
{
51+
return;
52+
}
53+
54+
stream.Position += 0x12;
55+
56+
action(stream, binaryReader);
57+
}
58+
}
59+
}
60+
}

src/BenchmarkDotNet/Toolchains/Roslyn/Builder.cs

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using JetBrains.Annotations;
1313
using Microsoft.CodeAnalysis;
1414
using Microsoft.CodeAnalysis.CSharp;
15+
using Microsoft.CodeAnalysis.Emit;
1516
using OurPlatform = BenchmarkDotNet.Environments.Platform;
1617

1718
namespace BenchmarkDotNet.Toolchains.Roslyn
@@ -70,17 +71,17 @@ private BuildResult Build(GenerateResult generateResult, BuildPartition buildPar
7071
.Select(uniqueMetadata => uniqueMetadata.GetReference())
7172
.ToList();
7273

73-
var (result, missingReferences) = Build(generateResult, syntaxTree, compilationOptions, references, cancellationToken);
74+
var (result, missingReferences) = Build(generateResult, buildPartition, syntaxTree, compilationOptions, references, cancellationToken);
7475

7576
if (result.IsBuildSuccess || !missingReferences.Any())
7677
return result;
7778

7879
var withMissingReferences = references.Union(missingReferences.Select(assemblyMetadata => assemblyMetadata.GetReference()));
7980

80-
return Build(generateResult, syntaxTree, compilationOptions, withMissingReferences, cancellationToken).result;
81+
return Build(generateResult, buildPartition, syntaxTree, compilationOptions, withMissingReferences, cancellationToken).result;
8182
}
8283

83-
private static (BuildResult result, AssemblyMetadata[] missingReference) Build(GenerateResult generateResult, SyntaxTree syntaxTree,
84+
private static (BuildResult result, AssemblyMetadata[] missingReference) Build(GenerateResult generateResult, BuildPartition buildPartition, SyntaxTree syntaxTree,
8485
CSharpCompilationOptions compilationOptions, IEnumerable<PortableExecutableReference> references, CancellationToken cancellationToken)
8586
{
8687
var compilation = CSharpCompilation
@@ -89,25 +90,31 @@ private static (BuildResult result, AssemblyMetadata[] missingReference) Build(G
8990
.WithOptions(compilationOptions)
9091
.AddReferences(references);
9192

93+
EmitResult emitResult;
9294
using (var executable = File.Create(generateResult.ArtifactsPaths.ExecutablePath))
9395
{
94-
var emitResult = compilation.Emit(executable, cancellationToken: cancellationToken);
95-
96-
if (emitResult.Success)
97-
return (BuildResult.Success(generateResult), default);
96+
emitResult = compilation.Emit(executable, cancellationToken: cancellationToken);
97+
}
98+
if (emitResult.Success)
99+
{
100+
if (buildPartition.RepresentativeBenchmarkCase.Job.Environment.LargeAddressAware)
101+
{
102+
LargeAddressAware.SetLargeAddressAware(generateResult.ArtifactsPaths.ExecutablePath);
103+
}
104+
return (BuildResult.Success(generateResult), default);
105+
}
98106

99-
var compilationErrors = emitResult.Diagnostics
100-
.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error)
101-
.ToArray();
107+
var compilationErrors = emitResult.Diagnostics
108+
.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error)
109+
.ToArray();
102110

103-
var errors = new StringBuilder("The build has failed!").AppendLine();
104-
foreach (var diagnostic in compilationErrors)
105-
errors.AppendLine($"{diagnostic.Id}: {diagnostic.GetMessage(CultureInfo.InvariantCulture)}");
111+
var errors = new StringBuilder("The build has failed!").AppendLine();
112+
foreach (var diagnostic in compilationErrors)
113+
errors.AppendLine($"{diagnostic.Id}: {diagnostic.GetMessage(CultureInfo.InvariantCulture)}");
106114

107-
var missingReferences = GetMissingReferences(compilationErrors);
115+
var missingReferences = GetMissingReferences(compilationErrors);
108116

109-
return (BuildResult.Failure(generateResult, errors.ToString()), missingReferences);
110-
}
117+
return (BuildResult.Failure(generateResult, errors.ToString()), missingReferences);
111118
}
112119

113120
private Platform GetPlatform(OurPlatform platform)

0 commit comments

Comments
 (0)