From 5a94384e9da1d5f25ebe17e68a159e482fb6943c Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 25 Mar 2026 09:48:13 -0700 Subject: [PATCH 1/9] Refactor ObjectDataInterner to use pluggable deduplicators Extract method-body-specific deduplication logic from ObjectDataInterner into a new MethodBodyDeduplicator class behind an IObjectDataDeduplicator interface. ObjectDataInterner now accepts a set of deduplicator instances and delegates the deduplication passes to them. - Add IObjectDataDeduplicator interface with DeduplicatePass method - Move MethodInternKey, MethodInternComparer, and CanFold logic into MethodBodyDeduplicator - Add virtual CanFold method on NodeFactory (returns false by default) - Override CanFold in RyuJitNodeFactory to delegate to MethodBodyDeduplicator Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DependencyAnalysis/NodeFactory.cs | 6 +- .../Compiler/MethodBodyDeduplicator.cs | 194 ++++++++++++++++++ .../Compiler/ObjectDataInterner.cs | 193 ++--------------- .../ILCompiler.Compiler.csproj | 1 + .../DependencyAnalysis/RyuJitNodeFactory.cs | 8 +- .../Compiler/RyuJitCompilationBuilder.cs | 14 +- 6 files changed, 233 insertions(+), 183 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MethodBodyDeduplicator.cs diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/NodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/NodeFactory.cs index d69693286b0bcb..bdec43b6ce9873 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/NodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/NodeFactory.cs @@ -134,6 +134,8 @@ internal ObjectDataInterner ObjectInterner get; } + protected virtual bool CanFold(MethodDesc method) => false; + public TypeMapManager TypeMapManager { get; @@ -1138,7 +1140,7 @@ public IMethodNode FatFunctionPointer(MethodDesc method, bool isUnboxingStub = f public IMethodNode FatAddressTakenFunctionPointer(MethodDesc method, bool isUnboxingStub = false) { - if (!ObjectInterner.CanFold(method)) + if (!CanFold(method)) return FatFunctionPointer(method, isUnboxingStub); return _fatAddressTakenFunctionPointers.GetOrAdd(new MethodKey(method, isUnboxingStub)); @@ -1204,7 +1206,7 @@ internal TypeGVMEntriesNode TypeGVMEntries(TypeDesc type) private NodeCache _addressTakenMethods; public IMethodNode AddressTakenMethodEntrypoint(MethodDesc method, bool unboxingStub = false) { - if (unboxingStub || !ObjectInterner.CanFold(method)) + if (unboxingStub || !CanFold(method)) return MethodEntrypoint(method, unboxingStub); return _addressTakenMethods.GetOrAdd(method); diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MethodBodyDeduplicator.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MethodBodyDeduplicator.cs new file mode 100644 index 00000000000000..2dd7f542b02b1a --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MethodBodyDeduplicator.cs @@ -0,0 +1,194 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +using ILCompiler.DependencyAnalysis; + +using Internal.IL.Stubs; +using Internal.TypeSystem; + +namespace ILCompiler +{ + public sealed class MethodBodyDeduplicator : IObjectDataDeduplicator + { + private readonly bool _genericsOnly; + private int _previousHashCount; + + public MethodBodyDeduplicator(bool genericsOnly) + { + _genericsOnly = genericsOnly; + } + + public bool CanFold(MethodDesc method) + { + if (!_genericsOnly || method.HasInstantiation || method.OwningType.HasInstantiation) + return true; + + if (method.GetTypicalMethodDefinition() is GetFieldHelperMethodOverride) + return true; + + return false; + } + + public void DeduplicatePass(NodeFactory factory, Dictionary previousSymbolRemapping, Dictionary symbolRemapping) + { + var methodHash = new HashSet(_previousHashCount, new MethodInternComparer(factory, previousSymbolRemapping, _genericsOnly)); + + foreach (IMethodBodyNode body in factory.MetadataManager.GetCompiledMethodBodies()) + { + if (!CanFold(body.Method)) + continue; + + // We don't track special unboxing thunks as virtual method use related so ignore them + if (body is ISpecialUnboxThunkNode unboxThunk && unboxThunk.IsSpecialUnboxingThunk) + continue; + + // Bodies that are visible from outside should not be folded because we don't know + // if they're address taken. + if (!factory.GetSymbolAlternateName(body, out _).IsNull) + continue; + + var key = new MethodInternKey(body, factory); + if (methodHash.TryGetValue(key, out MethodInternKey found)) + { + symbolRemapping.Add(body, found.Method); + } + else + { + methodHash.Add(key); + } + } + + _previousHashCount = methodHash.Count; + } + + private sealed class MethodInternKey + { + public IMethodBodyNode Method { get; } + public int HashCode { get; } + + public MethodInternKey(IMethodBodyNode node, NodeFactory factory) + { + ObjectNode.ObjectData data = ((ObjectNode)node).GetData(factory, relocsOnly: false); + + var hashCode = default(HashCode); + hashCode.AddBytes(data.Data); + + var nodeWithCodeInfo = (INodeWithCodeInfo)node; + + hashCode.AddBytes(nodeWithCodeInfo.GCInfo); + + foreach (FrameInfo fi in nodeWithCodeInfo.FrameInfos) + hashCode.Add(fi.GetHashCode()); + + ObjectNode.ObjectData ehData = nodeWithCodeInfo.EHInfo?.GetData(factory, relocsOnly: false); + + if (ehData is not null) + hashCode.AddBytes(ehData.Data); + + HashCode = hashCode.ToHashCode(); + Method = node; + } + } + + private sealed class MethodInternComparer : IEqualityComparer + { + private readonly NodeFactory _factory; + private readonly Dictionary _interner; + private readonly bool _genericsOnly; + + public MethodInternComparer(NodeFactory factory, Dictionary interner, bool genericsOnly) + => (_factory, _interner, _genericsOnly) = (factory, interner, genericsOnly); + + public int GetHashCode(MethodInternKey key) => key.HashCode; + + private static bool AreSame(ReadOnlySpan o1, ReadOnlySpan o2) => o1.SequenceEqual(o2); + + private bool AreSame(ObjectNode.ObjectData o1, ObjectNode.ObjectData o2) + { + if (AreSame(o1.Data, o2.Data) && o1.Relocs.Length == o2.Relocs.Length) + { + for (int i = 0; i < o1.Relocs.Length; i++) + { + ref Relocation r1 = ref o1.Relocs[i]; + ref Relocation r2 = ref o2.Relocs[i]; + if (r1.RelocType != r2.RelocType + || r1.Offset != r2.Offset) + { + return false; + } + + if (r1.Target != r2.Target) + { + if (r1.Target is MethodReadOnlyDataNode rodata1 + && r2.Target is MethodReadOnlyDataNode rodata2 + && AreSame(rodata1.GetData(_factory, relocsOnly: false), rodata2.GetData(_factory, relocsOnly: false))) + { + // We can consider same MethodReadOnlyDataNode the same. + } + else if (_interner != null && + ((_interner.TryGetValue(r1.Target, out ISymbolNode t1) && r2.Target == t1) + || (_interner.TryGetValue(r2.Target, out ISymbolNode t2) && r1.Target == t2))) + { + // These got already interned + } + else + { + return false; + } + } + } + + return true; + } + + return false; + } + + public bool Equals(MethodInternKey a, MethodInternKey b) + { + if (a.HashCode != b.HashCode) + return false; + + if (_genericsOnly + && a.Method.Method.GetTypicalMethodDefinition() != b.Method.Method.GetTypicalMethodDefinition()) + return false; + + ObjectNode.ObjectData o1data = ((ObjectNode)a.Method).GetData(_factory, relocsOnly: false); + ObjectNode.ObjectData o2data = ((ObjectNode)b.Method).GetData(_factory, relocsOnly: false); + + if (!AreSame(o1data, o2data)) + return false; + + var o1codeinfo = (INodeWithCodeInfo)a.Method; + var o2codeinfo = (INodeWithCodeInfo)b.Method; + if (!AreSame(o1codeinfo.GCInfo, o2codeinfo.GCInfo)) + return false; + + FrameInfo[] o1frames = o1codeinfo.FrameInfos; + FrameInfo[] o2frames = o2codeinfo.FrameInfos; + if (o1frames.Length != o2frames.Length) + return false; + + for (int i = 0; i < o1frames.Length; i++) + { + if (!o1frames[i].Equals(o2frames[i])) + return false; + } + + MethodExceptionHandlingInfoNode o1eh = o1codeinfo.EHInfo; + MethodExceptionHandlingInfoNode o2eh = o2codeinfo.EHInfo; + + if (o1eh == o2eh) + return true; + + if (o1eh is null || o2eh is null) + return false; + + return AreSame(o1eh.GetData(_factory, relocsOnly: false), o2eh.GetData(_factory, relocsOnly: false)); + } + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ObjectDataInterner.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ObjectDataInterner.cs index 9092b870f818da..1df1fb9f4932d9 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ObjectDataInterner.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ObjectDataInterner.cs @@ -1,42 +1,36 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Generic; using ILCompiler.DependencyAnalysis; -using Internal.IL.Stubs; -using Internal.TypeSystem; - using Debug = System.Diagnostics.Debug; namespace ILCompiler { + /// + /// Pluggable strategy for identifying and deduplicating equivalent object data. + /// + public interface IObjectDataDeduplicator + { + /// + /// Performs one deduplication pass, adding entries to . + /// Called iteratively until the overall mapping converges. + /// + void DeduplicatePass(NodeFactory factory, Dictionary previousSymbolRemapping, Dictionary symbolRemapping); + } + public sealed class ObjectDataInterner { - private readonly bool _genericsOnly; + private readonly IObjectDataDeduplicator[] _deduplicators; private Dictionary _symbolRemapping; - public static ObjectDataInterner Null { get; } = new ObjectDataInterner(genericsOnly: false) { _symbolRemapping = new() }; - - public ObjectDataInterner(bool genericsOnly) - { - _genericsOnly = genericsOnly; - } + public static ObjectDataInterner Null { get; } = new ObjectDataInterner() { _symbolRemapping = new() }; - public bool CanFold(MethodDesc method) + public ObjectDataInterner(params IObjectDataDeduplicator[] deduplicators) { - if (this == Null) - return false; - - if (!_genericsOnly || method.HasInstantiation || method.OwningType.HasInstantiation) - return true; - - if (method.GetTypicalMethodDefinition() is GetFieldHelperMethodOverride) - return true; - - return false; + _deduplicators = deduplicators; } private void EnsureMap(NodeFactory factory) @@ -46,41 +40,17 @@ private void EnsureMap(NodeFactory factory) if (_symbolRemapping != null) return; - HashSet previousMethodHash; - HashSet methodHash = null; Dictionary previousSymbolRemapping; Dictionary symbolRemapping = null; do { - previousMethodHash = methodHash; previousSymbolRemapping = symbolRemapping; - methodHash = new HashSet(previousMethodHash?.Count ?? 0, new MethodInternComparer(factory, previousSymbolRemapping, _genericsOnly)); symbolRemapping = new Dictionary((int)(1.05 * (previousSymbolRemapping?.Count ?? 0))); - foreach (IMethodBodyNode body in factory.MetadataManager.GetCompiledMethodBodies()) + foreach (IObjectDataDeduplicator deduplicator in _deduplicators) { - if (!CanFold(body.Method)) - continue; - - // We don't track special unboxing thunks as virtual method use related so ignore them - if (body is ISpecialUnboxThunkNode unboxThunk && unboxThunk.IsSpecialUnboxingThunk) - continue; - - // Bodies that are visible from outside should not be folded because we don't know - // if they're address taken. - if (!factory.GetSymbolAlternateName(body, out _).IsNull) - continue; - - var key = new MethodInternKey(body, factory); - if (methodHash.TryGetValue(key, out MethodInternKey found)) - { - symbolRemapping.Add(body, found.Method); - } - else - { - methodHash.Add(key); - } + deduplicator.DeduplicatePass(factory, previousSymbolRemapping, symbolRemapping); } } while (previousSymbolRemapping == null || previousSymbolRemapping.Count < symbolRemapping.Count); @@ -97,132 +67,5 @@ public ISymbolNode GetDeduplicatedSymbol(NodeFactory factory, ISymbolNode origin return _symbolRemapping.TryGetValue(target, out ISymbolNode result) ? result : original; } - - private sealed class MethodInternKey - { - public IMethodBodyNode Method { get; } - public int HashCode { get; } - - public MethodInternKey(IMethodBodyNode node, NodeFactory factory) - { - ObjectNode.ObjectData data = ((ObjectNode)node).GetData(factory, relocsOnly: false); - - var hashCode = default(HashCode); - hashCode.AddBytes(data.Data); - - var nodeWithCodeInfo = (INodeWithCodeInfo)node; - - hashCode.AddBytes(nodeWithCodeInfo.GCInfo); - - foreach (FrameInfo fi in nodeWithCodeInfo.FrameInfos) - hashCode.Add(fi.GetHashCode()); - - ObjectNode.ObjectData ehData = nodeWithCodeInfo.EHInfo?.GetData(factory, relocsOnly: false); - - if (ehData is not null) - hashCode.AddBytes(ehData.Data); - - HashCode = hashCode.ToHashCode(); - Method = node; - } - } - - private sealed class MethodInternComparer : IEqualityComparer - { - private readonly NodeFactory _factory; - private readonly Dictionary _interner; - private readonly bool _genericsOnly; - - public MethodInternComparer(NodeFactory factory, Dictionary interner, bool genericsOnly) - => (_factory, _interner, _genericsOnly) = (factory, interner, genericsOnly); - - public int GetHashCode(MethodInternKey key) => key.HashCode; - - private static bool AreSame(ReadOnlySpan o1, ReadOnlySpan o2) => o1.SequenceEqual(o2); - - private bool AreSame(ObjectNode.ObjectData o1, ObjectNode.ObjectData o2) - { - if (AreSame(o1.Data, o2.Data) && o1.Relocs.Length == o2.Relocs.Length) - { - for (int i = 0; i < o1.Relocs.Length; i++) - { - ref Relocation r1 = ref o1.Relocs[i]; - ref Relocation r2 = ref o2.Relocs[i]; - if (r1.RelocType != r2.RelocType - || r1.Offset != r2.Offset) - { - return false; - } - - if (r1.Target != r2.Target) - { - if (r1.Target is MethodReadOnlyDataNode rodata1 - && r2.Target is MethodReadOnlyDataNode rodata2 - && AreSame(rodata1.GetData(_factory, relocsOnly: false), rodata2.GetData(_factory, relocsOnly: false))) - { - // We can consider same MethodReadOnlyDataNode the same. - } - else if (_interner != null && - ((_interner.TryGetValue(r1.Target, out ISymbolNode t1) && r2.Target == t1) - || (_interner.TryGetValue(r2.Target, out ISymbolNode t2) && r1.Target == t2))) - { - // These got already interned - } - else - { - return false; - } - } - } - - return true; - } - - return false; - } - - public bool Equals(MethodInternKey a, MethodInternKey b) - { - if (a.HashCode != b.HashCode) - return false; - - if (_genericsOnly - && a.Method.Method.GetTypicalMethodDefinition() != b.Method.Method.GetTypicalMethodDefinition()) - return false; - - ObjectNode.ObjectData o1data = ((ObjectNode)a.Method).GetData(_factory, relocsOnly: false); - ObjectNode.ObjectData o2data = ((ObjectNode)b.Method).GetData(_factory, relocsOnly: false); - - if (!AreSame(o1data, o2data)) - return false; - - var o1codeinfo = (INodeWithCodeInfo)a.Method; - var o2codeinfo = (INodeWithCodeInfo)b.Method; - if (!AreSame(o1codeinfo.GCInfo, o2codeinfo.GCInfo)) - return false; - - FrameInfo[] o1frames = o1codeinfo.FrameInfos; - FrameInfo[] o2frames = o2codeinfo.FrameInfos; - if (o1frames.Length != o2frames.Length) - return false; - - for (int i = 0; i < o1frames.Length; i++) - { - if (!o1frames[i].Equals(o2frames[i])) - return false; - } - - MethodExceptionHandlingInfoNode o1eh = o1codeinfo.EHInfo; - MethodExceptionHandlingInfoNode o2eh = o2codeinfo.EHInfo; - - if (o1eh == o2eh) - return true; - - if (o1eh == null || o2eh == null) - return false; - - return AreSame(o1eh.GetData(_factory, relocsOnly: false), o2eh.GetData(_factory, relocsOnly: false)); - } - } } } diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj index 4592ac765a86a7..73e6a054b99eba 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj @@ -503,6 +503,7 @@ + diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/RyuJitNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/RyuJitNodeFactory.cs index 0e5574b7dd652c..8992e89f2d54e4 100644 --- a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/RyuJitNodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/DependencyAnalysis/RyuJitNodeFactory.cs @@ -9,13 +9,19 @@ namespace ILCompiler.DependencyAnalysis { public sealed class RyuJitNodeFactory : NodeFactory { + private readonly MethodBodyDeduplicator _methodBodyDeduplicator; + public RyuJitNodeFactory(CompilerTypeSystemContext context, CompilationModuleGroup compilationModuleGroup, MetadataManager metadataManager, InteropStubManager interopStubManager, NameMangler nameMangler, VTableSliceProvider vtableSliceProvider, DictionaryLayoutProvider dictionaryLayoutProvider, InlinedThreadStatics inlinedThreadStatics, PreinitializationManager preinitializationManager, - DevirtualizationManager devirtualizationManager, ObjectDataInterner dataInterner, TypeMapManager typeMapManager) + DevirtualizationManager devirtualizationManager, ObjectDataInterner dataInterner, MethodBodyDeduplicator methodBodyDeduplicator, TypeMapManager typeMapManager) : base(context, compilationModuleGroup, metadataManager, interopStubManager, nameMangler, new LazyGenericsDisabledPolicy(), vtableSliceProvider, dictionaryLayoutProvider, inlinedThreadStatics, new ExternSymbolsImportedNodeProvider(), preinitializationManager, devirtualizationManager, dataInterner, typeMapManager) { + _methodBodyDeduplicator = methodBodyDeduplicator; } + protected override bool CanFold(MethodDesc method) + => _methodBodyDeduplicator?.CanFold(method) ?? false; + protected override IMethodNode CreateMethodEntrypointNode(MethodDesc method) { if (method.IsInternalCall) diff --git a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilationBuilder.cs b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilationBuilder.cs index d4c69d52a3aa93..bbec7799701b31 100644 --- a/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilationBuilder.cs +++ b/src/coreclr/tools/aot/ILCompiler.RyuJit/Compiler/RyuJitCompilationBuilder.cs @@ -136,14 +136,18 @@ public override ICompilation ToCompilation() if (_resilient) options |= RyuJitCompilationOptions.UseResilience; - ObjectDataInterner interner = _methodBodyFolding switch + MethodBodyDeduplicator methodBodyDeduplicator = _methodBodyFolding switch { - MethodBodyFoldingMode.Generic => new ObjectDataInterner(genericsOnly: true), - MethodBodyFoldingMode.All => new ObjectDataInterner(genericsOnly: false), - _ => ObjectDataInterner.Null, + MethodBodyFoldingMode.Generic => new MethodBodyDeduplicator(genericsOnly: true), + MethodBodyFoldingMode.All => new MethodBodyDeduplicator(genericsOnly: false), + _ => null, }; - var factory = new RyuJitNodeFactory(_context, _compilationGroup, _metadataManager, _interopStubManager, _nameMangler, _vtableSliceProvider, _dictionaryLayoutProvider, _inlinedThreadStatics, GetPreinitializationManager(), _devirtualizationManager, interner, _typeMapManager); + ObjectDataInterner interner = methodBodyDeduplicator is not null + ? new ObjectDataInterner(methodBodyDeduplicator) + : ObjectDataInterner.Null; + + var factory = new RyuJitNodeFactory(_context, _compilationGroup, _metadataManager, _interopStubManager, _nameMangler, _vtableSliceProvider, _dictionaryLayoutProvider, _inlinedThreadStatics, GetPreinitializationManager(), _devirtualizationManager, interner, methodBodyDeduplicator, _typeMapManager); JitConfigProvider.Initialize(_context.Target, jitFlagBuilder.ToArray(), _ryujitOptions, _jitPath); DependencyAnalyzerBase graph = CreateDependencyGraph(factory, new ObjectNode.ObjectNodeComparer(CompilerComparer.Instance)); From 99dccf9ebfbcb11ad41292738820deb1300d114c Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 25 Mar 2026 10:00:37 -0700 Subject: [PATCH 2/9] Move ObjectDataInterner to Common/Compiler and include in ReadyToRun Move ObjectDataInterner.cs (with IObjectDataDeduplicator interface) from ILCompiler.Compiler/Compiler/ to Common/Compiler/ so it can be shared across AOT projects. Add it to ILCompiler.ReadyToRun.csproj. The MethodBodyDeduplicator implementation stays in ILCompiler.Compiler. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Compiler/ObjectDataInterner.cs | 0 .../tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj | 2 +- .../aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) rename src/coreclr/tools/{aot/ILCompiler.Compiler => Common}/Compiler/ObjectDataInterner.cs (100%) diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ObjectDataInterner.cs b/src/coreclr/tools/Common/Compiler/ObjectDataInterner.cs similarity index 100% rename from src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ObjectDataInterner.cs rename to src/coreclr/tools/Common/Compiler/ObjectDataInterner.cs diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj index 73e6a054b99eba..a0e5f77c7c533b 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj @@ -504,7 +504,7 @@ - + diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj index cf18c52c27f3d2..dfb42007460488 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj @@ -71,6 +71,7 @@ + From fcf26c515a108320d7a9624ec654977ba267d6f1 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 25 Mar 2026 10:21:23 -0700 Subject: [PATCH 3/9] Add CopiedMethodILDeduplicator for ReadyToRun Add a new IObjectDataDeduplicator implementation that deduplicates CopiedMethodILNode instances based on their IL body bytes. Uses a HashSet with a custom key/comparer to correctly handle hash collisions. - Add Values property to ReadyToRun NodeCache for enumeration - Add ObjectDataInterner to ReadyToRun NodeFactory - Wire up CopiedMethodILDeduplicator in NodeFactory constructor Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ReadyToRun/CopiedMethodILDeduplicator.cs | 74 +++++++++++++++++++ .../ReadyToRunCodegenNodeFactory.cs | 6 ++ .../ILCompiler.ReadyToRun.csproj | 1 + 3 files changed, 81 insertions(+) create mode 100644 src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/CopiedMethodILDeduplicator.cs diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/CopiedMethodILDeduplicator.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/CopiedMethodILDeduplicator.cs new file mode 100644 index 00000000000000..f4589f4189baa5 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/CopiedMethodILDeduplicator.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +using ILCompiler.DependencyAnalysis; + +namespace ILCompiler.DependencyAnalysis.ReadyToRun +{ + public sealed class CopiedMethodILDeduplicator : IObjectDataDeduplicator + { + private readonly IEnumerable _nodes; + + public CopiedMethodILDeduplicator(IEnumerable nodes) + { + _nodes = nodes; + } + + public void DeduplicatePass(NodeFactory factory, Dictionary previousSymbolRemapping, Dictionary symbolRemapping) + { + var hashSet = new HashSet(new InternComparer(factory)); + + foreach (CopiedMethodILNode node in _nodes) + { + var key = new InternKey(node, factory); + if (hashSet.TryGetValue(key, out InternKey existing)) + { + symbolRemapping.TryAdd(node, existing.Node); + } + else + { + hashSet.Add(key); + } + } + } + + private sealed class InternKey + { + public CopiedMethodILNode Node { get; } + public int HashCode { get; } + + public InternKey(CopiedMethodILNode node, NodeFactory factory) + { + Node = node; + + ObjectNode.ObjectData data = node.GetData(factory, relocsOnly: false); + var hashCode = new HashCode(); + hashCode.AddBytes(data.Data); + HashCode = hashCode.ToHashCode(); + } + } + + private sealed class InternComparer : IEqualityComparer + { + private readonly NodeFactory _factory; + + public InternComparer(NodeFactory factory) => _factory = factory; + + public int GetHashCode(InternKey key) => key.HashCode; + + public bool Equals(InternKey a, InternKey b) + { + if (a.HashCode != b.HashCode) + return false; + + ObjectNode.ObjectData aData = a.Node.GetData(_factory, relocsOnly: false); + ObjectNode.ObjectData bData = b.Node.GetData(_factory, relocsOnly: false); + + return aData.Data.AsSpan().SequenceEqual(bData.Data); + } + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs index d544c741e2d11b..4391dc9210b561 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs @@ -45,6 +45,8 @@ public TValue GetOrAdd(TKey key) { return _cache.GetOrAdd(key, _creator); } + + public ICollection Values => _cache.Values; } public enum TypeValidationRule @@ -87,6 +89,8 @@ public sealed class NodeFactory public MetadataManager MetadataManager { get; } + public ObjectDataInterner ObjectInterner { get; } + public CompositeImageSettings CompositeImageSettings { get; set; } public readonly NodeFactoryOptimizationFlags OptimizationFlags; @@ -240,6 +244,8 @@ public NodeFactory( CreateNodeCaches(); + ObjectInterner = new ObjectDataInterner(new CopiedMethodILDeduplicator(_copiedMethodIL.Values)); + if (genericCycleBreadthCutoff >= 0 || genericCycleDepthCutoff >= 0) { _genericCycleDetector = new LazyGenericsSupport.GenericCycleDetector( diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj index dfb42007460488..54cba4f077a7fc 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj @@ -236,6 +236,7 @@ + From f9427e7921636e343efb335df9df4ad72868768b Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 25 Mar 2026 10:44:08 -0700 Subject: [PATCH 4/9] Enable ObjectDataInterner in ObjectWriter for ReadyToRun Remove the three #if !READYTORUN / #if READYTORUN guards around ObjectInterner.GetDeduplicatedSymbol calls in the common ObjectWriter. Now that ReadyToRun's NodeFactory has an ObjectDataInterner, the deduplication logic applies uniformly to both NativeAOT and ReadyToRun. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tools/Common/Compiler/ObjectWriter/ObjectWriter.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs index a4eca181141b60..7260def9eb90b4 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs @@ -400,14 +400,12 @@ public virtual void EmitObject(Stream outputFileStream, IReadOnlyCollection checksumRelocationsBuilder = default; foreach (Relocation reloc in blockToRelocate.Relocations) { -#if READYTORUN - ISymbolNode relocTarget = reloc.Target; -#else ISymbolNode relocTarget = _nodeFactory.ObjectInterner.GetDeduplicatedSymbol(_nodeFactory, reloc.Target); -#endif if (reloc.RelocType == RelocType.IMAGE_REL_FILE_CHECKSUM_CALLBACK) { From 9cf56de242062faf0816608e2bb0a6d4093a57c2 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 26 Mar 2026 14:37:48 -0700 Subject: [PATCH 5/9] Address review feedback from Copilot - Guard GetDeduplicatedSymbol call in ObjectWriter when symbolNode is null - Cache IL bytes in InternKey to avoid repeated PE reads during equality checks - Use Func> for deferred enumeration so the deduplicator sees the final cache contents after marking is complete Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Compiler/ObjectWriter/ObjectWriter.cs | 11 +++++---- .../ReadyToRun/CopiedMethodILDeduplicator.cs | 24 +++++++------------ .../ReadyToRunCodegenNodeFactory.cs | 2 +- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs b/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs index 7260def9eb90b4..b9c1cfcf1832a5 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectWriter/ObjectWriter.cs @@ -400,11 +400,14 @@ public virtual void EmitObject(Stream outputFileStream, IReadOnlyCollection _nodes; + private readonly Func> _nodesProvider; - public CopiedMethodILDeduplicator(IEnumerable nodes) + public CopiedMethodILDeduplicator(Func> nodesProvider) { - _nodes = nodes; + _nodesProvider = nodesProvider; } public void DeduplicatePass(NodeFactory factory, Dictionary previousSymbolRemapping, Dictionary symbolRemapping) { - var hashSet = new HashSet(new InternComparer(factory)); + var hashSet = new HashSet(new InternComparer()); - foreach (CopiedMethodILNode node in _nodes) + foreach (CopiedMethodILNode node in _nodesProvider()) { var key = new InternKey(node, factory); if (hashSet.TryGetValue(key, out InternKey existing)) @@ -39,24 +39,21 @@ private sealed class InternKey { public CopiedMethodILNode Node { get; } public int HashCode { get; } + public byte[] Data { get; } public InternKey(CopiedMethodILNode node, NodeFactory factory) { Node = node; - ObjectNode.ObjectData data = node.GetData(factory, relocsOnly: false); + Data = node.GetData(factory, relocsOnly: false).Data; var hashCode = new HashCode(); - hashCode.AddBytes(data.Data); + hashCode.AddBytes(Data); HashCode = hashCode.ToHashCode(); } } private sealed class InternComparer : IEqualityComparer { - private readonly NodeFactory _factory; - - public InternComparer(NodeFactory factory) => _factory = factory; - public int GetHashCode(InternKey key) => key.HashCode; public bool Equals(InternKey a, InternKey b) @@ -64,10 +61,7 @@ public bool Equals(InternKey a, InternKey b) if (a.HashCode != b.HashCode) return false; - ObjectNode.ObjectData aData = a.Node.GetData(_factory, relocsOnly: false); - ObjectNode.ObjectData bData = b.Node.GetData(_factory, relocsOnly: false); - - return aData.Data.AsSpan().SequenceEqual(bData.Data); + return a.Data.AsSpan().SequenceEqual(b.Data); } } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs index 4391dc9210b561..f18ae2b51389b4 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRunCodegenNodeFactory.cs @@ -244,7 +244,7 @@ public NodeFactory( CreateNodeCaches(); - ObjectInterner = new ObjectDataInterner(new CopiedMethodILDeduplicator(_copiedMethodIL.Values)); + ObjectInterner = new ObjectDataInterner(new CopiedMethodILDeduplicator(() => _copiedMethodIL.Values)); if (genericCycleBreadthCutoff >= 0 || genericCycleDepthCutoff >= 0) { From 05f0f3e41ceed1d097012b562af12957d1ff2115 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 7 Apr 2026 13:05:17 -0700 Subject: [PATCH 6/9] Set marking complete for composite component images --- .../Compiler/ReadyToRunCodegenCompilation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs index d254986e09d2a3..931a6cb846c99c 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs @@ -534,6 +534,7 @@ private void RewriteComponentFile(string inputFile, string outputFile, string ow } componentGraph.ComputeMarkedNodes(); componentFactory.Header.Add(Internal.Runtime.ReadyToRunSectionType.OwnerCompositeExecutable, ownerExecutableNode); + componentFactory.SetMarkingComplete(); ReadyToRunObjectWriter.EmitObject( outputFile, componentModule: inputModule, From 88dceb7ff03ec8b214f6612ed1180738cef2d498 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 7 Apr 2026 13:29:21 -0700 Subject: [PATCH 7/9] Address review feedback: robust convergence, caching, determinism - ObjectDataInterner: Replace count-only convergence check with full dictionary content comparison to detect mapping changes even when count stays the same. - CopiedMethodILDeduplicator: Cache deduplication results to avoid repeated GetData allocations across convergence loop iterations. - CopiedMethodILDeduplicator: Sort nodes with CompilerComparer before iterating to ensure deterministic canonical representative selection regardless of ConcurrentDictionary enumeration order. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Common/Compiler/ObjectDataInterner.cs | 19 ++++++++++- .../ReadyToRun/CopiedMethodILDeduplicator.cs | 34 +++++++++++++------ 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/coreclr/tools/Common/Compiler/ObjectDataInterner.cs b/src/coreclr/tools/Common/Compiler/ObjectDataInterner.cs index 1df1fb9f4932d9..d986e238ad3943 100644 --- a/src/coreclr/tools/Common/Compiler/ObjectDataInterner.cs +++ b/src/coreclr/tools/Common/Compiler/ObjectDataInterner.cs @@ -52,11 +52,28 @@ private void EnsureMap(NodeFactory factory) { deduplicator.DeduplicatePass(factory, previousSymbolRemapping, symbolRemapping); } - } while (previousSymbolRemapping == null || previousSymbolRemapping.Count < symbolRemapping.Count); + } while (!MappingsEqual(previousSymbolRemapping, symbolRemapping)); _symbolRemapping = symbolRemapping; } + private static bool MappingsEqual(Dictionary a, Dictionary b) + { + if (a is null) + return false; + + if (a.Count != b.Count) + return false; + + foreach (KeyValuePair kvp in a) + { + if (!b.TryGetValue(kvp.Key, out ISymbolNode value) || value != kvp.Value) + return false; + } + + return true; + } + public ISymbolNode GetDeduplicatedSymbol(NodeFactory factory, ISymbolNode original) { EnsureMap(factory); diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/CopiedMethodILDeduplicator.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/CopiedMethodILDeduplicator.cs index 9fff84e2e8b075..90948fc46e854c 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/CopiedMethodILDeduplicator.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/CopiedMethodILDeduplicator.cs @@ -11,6 +11,7 @@ namespace ILCompiler.DependencyAnalysis.ReadyToRun public sealed class CopiedMethodILDeduplicator : IObjectDataDeduplicator { private readonly Func> _nodesProvider; + private Dictionary _cachedMapping; public CopiedMethodILDeduplicator(Func> nodesProvider) { @@ -19,20 +20,33 @@ public CopiedMethodILDeduplicator(Func> nodesPro public void DeduplicatePass(NodeFactory factory, Dictionary previousSymbolRemapping, Dictionary symbolRemapping) { - var hashSet = new HashSet(new InternComparer()); - - foreach (CopiedMethodILNode node in _nodesProvider()) + if (_cachedMapping is null) { - var key = new InternKey(node, factory); - if (hashSet.TryGetValue(key, out InternKey existing)) - { - symbolRemapping.TryAdd(node, existing.Node); - } - else + _cachedMapping = new Dictionary(); + + var sortedNodes = new List(_nodesProvider()); + sortedNodes.Sort(CompilerComparer.Instance); + + var hashSet = new HashSet(new InternComparer()); + + foreach (CopiedMethodILNode node in sortedNodes) { - hashSet.Add(key); + var key = new InternKey(node, factory); + if (hashSet.TryGetValue(key, out InternKey existing)) + { + _cachedMapping[node] = existing.Node; + } + else + { + hashSet.Add(key); + } } } + + foreach (KeyValuePair entry in _cachedMapping) + { + symbolRemapping.TryAdd(entry.Key, entry.Value); + } } private sealed class InternKey From 066c5f48aa1d26455999a1920d86339654ec4bbf Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 7 Apr 2026 13:32:03 -0700 Subject: [PATCH 8/9] Don't deduplicate unmarked method il nodes --- .../ReadyToRun/CopiedMethodILDeduplicator.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/CopiedMethodILDeduplicator.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/CopiedMethodILDeduplicator.cs index 90948fc46e854c..879ce8d4076174 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/CopiedMethodILDeduplicator.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/CopiedMethodILDeduplicator.cs @@ -31,6 +31,12 @@ public void DeduplicatePass(NodeFactory factory, Dictionary Date: Tue, 14 Apr 2026 11:48:48 -0700 Subject: [PATCH 9/9] Update src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MethodBodyDeduplicator.cs Co-authored-by: Milos Kotlar --- .../aot/ILCompiler.Compiler/Compiler/MethodBodyDeduplicator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MethodBodyDeduplicator.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MethodBodyDeduplicator.cs index 2dd7f542b02b1a..94880f401f49b3 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MethodBodyDeduplicator.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MethodBodyDeduplicator.cs @@ -53,7 +53,7 @@ public void DeduplicatePass(NodeFactory factory, Dictionary