Skip to content

Commit ae80f73

Browse files
Fold identical method bodies in the compiler (#101969)
Saves up to 5.2% in file size per rt-sz measurements. Adds a phase before object writing that looks for identical method bodies and deduplicates those that are same. To keep delegate equivalence working, the compiler also distinguishes between references to symbols and references to symbols that have an observable address identity. When a method is folded that has an observable address identity, the references that require observable address identity go through a unique jump thunk. This means that delegates point to jump thunks and reflection mapping tables point to jump thunks (whenever a method body got folded into a different method body). We do not need the jump thunks for references that are not address exposed (so a `call` in a method body will no go through a jump thunk). Since method body folding is still observable with stack trace APIs or debuggers, this is opt in. The user gets opted in by setting `StackTraceSupport=false` (or using an undocumented switch). I took a shortcut in a couple places where references that may or may not be address exposed get treated as address exposed. There are TODO comments around those. We may want to fix tracking within the compiler to tighten this. It may not matter much. I also took a shortcut in deduplication - we currently only look at leaf identical method bodies. The method bodies that become identical after first level of folding currently don't get folded. This leaves a bit size on the table still. There's a TODO comment as well. We also don't consider function pointers address exposed since there's no API to compare these. That's also a TODO for whenever we add such API.
1 parent 5508f79 commit ae80f73

32 files changed

+942
-73
lines changed

src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets

+1-1
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ The .NET Foundation licenses this file to you under the MIT license.
255255
<IlcArg Condition="$(IlcGenerateCompleteTypeMetadata) == 'true'" Include="--completetypemetadata" />
256256
<IlcArg Condition="$(StackTraceSupport) != 'false'" Include="--stacktracedata" />
257257
<IlcArg Condition="$(IlcScanReflection) != 'false' and $(IlcDisableReflection) != 'true'" Include="--scanreflection" />
258-
<IlcArg Condition="$(IlcFoldIdenticalMethodBodies) == 'true'" Include="--methodbodyfolding" />
258+
<IlcArg Condition="$(IlcFoldIdenticalMethodBodies) == 'true' or $(StackTraceSupport) == 'false'" Include="--methodbodyfolding" />
259259
<IlcArg Condition="$(Optimize) == 'true' and $(OptimizationPreference) == 'Size'" Include="--Os" />
260260
<IlcArg Condition="$(Optimize) == 'true' and $(OptimizationPreference) == 'Speed'" Include="--Ot" />
261261
<IlcArg Condition="'$(_linuxLibcFlavor)' == 'bionic'" Include="--noinlinetls" />

src/coreclr/tools/Common/Compiler/DependencyAnalysis/ObjectNodeSection.cs

-2
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,7 @@ public bool IsStandardSection
5252
public static readonly ObjectNodeSection BssSection = new ObjectNodeSection("bss", SectionType.Uninitialized);
5353
public static readonly ObjectNodeSection HydrationTargetSection = new ObjectNodeSection("hydrated", SectionType.Uninitialized);
5454
public static readonly ObjectNodeSection ManagedCodeWindowsContentSection = new ObjectNodeSection(".managedcode$I", SectionType.Executable);
55-
public static readonly ObjectNodeSection FoldableManagedCodeWindowsContentSection = new ObjectNodeSection(".managedcode$I", SectionType.Executable);
5655
public static readonly ObjectNodeSection ManagedCodeUnixContentSection = new ObjectNodeSection("__managedcode", SectionType.Executable);
57-
public static readonly ObjectNodeSection FoldableManagedCodeUnixContentSection = new ObjectNodeSection("__managedcode", SectionType.Executable);
5856

5957
// Section name on Windows has to be alphabetically less than the ending WindowsUnboxingStubsRegionNode node, and larger than
6058
// the begining WindowsUnboxingStubsRegionNode node, in order to have proper delimiters to the begining/ending of the

src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs

+2-7
Original file line numberDiff line numberDiff line change
@@ -480,13 +480,8 @@ private void PublishCode()
480480
}
481481
}
482482

483-
#pragma warning disable SA1001, SA1113, SA1115 // Comma should be on the same line as previous parameter
484-
_methodCodeNode.SetCode(objectData
485-
#if !SUPPORT_JIT && !READYTORUN
486-
, isFoldable: (_compilation._compilationOptions & RyuJitCompilationOptions.MethodBodyFolding) != 0
487-
#endif
488-
);
489-
#pragma warning restore SA1001, SA1113, SA1115 // Comma should be on the same line as previous parameter
483+
_methodCodeNode.SetCode(objectData);
484+
490485
#if READYTORUN
491486
if (_methodColdCodeNode != null)
492487
{

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DelegateCreationInfo.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,10 @@ public ISymbolNode GetTargetNode(NodeFactory factory)
141141
switch (_targetKind)
142142
{
143143
case TargetKind.CanonicalEntrypoint:
144-
return factory.CanonicalEntrypoint(TargetMethod, TargetMethodIsUnboxingThunk);
144+
return factory.AddressTakenMethodEntrypoint(TargetMethod, TargetMethodIsUnboxingThunk);
145145

146146
case TargetKind.ExactCallableAddress:
147-
return factory.ExactCallableAddress(TargetMethod, TargetMethodIsUnboxingThunk);
147+
return factory.ExactCallableAddressTakenAddress(TargetMethod, TargetMethodIsUnboxingThunk);
148148

149149
case TargetKind.InterfaceDispatch:
150150
return factory.InterfaceDispatchCell(TargetMethod);
@@ -347,7 +347,7 @@ internal int CompareTo(DelegateCreationInfo other, TypeSystemComparer comparer)
347347
if (compare != 0)
348348
return compare;
349349

350-
compare = comparer.Compare(TargetMethod, other.TargetMethod);
350+
compare = comparer.Compare(_targetMethod, other._targetMethod);
351351
if (compare != 0)
352352
return compare;
353353

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Diagnostics;
6+
7+
using ILCompiler.DependencyAnalysisFramework;
8+
9+
using Internal.Text;
10+
using Internal.TypeSystem;
11+
12+
namespace ILCompiler.DependencyAnalysis
13+
{
14+
/// <summary>
15+
/// Represents a method with address taken. Under normal circumstances, this node is not emitted
16+
/// into the object file and instead references to it are replaced to refer to the underlying method body.
17+
/// This is achieved through <see cref="ShouldSkipEmittingObjectNode(NodeFactory)"/> and <see cref="NodeForLinkage(NodeFactory)"/>.
18+
/// However, if the underlying method body got folded together with another method due to identical method body folding
19+
/// optimization, this node is not skipped and instead emits a jump stub. The purpose of the jump stub is to provide a
20+
/// unique code address for the address taken method.
21+
/// </summary>
22+
internal sealed class AddressTakenMethodNode : JumpStubNode, IMethodNode, ISymbolNodeWithLinkage
23+
{
24+
private readonly IMethodNode _methodNode;
25+
26+
public IMethodNode RealBody => _methodNode;
27+
28+
public AddressTakenMethodNode(IMethodNode methodNode)
29+
: base(methodNode)
30+
{
31+
_methodNode = methodNode;
32+
}
33+
34+
public MethodDesc Method => _methodNode.Method;
35+
36+
protected override string GetName(NodeFactory factory)
37+
{
38+
return "Address taken method: " + _methodNode.GetMangledName(factory.NameMangler);
39+
}
40+
41+
public override bool ShouldSkipEmittingObjectNode(NodeFactory factory)
42+
{
43+
return factory.ObjectInterner.GetDeduplicatedSymbol(factory, RealBody) == RealBody;
44+
}
45+
46+
public override IEnumerable<CombinedDependencyListEntry> GetConditionalStaticDependencies(NodeFactory factory) => null;
47+
public override IEnumerable<CombinedDependencyListEntry> SearchDynamicDependencies(List<DependencyNodeCore<NodeFactory>> markedNodes, int firstNode, NodeFactory context) => null;
48+
public override bool InterestingForDynamicDependencyAnalysis => false;
49+
public override bool HasDynamicDependencies => false;
50+
public override bool HasConditionalStaticDependencies => false;
51+
52+
public override void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb)
53+
{
54+
// We use the same mangled name as the underlying real method body.
55+
// This is okay since this node will go out of the way if the real body is marked
56+
// and part of the graph.
57+
_methodNode.AppendMangledName(nameMangler, sb);
58+
}
59+
60+
public override int CompareToImpl(ISortableNode other, CompilerComparer comparer)
61+
{
62+
return _methodNode.CompareToImpl(((AddressTakenMethodNode)other)._methodNode, comparer);
63+
}
64+
65+
public ISymbolNode NodeForLinkage(NodeFactory factory)
66+
{
67+
// If someone refers to this node but the target method still has a unique body,
68+
// refer to the target method.
69+
return factory.ObjectInterner.GetDeduplicatedSymbol(factory, RealBody) == RealBody ? RealBody : this;
70+
}
71+
72+
public override bool RepresentsIndirectionCell
73+
{
74+
get
75+
{
76+
Debug.Assert(!_methodNode.RepresentsIndirectionCell);
77+
return false;
78+
}
79+
}
80+
81+
public override int ClassCode => 0xfab0355;
82+
83+
public override bool IsShareable => ((ObjectNode)_methodNode).IsShareable;
84+
}
85+
}

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/DelegateTargetVirtualMethodNode.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@ namespace ILCompiler.DependencyAnalysis
1616
public class DelegateTargetVirtualMethodNode : DependencyNodeCore<NodeFactory>
1717
{
1818
private readonly MethodDesc _method;
19+
private readonly bool _reflected;
1920

20-
public DelegateTargetVirtualMethodNode(MethodDesc method)
21+
public DelegateTargetVirtualMethodNode(MethodDesc method, bool reflected)
2122
{
2223
Debug.Assert(method.GetCanonMethodTarget(CanonicalFormKind.Specific) == method);
2324
_method = method;
25+
_reflected = reflected;
2426
}
2527

2628
protected override string GetName(NodeFactory factory)
2729
{
28-
return "Delegate target method: " + _method.ToString();
30+
return (_reflected ? "Reflected delegate target method:" : "Delegate target method: ") + _method.ToString();
2931
}
3032

3133
public override IEnumerable<DependencyListEntry> GetStaticDependencies(NodeFactory factory) => null;

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/EETypeNode.cs

+28-3
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,10 @@ public sealed override IEnumerable<CombinedDependencyListEntry> GetConditionalSt
426426
factory.TentativeMethodEntrypoint(canonImpl, impl.OwningType.IsValueType) :
427427
factory.MethodEntrypoint(canonImpl, impl.OwningType.IsValueType);
428428
result.Add(new CombinedDependencyListEntry(implNode, factory.VirtualMethodUse(decl), "Virtual method"));
429+
430+
result.Add(new CombinedDependencyListEntry(
431+
factory.AddressTakenMethodEntrypoint(canonImpl, impl.OwningType.IsValueType),
432+
factory.DelegateTargetVirtualMethod(decl.GetCanonMethodTarget(CanonicalFormKind.Specific)), "Slot is a delegate target"));
429433
}
430434

431435
if (impl.OwningType == defType)
@@ -498,11 +502,22 @@ public sealed override IEnumerable<CombinedDependencyListEntry> GetConditionalSt
498502

499503
// If the interface method is used virtually, the implementation body is used
500504
result.Add(new CombinedDependencyListEntry(factory.MethodEntrypoint(defaultIntfMethod), factory.VirtualMethodUse(interfaceMethod), "Interface method"));
505+
506+
// If the interface method is virtual delegate target, the implementation is address taken
507+
result.Add(new CombinedDependencyListEntry(
508+
factory.AddressTakenMethodEntrypoint(defaultIntfMethod),
509+
factory.DelegateTargetVirtualMethod(interfaceMethod.GetCanonMethodTarget(CanonicalFormKind.Specific)), "Interface slot is delegate target"));
501510
}
502511
else
503512
{
504513
// If the interface method is used virtually, the slot is used virtually
505514
result.Add(new CombinedDependencyListEntry(factory.VirtualMethodUse(implMethod), factory.VirtualMethodUse(interfaceMethod), "Interface method"));
515+
516+
// If the interface method is virtual delegate target, the slot is virtual delegate target
517+
result.Add(new CombinedDependencyListEntry(
518+
factory.DelegateTargetVirtualMethod(implMethod.GetCanonMethodTarget(CanonicalFormKind.Specific)),
519+
factory.DelegateTargetVirtualMethod(interfaceMethod.GetCanonMethodTarget(CanonicalFormKind.Specific)),
520+
"Interface slot is delegate target"));
506521
}
507522

508523
// If any of the implemented interfaces have variance, calls against compatible interface methods
@@ -550,6 +565,11 @@ public sealed override IEnumerable<CombinedDependencyListEntry> GetConditionalSt
550565
}
551566
result.Add(new CombinedDependencyListEntry(factory.MethodEntrypoint(defaultIntfMethod), factory.VirtualMethodUse(interfaceMethod), "Interface method"));
552567

568+
result.Add(new CombinedDependencyListEntry(
569+
factory.AddressTakenMethodEntrypoint(defaultIntfMethod),
570+
factory.DelegateTargetVirtualMethod(interfaceMethod.GetCanonMethodTarget(CanonicalFormKind.Specific)),
571+
"Slot is delegate target"));
572+
553573
factory.MetadataManager.NoteOverridingMethod(interfaceMethod, implMethod);
554574

555575
factory.MetadataManager.GetDependenciesForOverridingMethod(ref result, factory, interfaceMethod, implMethod);
@@ -1103,9 +1123,14 @@ private void OutputVirtualSlots(NodeFactory factory, ref ObjectDataBuilder objDa
11031123
&& implMethod.OwningType is MetadataType mdImplMethodType && mdImplMethodType.IsAbstract
11041124
&& factory.CompilationModuleGroup.AllowVirtualMethodOnAbstractTypeOptimization(canonImplMethod);
11051125

1106-
IMethodNode implSymbol = canUseTentativeEntrypoint ?
1107-
factory.TentativeMethodEntrypoint(canonImplMethod, implMethod.OwningType.IsValueType) :
1108-
factory.MethodEntrypoint(canonImplMethod, implMethod.OwningType.IsValueType);
1126+
IMethodNode implSymbol;
1127+
if (canUseTentativeEntrypoint)
1128+
implSymbol = factory.TentativeMethodEntrypoint(canonImplMethod, implMethod.OwningType.IsValueType);
1129+
else if (factory.DelegateTargetVirtualMethod(declMethod.GetCanonMethodTarget(CanonicalFormKind.Specific)).Marked)
1130+
implSymbol = factory.AddressTakenMethodEntrypoint(canonImplMethod, implMethod.OwningType.IsValueType);
1131+
else
1132+
implSymbol = factory.MethodEntrypoint(canonImplMethod, implMethod.OwningType.IsValueType);
1133+
11091134
objData.EmitPointerReloc(implSymbol);
11101135
}
11111136
else

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ExactMethodInstantiationsNode.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
5858
// Get the method pointer vertex
5959

6060
bool getUnboxingStub = method.OwningType.IsValueType && !method.Signature.IsStatic;
61-
IMethodNode methodEntryPointNode = factory.MethodEntrypoint(method, getUnboxingStub);
61+
// TODO-SIZE: we need address taken entrypoint only if this was a target of a delegate
62+
IMethodNode methodEntryPointNode = factory.AddressTakenMethodEntrypoint(method, getUnboxingStub);
6263
Vertex methodPointer = nativeWriter.GetUnsignedConstant(_externalReferences.GetIndex(methodEntryPointNode));
6364

6465
// Get native layout vertices for the declaring type
@@ -112,7 +113,8 @@ public static void GetExactMethodInstantiationDependenciesForMethod(ref Dependen
112113

113114
// Method entry point dependency
114115
bool getUnboxingStub = method.OwningType.IsValueType && !method.Signature.IsStatic;
115-
IMethodNode methodEntryPointNode = factory.MethodEntrypoint(method, getUnboxingStub);
116+
// TODO-SIZE: we need address taken entrypoint only if this was a target of a delegate
117+
IMethodNode methodEntryPointNode = factory.AddressTakenMethodEntrypoint(method, getUnboxingStub);
116118
dependencies.Add(new DependencyListEntry(methodEntryPointNode, "Exact method instantiation entry"));
117119

118120
// Get native layout dependencies for the declaring type

src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/FatFunctionPointerNode.cs

+13-4
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,24 @@ namespace ILCompiler.DependencyAnalysis
1515
/// </summary>
1616
public class FatFunctionPointerNode : DehydratableObjectNode, IMethodNode, ISymbolDefinitionNode
1717
{
18-
private bool _isUnboxingStub;
18+
private readonly bool _isUnboxingStub;
19+
private readonly bool _isAddressTaken;
1920

2021
public bool IsUnboxingStub => _isUnboxingStub;
2122

22-
public FatFunctionPointerNode(MethodDesc methodRepresented, bool isUnboxingStub)
23+
public FatFunctionPointerNode(MethodDesc methodRepresented, bool isUnboxingStub, bool addressTaken)
2324
{
2425
// We should not create these for methods that don't have a canonical method body
2526
Debug.Assert(methodRepresented.GetCanonMethodTarget(CanonicalFormKind.Specific) != methodRepresented);
2627

2728
Method = methodRepresented;
2829
_isUnboxingStub = isUnboxingStub;
30+
_isAddressTaken = addressTaken;
2931
}
3032

3133
public void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb)
3234
{
33-
string prefix = _isUnboxingStub ? "__fatunboxpointer_" : "__fatpointer_";
35+
string prefix = $"__fat{(_isUnboxingStub ? "unbox" : "")}{(_isAddressTaken ? "addresstaken" : "")}pointer_";
3436
sb.Append(prefix).Append(nameMangler.GetMangledMethodName(Method));
3537
}
3638

@@ -67,7 +69,10 @@ protected override ObjectData GetDehydratableData(NodeFactory factory, bool relo
6769
MethodDesc canonMethod = Method.GetCanonMethodTarget(CanonicalFormKind.Specific);
6870

6971
// Pointer to the canonical body of the method
70-
builder.EmitPointerReloc(factory.MethodEntrypoint(canonMethod, _isUnboxingStub));
72+
ISymbolNode target = _isAddressTaken
73+
? factory.AddressTakenMethodEntrypoint(canonMethod, _isUnboxingStub)
74+
: factory.MethodEntrypoint(canonMethod, _isUnboxingStub);
75+
builder.EmitPointerReloc(target);
7176

7277
// Find out what's the context to use
7378
ISortableSymbolNode contextParameter;
@@ -97,6 +102,10 @@ public override int CompareToImpl(ISortableNode other, CompilerComparer comparer
97102
if (compare != 0)
98103
return compare;
99104

105+
compare = _isAddressTaken.CompareTo(((FatFunctionPointerNode)other)._isAddressTaken);
106+
if (compare != 0)
107+
return compare;
108+
100109
return comparer.Compare(Method, ((FatFunctionPointerNode)other).Method);
101110
}
102111
}

0 commit comments

Comments
 (0)