From f7fb9232c295c8377c048fdcdd46b5d85b5ed2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Fri, 15 Aug 2025 13:00:20 +0200 Subject: [PATCH 1/3] Add mechanism to allow CoreLib trimming to leverage whole program analysis A couple times in the past I needed a way to say "if X is not part of the program, eliminate the entire basic block". We can do this for allocated types (this is how branches under `is` checks elimination works), but we can't do this for more general "characteristics". This introduces a mechanism where AOT compiler and CoreLib (or System.Private.* universe in general) can define whole program tags such as "whole program has X in it" and CoreLib can condition code on the presence of this tag. This is easier shown than described, so I extracted the first use of this into a separate commit. In this commit, we eliminate code that tries looking for `StackTraceHiddenAttribute` if we know the whole program has no `StackTraceHiddenAttribute` in it. With this code eliminated, #118640 can then eliminate all custom attributes on methods, which in turn plays into #118718 and we can eliminate enum boxing even when StackTraceSupport is not set to false (right now #118718 really needs the StackTraceSupport=false to get rid of boxed enums; we get more boxed enums from method attributes). We have a new node that represents the characteristic. The node can be dropped into the graph wherever needed. ILScanner then uses this to condition parts of the method body on this characteristic node. We need similar logic in the substitution IL provider because we need to guarantee that RyuJIT is not going to see basic blocks we didn't scan. So we treat it as a substitution during codegen phase too. --- .../AnalysisCharacteristicAttribute.cs | 11 +++++++ .../AnalysisCharacteristicNode.cs | 26 ++++++++++++++++ .../DependencyAnalysis/NodeFactory.cs | 11 +++++++ .../ILCompiler.Compiler/Compiler/ILScanner.cs | 9 ++++++ .../Compiler/SubstitutedILProvider.cs | 30 ++++++++++++++++++- .../IL/ILImporter.Scanner.cs | 23 +++++++++++++- .../ILCompiler.Compiler.csproj | 1 + src/coreclr/tools/aot/ILCompiler/Program.cs | 2 +- 8 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/AnalysisCharacteristicAttribute.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalysisCharacteristicNode.cs diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/AnalysisCharacteristicAttribute.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/AnalysisCharacteristicAttribute.cs new file mode 100644 index 00000000000000..63dbbfe07e173e --- /dev/null +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/CompilerServices/AnalysisCharacteristicAttribute.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices +{ + // When applied to an intrinsic method, the method will become a characteristic check. + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + internal class AnalysisCharacteristicAttribute : Attribute + { + } +} diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalysisCharacteristicNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalysisCharacteristicNode.cs new file mode 100644 index 00000000000000..1d667fd833d72c --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalysisCharacteristicNode.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +using ILCompiler.DependencyAnalysisFramework; + +namespace ILCompiler.DependencyAnalysis +{ + public class AnalysisCharacteristicNode : DependencyNodeCore + { + public AnalysisCharacteristicNode(string characteristic) + => Characteristic = characteristic; + + public string Characteristic { get; } + + public override bool InterestingForDynamicDependencyAnalysis => false; + public override bool HasDynamicDependencies => false; + public override bool HasConditionalStaticDependencies => false; + public override bool StaticDependenciesAreComputed => true; + public override IEnumerable GetConditionalStaticDependencies(NodeFactory context) => null; + public override IEnumerable GetStaticDependencies(NodeFactory context) => null; + public override IEnumerable SearchDynamicDependencies(List> markedNodes, int firstNode, NodeFactory context) => null; + protected override string GetName(NodeFactory context) => $"Analysis characteristic: {Characteristic}"; + } +} 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 ef6a4fed1c1949..8afd930fb88383 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/NodeFactory.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/NodeFactory.cs @@ -604,6 +604,11 @@ private void CreateNodeCaches() return new ProxyTypeMapRequestNode(type); }); + _analysisCharacteristics = new NodeCache(c => + { + return new AnalysisCharacteristicNode(c); + }); + NativeLayout = new NativeLayoutHelper(this); } @@ -1526,6 +1531,12 @@ public ProxyTypeMapRequestNode ProxyTypeMapRequest(TypeDesc type) return _proxyTypeMapRequests.GetOrAdd(type); } + private NodeCache _analysisCharacteristics; + public AnalysisCharacteristicNode AnalysisCharacteristic(string ch) + { + return _analysisCharacteristics.GetOrAdd(ch); + } + /// /// Returns alternative symbol name that object writer should produce for given symbols /// in addition to the regular one. diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ILScanner.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ILScanner.cs index d9ad0888ee2f4b..18fdd2f65550eb 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ILScanner.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/ILScanner.cs @@ -310,6 +310,15 @@ public TypeMapManager GetTypeMapManager() return new ScannedTypeMapManager(_factory); } + public IEnumerable GetAnalysisCharacteristics() + { + foreach (DependencyNodeCore n in MarkedNodes) + { + if (n is AnalysisCharacteristicNode acn) + yield return acn.Characteristic; + } + } + private sealed class ScannedVTableProvider : VTableSliceProvider { private readonly Dictionary _vtableSlices = new Dictionary(); diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/SubstitutedILProvider.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/SubstitutedILProvider.cs index 8476dd391808bd..d455183bb0b5ae 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/SubstitutedILProvider.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/SubstitutedILProvider.cs @@ -10,6 +10,7 @@ using ILCompiler.DependencyAnalysis; using Internal.IL; +using Internal.IL.Stubs; using Internal.TypeSystem; using Internal.TypeSystem.Ecma; @@ -24,13 +25,15 @@ public class SubstitutedILProvider : ILProvider private readonly SubstitutionProvider _substitutionProvider; private readonly DevirtualizationManager _devirtualizationManager; private readonly MetadataManager _metadataManager; + private readonly HashSet _characteristics; - public SubstitutedILProvider(ILProvider nestedILProvider, SubstitutionProvider substitutionProvider, DevirtualizationManager devirtualizationManager, MetadataManager metadataManager = null) + public SubstitutedILProvider(ILProvider nestedILProvider, SubstitutionProvider substitutionProvider, DevirtualizationManager devirtualizationManager, MetadataManager metadataManager = null, IEnumerable characteristics = null) { _nestedILProvider = nestedILProvider; _substitutionProvider = substitutionProvider; _devirtualizationManager = devirtualizationManager; _metadataManager = metadataManager; + _characteristics = characteristics != null ? new HashSet(characteristics) : null; } public override MethodIL GetMethodIL(MethodDesc method) @@ -41,6 +44,13 @@ public override MethodIL GetMethodIL(MethodDesc method) return substitution.EmitIL(method); } + if (TryGetCharacteristicValue(method, out bool characteristicEnabled)) + { + return new ILStubMethodIL(method, + [characteristicEnabled ? (byte)ILOpCode.Ldc_i4_1 : (byte)ILOpCode.Ldc_i4_0, (byte)ILOpCode.Ret], + [], []); + } + // BEGIN TEMPORARY WORKAROUND // // The following lines should just be: @@ -819,6 +829,11 @@ private bool TryGetConstantArgument(MethodIL methodIL, byte[] body, OpcodeFlags[ { return true; } + else if (TryGetCharacteristicValue(method, out bool characteristic)) + { + constant = characteristic ? 1 : 0; + return true; + } else { constant = 0; @@ -1127,6 +1142,19 @@ private static bool ReadGetTypeFromHandle(ref ILReader reader, MethodIL methodIL return true; } + private bool TryGetCharacteristicValue(MethodDesc maybeCharacteristicMethod, out bool value) + { + if (maybeCharacteristicMethod.IsIntrinsic + && maybeCharacteristicMethod.HasCustomAttribute("System.Runtime.CompilerServices", "AnalysisCharacteristicAttribute")) + { + value = _characteristics == null || _characteristics.Contains(maybeCharacteristicMethod.Name); + return true; + } + + value = false; + return false; + } + private sealed class SubstitutedMethodIL : MethodIL { private readonly byte[] _body; diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs index 92e8126b482dd1..799a17899a6779 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/IL/ILImporter.Scanner.cs @@ -61,6 +61,8 @@ public enum ImportState : byte private bool _isReadOnly; private TypeDesc _constrained; + private int _currentInstructionOffset; + private int _previousInstructionOffset; private DependencyList _dependencies; private BasicBlock _lateBasicBlocks; @@ -258,6 +260,13 @@ private void StartImportingBasicBlock(BasicBlock basicBlock) _typeEqualityPatternAnalyzer = default; _isInstCheckPatternAnalyzer = default; + _currentInstructionOffset = 0; + _previousInstructionOffset = -1; + } + + private void StartImportingInstruction() + { + _currentInstructionOffset = _currentOffset; } partial void StartImportingInstruction(ILOpcode opcode) @@ -271,6 +280,8 @@ private void EndImportingInstruction() // The instruction should have consumed any prefixes. _constrained = null; _isReadOnly = false; + + _previousInstructionOffset = _currentInstructionOffset; } private void ImportCasting(ILOpcode opcode, int token) @@ -853,6 +864,17 @@ private void ImportBranch(ILOpcode opcode, BasicBlock target, BasicBlock fallthr } } + if (opcode == ILOpcode.brfalse && _previousInstructionOffset >= 0) + { + var reader = new ILReader(_ilBytes, _previousInstructionOffset); + if (reader.ReadILOpcode() == ILOpcode.call + && _methodIL.GetObject(reader.ReadILToken()) is MethodDesc { IsIntrinsic: true } intrinsicMethod + && intrinsicMethod.HasCustomAttribute("System.Runtime.CompilerServices", "AnalysisCharacteristicAttribute")) + { + condition = _factory.AnalysisCharacteristic(intrinsicMethod.Name); + } + } + ImportFallthrough(target); if (fallthrough != null) @@ -1531,7 +1553,6 @@ private DefType GetWellKnownType(WellKnownType wellKnownType) return _compilation.TypeSystemContext.GetWellKnownType(wellKnownType); } - private static void StartImportingInstruction() { } private static void ImportNop() { } private static void ImportBreak() { } private static void ImportLoadVar(int index, bool argument) { } diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj index 3ac5cfb27289d4..862404c49d4eb6 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj @@ -347,6 +347,7 @@ + diff --git a/src/coreclr/tools/aot/ILCompiler/Program.cs b/src/coreclr/tools/aot/ILCompiler/Program.cs index 6a2b680f279ab6..04795875d6d071 100644 --- a/src/coreclr/tools/aot/ILCompiler/Program.cs +++ b/src/coreclr/tools/aot/ILCompiler/Program.cs @@ -534,7 +534,7 @@ void RunScanner() substitutionProvider = new SubstitutionProvider(logger, featureSwitches, substitutions); - ilProvider = new SubstitutedILProvider(unsubstitutedILProvider, substitutionProvider, devirtualizationManager, metadataManager); + ilProvider = new SubstitutedILProvider(unsubstitutedILProvider, substitutionProvider, devirtualizationManager, metadataManager, scanResults.GetAnalysisCharacteristics()); // Use a more precise IL provider that uses whole program analysis for dead branch elimination builder.UseILProvider(ilProvider); From 383a4e839e0e30d6b50d2c89fe3f623357109280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Fri, 15 Aug 2025 13:02:33 +0200 Subject: [PATCH 2/3] Do not look for StackTraceHiddenAttribute if there's none in the app --- .../StackTraceMetadata/StackTraceMetadata.cs | 28 +++++++++------ .../System.Private.StackTraceMetadata.csproj | 2 ++ .../DependencyAnalysis/MethodMetadataNode.cs | 10 +++++- .../AttributeTrimming/AttributeTrimming.cs | 34 +++++++++++++++++++ .../AttributeTrimming.csproj | 13 +++++++ 5 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.cs create mode 100644 src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.csproj diff --git a/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/Internal/StackTraceMetadata/StackTraceMetadata.cs b/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/Internal/StackTraceMetadata/StackTraceMetadata.cs index 008119ebd10cad..352568239c0d4e 100644 --- a/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/Internal/StackTraceMetadata/StackTraceMetadata.cs +++ b/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/Internal/StackTraceMetadata/StackTraceMetadata.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Reflection.Runtime.General; +using System.Runtime.CompilerServices; using Internal.Metadata.NativeFormat; using Internal.NativeFormat; @@ -41,6 +42,10 @@ internal static void Initialize() RuntimeAugments.InitializeStackTraceMetadataSupport(new StackTraceMetadataCallbacksImpl()); } + [Intrinsic] + [AnalysisCharacteristic] + internal static extern bool StackTraceHiddenMetadataPresent(); + /// /// Locate the containing module for a method and try to resolve its name based on start address. /// @@ -75,21 +80,24 @@ public static unsafe string GetMethodNameFromStartAddressIfAvailable(IntPtr meth out TypeDefinitionHandle typeHandle, out MethodHandle methodHandle)) { - foreach (CustomAttributeHandle cah in reader.GetTypeDefinition(typeHandle).CustomAttributes) + if (StackTraceHiddenMetadataPresent()) { - if (cah.IsCustomAttributeOfType(reader, ["System", "Diagnostics"], "StackTraceHiddenAttribute")) + foreach (CustomAttributeHandle cah in reader.GetTypeDefinition(typeHandle).CustomAttributes) { - isStackTraceHidden = true; - break; + if (cah.IsCustomAttributeOfType(reader, ["System", "Diagnostics"], "StackTraceHiddenAttribute")) + { + isStackTraceHidden = true; + break; + } } - } - foreach (CustomAttributeHandle cah in reader.GetMethod(methodHandle).CustomAttributes) - { - if (cah.IsCustomAttributeOfType(reader, ["System", "Diagnostics"], "StackTraceHiddenAttribute")) + foreach (CustomAttributeHandle cah in reader.GetMethod(methodHandle).CustomAttributes) { - isStackTraceHidden = true; - break; + if (cah.IsCustomAttributeOfType(reader, ["System", "Diagnostics"], "StackTraceHiddenAttribute")) + { + isStackTraceHidden = true; + break; + } } } diff --git a/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/System.Private.StackTraceMetadata.csproj b/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/System.Private.StackTraceMetadata.csproj index cd40e0ec8ba54d..1fe8f87c09d76e 100644 --- a/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/System.Private.StackTraceMetadata.csproj +++ b/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/System.Private.StackTraceMetadata.csproj @@ -23,5 +23,7 @@ Internal\Runtime\StackTraceData.cs + + diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/MethodMetadataNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/MethodMetadataNode.cs index 8cccdf4b93ea93..f145aa9504eaaa 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/MethodMetadataNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/MethodMetadataNode.cs @@ -40,7 +40,9 @@ public MethodMetadataNode(MethodDesc method, bool isMinimal) public override IEnumerable GetStaticDependencies(NodeFactory factory) { DependencyList dependencies = new DependencyList(); - dependencies.Add(factory.TypeMetadata((MetadataType)_method.OwningType), "Owning type metadata"); + + var owningType = (MetadataType)_method.OwningType; + dependencies.Add(factory.TypeMetadata(owningType), "Owning type metadata"); if (!_isMinimal) { @@ -77,6 +79,12 @@ public override IEnumerable GetStaticDependencies(NodeFacto { GenericArgumentDataFlow.ProcessGenericArgumentDataFlow(ref dependencies, factory, new MessageOrigin(_method), parameterType, _method); } + + if (_method.HasCustomAttribute("System.Diagnostics", "StackTraceHiddenAttribute") + || owningType.HasCustomAttribute("System.Diagnostics", "StackTraceHiddenAttribute")) + { + dependencies.Add(factory.AnalysisCharacteristic("StackTraceHiddenMetadataPresent"), "Method is StackTraceHidden"); + } } return dependencies; diff --git a/src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.cs b/src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.cs new file mode 100644 index 00000000000000..7b4102200d4305 --- /dev/null +++ b/src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.cs @@ -0,0 +1,34 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Reflection; + +[Type] +class Program +{ + [Method] + static int Main() + { + // Sanity check: we don't currently expect attributes on types to be optimized away + if (GetTypeSecretly(nameof(TypeAttribute)) == null) + throw new Exception("Type"); + + // Main should be reflection visible + if (MethodBase.GetCurrentMethod().Name != nameof(Main)) + throw new Exception("Name"); + + // But we should have optimized out the attributes on it + if (GetTypeSecretly(nameof(MethodAttribute)) != null) + throw new Exception("Method"); + + return 100; + } + + [UnconditionalSuppressMessage("Trimming", "IL2057", Justification = "That's the point")] + static Type GetTypeSecretly(string name) => Type.GetType(name); +} + +class MethodAttribute : Attribute; +class TypeAttribute : Attribute; diff --git a/src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.csproj b/src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.csproj new file mode 100644 index 00000000000000..7ba6b7ad23544b --- /dev/null +++ b/src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.csproj @@ -0,0 +1,13 @@ + + + Exe + 0 + true + true + false + true + + + + + From d51a2c4c9b5b55417ba06aaf27f283e7ff5ab6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Fri, 15 Aug 2025 15:02:37 +0200 Subject: [PATCH 3/3] We need optimized S.P.StackTraceMetadata --- .../nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.cs | 2 ++ .../SmokeTests/AttributeTrimming/AttributeTrimming.csproj | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.cs b/src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.cs index 7b4102200d4305..863b2708f1b6bc 100644 --- a/src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.cs +++ b/src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.cs @@ -19,9 +19,11 @@ static int Main() if (MethodBase.GetCurrentMethod().Name != nameof(Main)) throw new Exception("Name"); +#if !DEBUG // But we should have optimized out the attributes on it if (GetTypeSecretly(nameof(MethodAttribute)) != null) throw new Exception("Method"); +#endif return 100; } diff --git a/src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.csproj b/src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.csproj index 7ba6b7ad23544b..e4650db0af37db 100644 --- a/src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.csproj +++ b/src/tests/nativeaot/SmokeTests/AttributeTrimming/AttributeTrimming.csproj @@ -5,7 +5,6 @@ true true false - true