Skip to content

Commit

Permalink
Inline resource strings in the compiler (#80896)
Browse files Browse the repository at this point in the history
On a high level:
* When we're looking at IL to do substitutions, we additionally look for calls to `SR.SomeResourceName`. These are generated properties (generated by a piece of code in Arcade) that basically just do `GetResourceString(nameof(SomeResourceName))`. We look up what the resource string is (in the manifest resource) and replace the call with the looked up string literal.
* We also keep track of calls to `SR.GetResourceString`. Seeing this in the graph means that the optimization was defeated - someone bypassed the generated accessors. If we see one, we add dependency graph node to the graph that represent the manifest resource that has the string.
* When generating managed resources we skip over the one that has the strings unless the above dependency node is in the graph. This allows optimizing away the resource blobs if all accesses were inlined.
  • Loading branch information
MichalStrehovsky committed Jan 30, 2023
1 parent b6935d3 commit 759fecb
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,16 @@ internal void AppendToStackTrace(StringBuilder builder)
{
// Passing a default string for "at" in case SR.UsingResourceKeys() is true
// as this is a special case and we don't want to have "Word_At" on stack traces.
string word_At = SR.GetResourceString(nameof(SR.Word_At), defaultString: "at");
string word_At = SR.UsingResourceKeys() ? "at" : SR.Word_At;
builder.Append(" ").Append(word_At).Append(' ');
builder.AppendLine(DeveloperExperience.Default.CreateStackTraceString(_ipAddress, _needFileInfo));
}
if (_isLastFrameFromForeignExceptionStackTrace)
{
// Passing default for Exception_EndStackTraceFromPreviousThrow in case SR.UsingResourceKeys is set.
builder.AppendLine(SR.GetResourceString(nameof(SR.Exception_EndStackTraceFromPreviousThrow),
defaultString: "--- End of stack trace from previous location ---"));
builder.AppendLine(SR.UsingResourceKeys() ?
"--- End of stack trace from previous location ---" :
SR.Exception_EndStackTraceFromPreviousThrow);
}
}
}
Expand Down
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
Expand Up @@ -462,6 +462,11 @@ private void CreateNodeCaches()
return new ModuleMetadataNode(module);
});

_inlineableStringResources = new NodeCache<EcmaModule, InlineableStringsResourceNode>(module =>
{
return new InlineableStringsResourceNode(module);
});

_customAttributesWithMetadata = new NodeCache<ReflectableCustomAttribute, CustomAttributeMetadataNode>(ca =>
{
return new CustomAttributeMetadataNode(ca);
Expand Down Expand Up @@ -1139,6 +1144,12 @@ internal ModuleMetadataNode ModuleMetadata(ModuleDesc module)
return _modulesWithMetadata.GetOrAdd(module);
}

private NodeCache<EcmaModule, InlineableStringsResourceNode> _inlineableStringResources;
internal InlineableStringsResourceNode InlineableStringResource(EcmaModule module)
{
return _inlineableStringResources.GetOrAdd(module);
}

private NodeCache<ReflectableCustomAttribute, CustomAttributeMetadataNode> _customAttributesWithMetadata;

internal CustomAttributeMetadataNode CustomAttributeMetadata(ReflectableCustomAttribute ca)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public IReadOnlyList<ResourceIndexData> GetOrCreateIndexData(NodeFactory factory
string resourceName = module.MetadataReader.GetString(resource.Name);

// Check if emitting the manifest resource is blocked by policy.
if (factory.MetadataManager.IsManifestResourceBlocked(module, resourceName))
if (factory.MetadataManager.IsManifestResourceBlocked(factory, module, resourceName))
continue;

string assemblyName = module.GetName().FullName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
}
Expand All @@ -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;

ILExceptionRegion[] ehRegions = method.GetExceptionRegions();
byte[] methodBytes = method.GetILBytes();
OpcodeFlags[] flags = new OpcodeFlags[methodBytes.Length];
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -405,7 +435,7 @@ public MethodIL GetMethodILWithInlinedSubstitutions(MethodIL method)
}
}

if (!hasUnmarkedInstruction)
if (!hasUnmarkedInstruction && !hasGetResourceStringCall)
return method;

byte[] newBody = (byte[])methodBytes.Clone();
Expand Down Expand Up @@ -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);

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)
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

Expand All @@ -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)
Expand All @@ -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)
{
Expand Down Expand Up @@ -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;
}
}
}
}
}
Expand Down
Loading

0 comments on commit 759fecb

Please sign in to comment.