-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Inline resource strings in the compiler #80896
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
22d8052
6064206
1280224
d15a4a9
4878af6
53ada6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| // 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.DependencyAnalysisFramework; | ||
| using Internal.TypeSystem; | ||
| using Internal.TypeSystem.Ecma; | ||
|
|
||
| namespace ILCompiler.DependencyAnalysis | ||
| { | ||
| /// <summary> | ||
| /// Represents a resource blob used by the SR class in the BCL. | ||
| /// If this node is present in the graph, it means we were not able to optimize its use away | ||
| /// and the blob has to be generated. | ||
| /// </summary> | ||
| internal sealed class InlineableStringsResourceNode : DependencyNodeCore<NodeFactory> | ||
| { | ||
| private readonly EcmaModule _module; | ||
|
|
||
| public const string ResourceAccessorTypeName = "SR"; | ||
| public const string ResourceAccessorTypeNamespace = "System"; | ||
| public const string ResourceAccessorGetStringMethodName = "GetResourceString"; | ||
|
|
||
| public InlineableStringsResourceNode(EcmaModule module) | ||
| { | ||
| _module = module; | ||
| } | ||
|
|
||
| public override bool InterestingForDynamicDependencyAnalysis => false; | ||
|
|
||
| public override bool HasDynamicDependencies => false; | ||
|
|
||
| public override bool HasConditionalStaticDependencies => false; | ||
|
|
||
| public override bool StaticDependenciesAreComputed => true; | ||
|
|
||
| public static bool IsInlineableStringsResource(EcmaModule module, string resourceName) | ||
| { | ||
| if (!resourceName.EndsWith(".resources", StringComparison.Ordinal)) | ||
| return false; | ||
|
|
||
| // Make a guess at the name of the resource Arcade tooling generated for the resource | ||
| // strings. | ||
| // https://github.com/dotnet/runtime/issues/81385 tracks not having to guess this. | ||
| string simpleName = module.Assembly.GetName().Name; | ||
| string resourceName1 = $"{simpleName}.Strings.resources"; | ||
| string resourceName2 = $"FxResources.{simpleName}.SR.resources"; | ||
|
|
||
| if (resourceName != resourceName1 && resourceName != resourceName2) | ||
| return false; | ||
|
|
||
| MetadataType srType = module.GetType(ResourceAccessorTypeNamespace, ResourceAccessorTypeName, throwIfNotFound: false); | ||
| if (srType == null) | ||
| return false; | ||
|
|
||
| return srType.GetMethod(ResourceAccessorGetStringMethodName, null) != null; | ||
| } | ||
|
|
||
| public static void AddDependenciesDueToResourceStringUse(ref DependencyList dependencies, NodeFactory factory, MethodDesc method) | ||
| { | ||
| if (method.Name == ResourceAccessorGetStringMethodName && method.OwningType is MetadataType mdType | ||
| && mdType.Name == ResourceAccessorTypeName && mdType.Namespace == ResourceAccessorTypeNamespace) | ||
| { | ||
| dependencies ??= new DependencyList(); | ||
| dependencies.Add(factory.InlineableStringResource((EcmaModule)mdType.Module), "Using the System.SR class"); | ||
| } | ||
| } | ||
|
|
||
| public override IEnumerable<CombinedDependencyListEntry> GetConditionalStaticDependencies(NodeFactory context) => null; | ||
| public override IEnumerable<DependencyListEntry> GetStaticDependencies(NodeFactory context) => null; | ||
| public override IEnumerable<CombinedDependencyListEntry> SearchDynamicDependencies(List<DependencyNodeCore<NodeFactory>> markedNodes, int firstNode, NodeFactory context) => null; | ||
| protected override string GetName(NodeFactory context) | ||
| => $"String resources for {_module.Assembly.GetName().Name}"; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,11 @@ | |
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Reflection.Metadata; | ||
| using System.Reflection.Metadata.Ecma335; | ||
| using System.Reflection.PortableExecutable; | ||
| using System.Resources; | ||
|
|
||
| using ILCompiler.DependencyAnalysis; | ||
|
|
||
| using Internal.IL; | ||
| using Internal.TypeSystem; | ||
|
|
@@ -114,6 +118,9 @@ private enum OpcodeFlags : byte | |
| // (Lets us avoid seeing lots of small basic blocks within eliminated chunks.) | ||
| VisibleBasicBlockStart = 0x10, | ||
|
|
||
| // This is a potential SR.get_SomeResourceString call. | ||
| GetResourceStringCall = 0x20, | ||
|
|
||
| // The instruction at this offset is reachable | ||
| Mark = 0x80, | ||
| } | ||
|
|
@@ -139,9 +146,17 @@ public MethodIL GetMethodILWithInlinedSubstitutions(MethodIL method) | |
| // Last step is a sweep - we replace the tail of all unreachable blocks with "br $-2" | ||
| // and nop out the rest. If the basic block is smaller than 2 bytes, we don't touch it. | ||
| // We also eliminate any EH records that correspond to the stubbed out basic block. | ||
| // | ||
| // We also attempt to rewrite calls to SR.SomeResourceString accessors with string | ||
| // literals looked up from the managed resources. | ||
|
|
||
| Debug.Assert(method.GetMethodILDefinition() == method); | ||
|
|
||
| // Do not attempt to inline resource strings if we only want to use resource keys. | ||
| // The optimizations are not compatible. | ||
| bool shouldInlineResourceStrings = | ||
| !_hashtable._switchValues.TryGetValue("System.Resources.UseSystemResourceKeys", out bool useResourceKeys) || !useResourceKeys; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any estimate on additional saving we would get if this feature also worked with
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This will always be inlined - for the below app: Console.WriteLine(new NullReferenceException().Message);
throw null;If I compile this with .NET 7 as
vitek-karas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ILExceptionRegion[] ehRegions = method.GetExceptionRegions(); | ||
| byte[] methodBytes = method.GetILBytes(); | ||
| OpcodeFlags[] flags = new OpcodeFlags[methodBytes.Length]; | ||
|
|
@@ -243,6 +258,8 @@ public MethodIL GetMethodILWithInlinedSubstitutions(MethodIL method) | |
| } | ||
| } | ||
|
|
||
| bool hasGetResourceStringCall = false; | ||
|
|
||
| // Mark all reachable basic blocks | ||
| // | ||
| // We also do another round of basic block marking to mark beginning of visible basic blocks | ||
|
|
@@ -384,6 +401,19 @@ public MethodIL GetMethodILWithInlinedSubstitutions(MethodIL method) | |
| if (reader.HasNext) | ||
| flags[reader.Offset] |= OpcodeFlags.VisibleBasicBlockStart; | ||
| } | ||
| else if (shouldInlineResourceStrings && opcode == ILOpcode.call) | ||
| { | ||
| var callee = method.GetObject(reader.ReadILToken(), NotFoundBehavior.ReturnNull) as EcmaMethod; | ||
| if (callee != null && callee.IsSpecialName && callee.OwningType is EcmaType calleeType | ||
| && calleeType.Name == InlineableStringsResourceNode.ResourceAccessorTypeName | ||
| && calleeType.Namespace == InlineableStringsResourceNode.ResourceAccessorTypeNamespace | ||
| && callee.Signature is { Length: 0, IsStatic: true } | ||
| && callee.Name.StartsWith("get_", StringComparison.Ordinal)) | ||
| { | ||
| flags[offset] |= OpcodeFlags.GetResourceStringCall; | ||
| hasGetResourceStringCall = true; | ||
| } | ||
| } | ||
| else | ||
| { | ||
| reader.Skip(opcode); | ||
|
|
@@ -405,7 +435,7 @@ public MethodIL GetMethodILWithInlinedSubstitutions(MethodIL method) | |
| } | ||
| } | ||
|
|
||
| if (!hasUnmarkedInstruction) | ||
| if (!hasUnmarkedInstruction && !hasGetResourceStringCall) | ||
| return method; | ||
|
|
||
| byte[] newBody = (byte[])methodBytes.Clone(); | ||
|
|
@@ -472,7 +502,47 @@ public MethodIL GetMethodILWithInlinedSubstitutions(MethodIL method) | |
| debugInfo = new SubstitutedDebugInformation(debugInfo, sequencePoints.ToArray()); | ||
| } | ||
|
|
||
| return new SubstitutedMethodIL(method, newBody, newEHRegions.ToArray(), debugInfo); | ||
| // We only optimize EcmaMethods because there we can find out the highest string token RID | ||
| // in use. | ||
| ArrayBuilder<string> newStrings = default; | ||
| if (hasGetResourceStringCall && method.GetMethodILDefinition() is EcmaMethodIL ecmaMethodIL) | ||
| { | ||
| // We're going to inject new string tokens. Start where the last token of the module left off. | ||
| // We don't need this token to be globally unique because all token resolution happens in the context | ||
| // of a MethodIL and we're making a new one here. It just has to be unique to the MethodIL. | ||
| int tokenRid = ecmaMethodIL.Module.MetadataReader.GetHeapSize(HeapIndex.UserString); | ||
vitek-karas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| for (int offset = 0; offset < flags.Length; offset++) | ||
| { | ||
| if ((flags[offset] & OpcodeFlags.GetResourceStringCall) == 0) | ||
| continue; | ||
|
|
||
| Debug.Assert(newBody[offset] == (byte)ILOpcode.call); | ||
| var getter = (EcmaMethod)method.GetObject(new ILReader(newBody, offset + 1).ReadILToken()); | ||
|
|
||
| // If we can't get the string, this might be something else. | ||
| string resourceString = GetResourceStringForAccessor(getter); | ||
| if (resourceString == null) | ||
| continue; | ||
|
|
||
| // If we ran out of tokens, we can't optimize anymore. | ||
| if (tokenRid > 0xFFFFFF) | ||
| continue; | ||
|
|
||
| newStrings.Add(resourceString); | ||
|
|
||
| // call and ldstr are both 5-byte instructions: opcode followed by a token. | ||
| newBody[offset] = (byte)ILOpcode.ldstr; | ||
| newBody[offset + 1] = (byte)tokenRid; | ||
| newBody[offset + 2] = (byte)(tokenRid >> 8); | ||
| newBody[offset + 3] = (byte)(tokenRid >> 16); | ||
| newBody[offset + 4] = TokenTypeString; | ||
|
|
||
| tokenRid++; | ||
| } | ||
| } | ||
|
|
||
| return new SubstitutedMethodIL(method, newBody, newEHRegions.ToArray(), debugInfo, newStrings.ToArray()); | ||
| } | ||
|
|
||
| private bool TryGetConstantArgument(MethodIL methodIL, byte[] body, OpcodeFlags[] flags, int offset, int argIndex, out int constant) | ||
|
|
@@ -641,19 +711,36 @@ private bool TryGetConstantArgument(MethodIL methodIL, byte[] body, OpcodeFlags[ | |
| return false; | ||
| } | ||
|
|
||
| private string GetResourceStringForAccessor(EcmaMethod method) | ||
| { | ||
| Debug.Assert(method.Name.StartsWith("get_", StringComparison.Ordinal)); | ||
| string resourceStringName = method.Name.Substring(4); | ||
|
|
||
| Dictionary<string, string> dict = _hashtable.GetOrCreateValue(method.Module).InlineableResourceStrings; | ||
| if (dict != null | ||
| && dict.TryGetValue(resourceStringName, out string result)) | ||
| { | ||
| return result; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| private sealed class SubstitutedMethodIL : MethodIL | ||
| { | ||
| private readonly byte[] _body; | ||
| private readonly ILExceptionRegion[] _ehRegions; | ||
| private readonly MethodIL _wrappedMethodIL; | ||
| private readonly MethodDebugInformation _debugInfo; | ||
| private readonly string[] _newStrings; | ||
|
|
||
| public SubstitutedMethodIL(MethodIL wrapped, byte[] body, ILExceptionRegion[] ehRegions, MethodDebugInformation debugInfo) | ||
| public SubstitutedMethodIL(MethodIL wrapped, byte[] body, ILExceptionRegion[] ehRegions, MethodDebugInformation debugInfo, string[] newStrings) | ||
| { | ||
| _wrappedMethodIL = wrapped; | ||
| _body = body; | ||
| _ehRegions = ehRegions; | ||
| _debugInfo = debugInfo; | ||
| _newStrings = newStrings; | ||
| } | ||
|
|
||
| public override MethodDesc OwningMethod => _wrappedMethodIL.OwningMethod; | ||
|
|
@@ -662,7 +749,23 @@ public SubstitutedMethodIL(MethodIL wrapped, byte[] body, ILExceptionRegion[] eh | |
| public override ILExceptionRegion[] GetExceptionRegions() => _ehRegions; | ||
| public override byte[] GetILBytes() => _body; | ||
| public override LocalVariableDefinition[] GetLocals() => _wrappedMethodIL.GetLocals(); | ||
| public override object GetObject(int token, NotFoundBehavior notFoundBehavior) => _wrappedMethodIL.GetObject(token, notFoundBehavior); | ||
| public override object GetObject(int token, NotFoundBehavior notFoundBehavior) | ||
| { | ||
| // If this is a string token, it could be one of the new string tokens we injected. | ||
| if ((token >>> 24) == TokenTypeString | ||
| && _wrappedMethodIL.GetMethodILDefinition() is EcmaMethodIL ecmaMethodIL) | ||
| { | ||
| int rid = token & 0xFFFFFF; | ||
| int maxRealTokenRid = ecmaMethodIL.Module.MetadataReader.GetHeapSize(HeapIndex.UserString); | ||
| if (rid >= maxRealTokenRid) | ||
| { | ||
| // Yep, string injected by us. | ||
| return _newStrings[rid - maxRealTokenRid]; | ||
| } | ||
| } | ||
|
|
||
| return _wrappedMethodIL.GetObject(token, notFoundBehavior); | ||
| } | ||
| public override MethodDebugInformation GetDebugInfo() => _debugInfo; | ||
| } | ||
|
|
||
|
|
@@ -682,9 +785,11 @@ public SubstitutedDebugInformation(MethodDebugInformation originalDebugInformati | |
| public override IEnumerable<ILSequencePoint> GetSequencePoints() => _sequencePoints; | ||
| } | ||
|
|
||
| private const int TokenTypeString = 0x70; // CorTokenType for strings | ||
|
|
||
| private sealed class FeatureSwitchHashtable : LockFreeReaderHashtable<EcmaModule, AssemblyFeatureInfo> | ||
| { | ||
| private readonly Dictionary<string, bool> _switchValues; | ||
| internal readonly Dictionary<string, bool> _switchValues; | ||
| private readonly Logger _logger; | ||
|
|
||
| public FeatureSwitchHashtable(Logger logger, Dictionary<string, bool> switchValues) | ||
|
|
@@ -710,6 +815,7 @@ private sealed class AssemblyFeatureInfo | |
|
|
||
| public Dictionary<MethodDesc, BodySubstitution> BodySubstitutions { get; } | ||
| public Dictionary<FieldDesc, object> FieldSubstitutions { get; } | ||
| public Dictionary<string, string> InlineableResourceStrings { get; } | ||
|
|
||
| public AssemblyFeatureInfo(EcmaModule module, Logger logger, IReadOnlyDictionary<string, bool> featureSwitchValues) | ||
| { | ||
|
|
@@ -741,6 +847,27 @@ public AssemblyFeatureInfo(EcmaModule module, Logger logger, IReadOnlyDictionary | |
|
|
||
| (BodySubstitutions, FieldSubstitutions) = BodySubstitutionsParser.GetSubstitutions(logger, module.Context, ms, resource, module, "name", featureSwitchValues); | ||
| } | ||
| else if (InlineableStringsResourceNode.IsInlineableStringsResource(module, resourceName)) | ||
| { | ||
| BlobReader reader = resourceDirectory.GetReader((int)resource.Offset, resourceDirectory.Length - (int)resource.Offset); | ||
| int length = (int)reader.ReadUInt32(); | ||
|
|
||
| UnmanagedMemoryStream ms; | ||
| unsafe | ||
| { | ||
| ms = new UnmanagedMemoryStream(reader.CurrentPointer, length); | ||
| } | ||
|
|
||
| InlineableResourceStrings = new Dictionary<string, string>(); | ||
|
|
||
| using var resReader = new ResourceReader(ms); | ||
| var enumerator = resReader.GetEnumerator(); | ||
| while (enumerator.MoveNext()) | ||
| { | ||
| if (enumerator.Key is string key && enumerator.Value is string value) | ||
| InlineableResourceStrings[key] = value; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.