Skip to content

Commit 9ac7770

Browse files
authored
Ref readonly support (#1389)
* add tests * bump the language version to be able to compile in parameters * emit the "in" modifier when needed * add more test cases to make sure we differentiate readonly ref return and readonly struct return and their combination * implement ref readonly returning benchmarks support * fix a bug with nested byref type names * set LangVersion when needed (and hopefully fix Travis failure)
1 parent 54a0610 commit 9ac7770

File tree

10 files changed

+164
-18
lines changed

10 files changed

+164
-18
lines changed

src/BenchmarkDotNet/Code/CodeGenerator.cs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ internal static string Generate(BuildPartition buildPartition)
4747
.Replace("$WorkloadTypeName$", provider.WorkloadTypeName)
4848
.Replace("$WorkloadMethodDelegate$", provider.WorkloadMethodDelegate(passArguments))
4949
.Replace("$WorkloadMethodReturnType$", provider.WorkloadMethodReturnTypeName)
50+
.Replace("$WorkloadMethodReturnTypeModifiers$", provider.WorkloadMethodReturnTypeModifiers)
5051
.Replace("$OverheadMethodReturnTypeName$", provider.OverheadMethodReturnTypeName)
5152
.Replace("$GlobalSetupMethodName$", provider.GlobalSetupMethodName)
5253
.Replace("$GlobalCleanupMethodName$", provider.GlobalCleanupMethodName)
@@ -61,7 +62,6 @@ internal static string Generate(BuildPartition buildPartition)
6162
.Replace("$InitializeArgumentFields$", GetInitializeArgumentFields(benchmark)).Replace("$LoadArguments$", GetLoadArguments(benchmark))
6263
.Replace("$PassArguments$", passArguments)
6364
.Replace("$EngineFactoryType$", GetEngineFactoryTypeName(benchmark))
64-
.Replace("$Ref$", provider.UseRefKeyword ? "ref" : null)
6565
.Replace("$MeasureExtraStats$", buildInfo.Config.HasExtraStatsDiagnoser() ? "true" : "false")
6666
.Replace("$DisassemblerEntryMethodName$", DisassemblerConstants.DisassemblerEntryMethodName)
6767
.Replace("$WorkloadMethodCall$", provider.GetWorkloadMethodCall(passArguments)).ToString();
@@ -174,7 +174,11 @@ private static DeclarationsProvider GetDeclarationsProvider(Descriptor descripto
174174

175175
if (method.ReturnType.IsByRef)
176176
{
177-
return new ByRefDeclarationsProvider(descriptor);
177+
// System.Runtime.CompilerServices.IsReadOnlyAttribute is part of .NET Standard 2.1, we can't use it here..
178+
if (method.ReturnParameter.GetCustomAttributes().Any(attribute => attribute.GetType().Name == "IsReadOnlyAttribute"))
179+
return new ByReadOnlyRefDeclarationsProvider(descriptor);
180+
else
181+
return new ByRefDeclarationsProvider(descriptor);
178182
}
179183

180184
return new NonVoidDeclarationsProvider(descriptor);
@@ -234,11 +238,20 @@ private static string GetEngineFactoryTypeName(BenchmarkCase benchmarkCase)
234238
}
235239

236240
private static string GetParameterModifier(ParameterInfo parameterInfo)
237-
=> parameterInfo.ParameterType.IsByRef
238-
? "ref"
239-
: parameterInfo.IsOut
240-
? "out"
241-
: string.Empty;
241+
{
242+
if (!parameterInfo.ParameterType.IsByRef)
243+
return string.Empty;
244+
245+
// From https://stackoverflow.com/a/38110036/5852046 :
246+
// "If you don't do the IsByRef check for out parameters, then you'll incorrectly get members decorated with the
247+
// [Out] attribute from System.Runtime.InteropServices but which aren't actually C# out parameters."
248+
if (parameterInfo.IsOut)
249+
return "out";
250+
else if (parameterInfo.IsIn)
251+
return "in";
252+
else
253+
return "ref";
254+
}
242255

243256
/// <summary>
244257
/// for CoreRT we can't use reflection to load type and run a method, so we simply generate a switch for all types..

src/BenchmarkDotNet/Code/DeclarationsProvider.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ internal abstract class DeclarationsProvider
3838

3939
public virtual string WorkloadMethodDelegate(string passArguments) => Descriptor.WorkloadMethod.Name;
4040

41+
public virtual string WorkloadMethodReturnTypeModifiers => null;
42+
4143
public virtual string GetWorkloadMethodCall(string passArguments) => $"{Descriptor.WorkloadMethod.Name}({passArguments})";
4244

4345
public virtual string ConsumeField => null;
@@ -48,8 +50,6 @@ internal abstract class DeclarationsProvider
4850

4951
public abstract string OverheadImplementation { get; }
5052

51-
public virtual bool UseRefKeyword => false;
52-
5353
private string GetMethodName(MethodInfo method)
5454
{
5555
if (method == null)
@@ -133,7 +133,16 @@ public ByRefDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
133133

134134
public override string ExtraDefines => "#define RETURNS_BYREF";
135135

136-
public override bool UseRefKeyword => true;
136+
public override string WorkloadMethodReturnTypeModifiers => "ref";
137+
}
138+
139+
internal class ByReadOnlyRefDeclarationsProvider : ByRefDeclarationsProvider
140+
{
141+
public ByReadOnlyRefDeclarationsProvider(Descriptor descriptor) : base(descriptor) { }
142+
143+
public override string ExtraDefines => "#define RETURNS_BYREF_READONLY";
144+
145+
public override string WorkloadMethodReturnTypeModifiers => "ref readonly";
137146
}
138147

139148
internal class TaskDeclarationsProvider : VoidDeclarationsProvider

src/BenchmarkDotNet/Engines/DeadCodeEliminationHelper.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,14 @@ public static void KeepAliveWithoutBoxing<T>(T value) { }
2020
[MethodImpl(MethodImplOptions.NoInlining)]
2121
[UsedImplicitly] // Used in generated benchmarks
2222
public static void KeepAliveWithoutBoxing<T>(ref T value) { }
23+
24+
/// <summary>
25+
/// This method can't get inlined, so any value send to it
26+
/// will not get eliminated by the dead code elimination
27+
/// it's not called KeepAliveWithoutBoxing because compiler would not be able to diff `ref` and `in`
28+
/// </summary>
29+
[MethodImpl(MethodImplOptions.NoInlining)]
30+
[UsedImplicitly] // Used in generated benchmarks
31+
public static void KeepAliveWithoutBoxingReadonly<T>(in T value) { }
2332
}
2433
}

src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.Linq;
45
using System.Reflection;
56
using BenchmarkDotNet.Attributes;
@@ -36,12 +37,11 @@ internal static bool HasAttribute<T>(this MethodInfo methodInfo) where T : Attri
3637
/// </summary>
3738
internal static string GetCorrectCSharpTypeName(this Type type, bool includeNamespace = true, bool includeGenericArgumentsNamespace = true)
3839
{
39-
if (!type.Name.EndsWith("&"))
40-
while (!(type.IsPublic || type.IsNestedPublic) && type.BaseType != null)
41-
type = type.BaseType;
40+
while (!(type.IsPublic || type.IsNestedPublic) && type.BaseType != null)
41+
type = type.BaseType;
4242

4343
// the reflection is missing information about types passed by ref (ie ref ValueTuple<int> is reported as NON generic type)
44-
if (type.IsByRef && !type.IsGenericType && type.Name.Contains('`'))
44+
if (type.IsByRef && !type.IsGenericType)
4545
type = type.GetElementType() ?? throw new NullReferenceException(nameof(type.GetElementType)); // https://github.com/dotnet/corefx/issues/29975#issuecomment-393134330
4646

4747
if (type == typeof(void))

src/BenchmarkDotNet/Templates/BenchmarkType.txt

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353

5454
public delegate $OverheadMethodReturnTypeName$ OverheadDelegate($ArgumentsDefinition$);
5555

56-
public delegate $Ref$ $WorkloadMethodReturnType$ WorkloadDelegate($ArgumentsDefinition$);
56+
public delegate $WorkloadMethodReturnTypeModifiers$ $WorkloadMethodReturnType$ WorkloadDelegate($ArgumentsDefinition$);
5757

5858
public Runnable_$ID$()
5959
{
@@ -281,6 +281,65 @@
281281
return ref $WorkloadMethodCall$;
282282
}
283283

284+
return ref workloadDefaultValueHolder;
285+
}
286+
#elif RETURNS_BYREF_READONLY_$ID$
287+
288+
private void OverheadActionUnroll(System.Int64 invokeCount)
289+
{
290+
$LoadArguments$
291+
$OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$);
292+
for (System.Int64 i = 0; i < invokeCount; i++)
293+
{
294+
value = overheadDelegate($PassArguments$);@Unroll@
295+
}
296+
BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value);
297+
}
298+
299+
private void OverheadActionNoUnroll(System.Int64 invokeCount)
300+
{
301+
$LoadArguments$
302+
$OverheadMethodReturnTypeName$ value = default($OverheadMethodReturnTypeName$);
303+
for (System.Int64 i = 0; i < invokeCount; i++)
304+
{
305+
value = overheadDelegate($PassArguments$);
306+
}
307+
BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxing(value);
308+
}
309+
310+
private $WorkloadMethodReturnType$ workloadDefaultValueHolder = default($WorkloadMethodReturnType$);
311+
312+
private void WorkloadActionUnroll(System.Int64 invokeCount)
313+
{
314+
$LoadArguments$
315+
ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder;
316+
for (System.Int64 i = 0; i < invokeCount; i++)
317+
{
318+
alias = workloadDelegate($PassArguments$);@Unroll@
319+
}
320+
BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxingReadonly(alias);
321+
}
322+
323+
private void WorkloadActionNoUnroll(System.Int64 invokeCount)
324+
{
325+
$LoadArguments$
326+
ref $WorkloadMethodReturnType$ alias = ref workloadDefaultValueHolder;
327+
for (System.Int64 i = 0; i < invokeCount; i++)
328+
{
329+
alias = workloadDelegate($PassArguments$);
330+
}
331+
BenchmarkDotNet.Engines.DeadCodeEliminationHelper.KeepAliveWithoutBoxingReadonly(alias);
332+
}
333+
334+
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoOptimization | System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
335+
public ref readonly $WorkloadMethodReturnType$ $DisassemblerEntryMethodName$()
336+
{
337+
if (NotEleven == 11)
338+
{
339+
$LoadArguments$
340+
return ref $WorkloadMethodCall$;
341+
}
342+
284343
return ref workloadDefaultValueHolder;
285344
}
286345
#elif RETURNS_VOID_$ID$

src/BenchmarkDotNet/Templates/CsProj.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
<UseSharedCompilation>false</UseSharedCompilation>
1717
<CodeAnalysisRuleSet></CodeAnalysisRuleSet>
1818
$COPIEDSETTINGS$
19+
<!-- we set LangVersion after $COPIEDSETTINGS$ which might contain LangVersion copied from the benchmarks project -->
20+
<LangVersion Condition="'$(LangVersion)' == '' Or '$(LangVersion)' &lt; '7.3'">latest</LangVersion>
1921
</PropertyGroup>
2022

2123
<ItemGroup>

src/BenchmarkDotNet/Toolchains/Roslyn/Builder.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ public BuildResult Build(GenerateResult generateResult, BuildPartition buildPart
2929
{
3030
logger.WriteLineInfo($"BuildScript: {generateResult.ArtifactsPaths.BuildScriptFilePath}");
3131

32-
var syntaxTree = CSharpSyntaxTree.ParseText(File.ReadAllText(generateResult.ArtifactsPaths.ProgramCodePath));
32+
var syntaxTree = CSharpSyntaxTree.ParseText(
33+
text: File.ReadAllText(generateResult.ArtifactsPaths.ProgramCodePath),
34+
// this version is used to parse the boilerplate code generated by BDN, so th benchmark themselves can use more recent version
35+
options: new CSharpParseOptions(LanguageVersion.CSharp7_3));
3336

3437
var compilationOptions = new CSharpCompilationOptions(
3538
outputKind: OutputKind.ConsoleApplication,

tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,21 @@ public void Simple(ref bool boolean, ref int number)
9696
}
9797
}
9898

99+
[Theory, MemberData(nameof(GetToolchains))]
100+
public void ArgumentsCanBePassedByReadonlyReferenceToBenchmark(IToolchain toolchain) => CanExecute<WithInArguments>(toolchain);
101+
102+
public class WithInArguments
103+
{
104+
[Benchmark]
105+
[Arguments(true, 1)]
106+
[Arguments(false, 2)]
107+
public void Simple(in bool boolean, in int number)
108+
{
109+
if (boolean && number != 1 || !boolean && number != 2)
110+
throw new InvalidOperationException("Incorrect values were passed");
111+
}
112+
}
113+
99114
[Theory, MemberData(nameof(GetToolchains))]
100115
public void NonCompileTimeConstantsCanBeReturnedFromSource(IToolchain toolchain) => CanExecute<WithComplexTypesReturnedFromSources>(toolchain);
101116

tests/BenchmarkDotNet.IntegrationTests/ValuesReturnedByBenchmarkTest.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,24 @@ public class ValuesReturnedByBenchmark
4848
[Benchmark]
4949
public Jit ReturnEnum() => Jit.RyuJit;
5050

51-
private int field = 123;
51+
private int intergerField = 123;
5252
[Benchmark]
53-
public ref int ReturnByRef() => ref field;
53+
public ref int ReturnByRef() => ref intergerField;
54+
55+
[Benchmark]
56+
public ref readonly int ReturnByReadonlyRef() => ref intergerField;
57+
58+
public readonly struct ReadOnlyStruct { }
59+
private ReadOnlyStruct readOnlyStructField;
60+
61+
[Benchmark]
62+
public ReadOnlyStruct ReturnReadOnlyStruct() => new ReadOnlyStruct();
63+
64+
[Benchmark]
65+
public ref ReadOnlyStruct ReturnReadOnlyStructByRef() => ref readOnlyStructField;
66+
67+
[Benchmark]
68+
public ref readonly ReadOnlyStruct ReturnReadOnlyStructByReadonlyRef() => ref readOnlyStructField;
5469

5570
[Benchmark]
5671
public Span<byte> ReturnStackOnlyType() => new Span<byte>(Array.Empty<byte>());

tests/BenchmarkDotNet.Tests/ReflectionTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,27 @@ public class GenericByRef
4343
public void TheMethod(ref ValueTuple<int, short> _) { }
4444
}
4545

46+
[Fact]
47+
public void GetCorrectCSharpTypeNameSupportsNestedTypes()
48+
{
49+
var nestedType = typeof(Nested);
50+
51+
CheckCorrectTypeName("BenchmarkDotNet.Tests.ReflectionTests.Nested", nestedType);
52+
}
53+
54+
[Fact]
55+
public void GetCorrectCSharpTypeNameSupportsNestedTypesPassedByReference()
56+
{
57+
var byRefNestedType = typeof(Nested).GetMethod(nameof(Nested.TheMethod)).GetParameters().Single().ParameterType;
58+
59+
CheckCorrectTypeName("BenchmarkDotNet.Tests.ReflectionTests.Nested", byRefNestedType);
60+
}
61+
62+
public class Nested
63+
{
64+
public void TheMethod(ref Nested _) { }
65+
}
66+
4667
[AssertionMethod]
4768
private static void CheckCorrectTypeName(string expectedName, Type type)
4869
{

0 commit comments

Comments
 (0)