Skip to content
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

Place frozen objects into dehydrated section #78688

Merged
merged 2 commits into from
Nov 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,34 +1,56 @@
// 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 System;

using Internal.Text;
using Internal.TypeSystem;

namespace ILCompiler.DependencyAnalysis
{
public class ArrayOfFrozenObjectsNode<TEmbedded> : ArrayOfEmbeddedDataNode<TEmbedded>
where TEmbedded : EmbeddedObjectNode
public class ArrayOfFrozenObjectsNode : DehydratableObjectNode, ISymbolDefinitionNode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dehydratable I assume means serializable. This is an interesting design. What happens if there are cycles in this graph? Are they correctly preserved?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dehydratable means we will run the code from #77884 on this and "dehydrate" pointers and excess zeros in the data blob this node generated into a more succinct representation in the executable image. It will get "rehydrated" at startup.

For cycles in the graph of frozen objects: those can't happen - we don't allow freezing objects with mutable reference-typed fields. The write barriers don't handle frozen segment and the GC wouldn't handle references from those either. The freezing/serialization algorithm wouldn't probably have problems with it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not having writeable fields does not prevent cycles. The following will have a cycle:

class c1
{
    private readonly c1 Self;
    c1() => Self = this;
}

Can we have this in frozen objects?

(I am mostly curious if there are other restrictions that will prevent cycles. I do not think dehydration will have problems)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That one bails at the stfld:

if (field.FieldType.IsGCPointer && value != null)
{
return Status.Fail(methodIL.OwningMethod, opcode, "Reference field");
}

In theory the cycle should be fine because the reference would be just a reloc to another symbol and the data blob associated with that symbol has another reloc.

I think the only reference-typed instance fields we currently really allow are those of delegates to instance methods, and those are pretty special.

{
public ArrayOfFrozenObjectsNode(string startSymbolMangledName, string endSymbolMangledName, IComparer<TEmbedded> nodeSorter) : base(startSymbolMangledName, endSymbolMangledName, nodeSorter)
private readonly ObjectAndOffsetSymbolNode _endSymbol;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this get dehyrated as a pointer? Or written inline?

If the former, is there a big benefit to the extra pointer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This defines an additional symbol - it's up to whoever references it how they'll do it. ReadyToRun header currently references it as a full pointer. It doesn't have to. It's one of those things I'm planning to convert to 32bit relative relocs (although this one doesn't contribute much).

public ISymbolNode EndSymbol => _endSymbol;

public ArrayOfFrozenObjectsNode()
{
_endSymbol = new ObjectAndOffsetSymbolNode(this, 0, "__FrozenSegmentEnd", true);
}

public void AppendMangledName(NameMangler nameMangler, Utf8StringBuilder sb)
=> sb.Append(nameMangler.CompilationUnitPrefix).Append("__FrozenSegmentStart");

public int Offset => 0;

private static void AlignNextObject(ref ObjectDataBuilder builder, NodeFactory factory)
{
builder.EmitZeros(AlignmentHelper.AlignUp(builder.CountBytes, factory.Target.PointerSize) - builder.CountBytes);
}

protected override void GetElementDataForNodes(ref ObjectDataBuilder builder, NodeFactory factory, bool relocsOnly)
protected override ObjectData GetDehydratableData(NodeFactory factory, bool relocsOnly)
{
foreach (EmbeddedObjectNode node in NodesList)
// This is a summary node
if (relocsOnly)
return new ObjectData(Array.Empty<byte>(), Array.Empty<Relocation>(), 1, Array.Empty<ISymbolDefinitionNode>());

var builder = new ObjectDataBuilder(factory, relocsOnly);
builder.AddSymbol(this);
foreach (EmbeddedObjectNode node in factory.MetadataManager.GetFrozenObjects())
{
AlignNextObject(ref builder, factory);

if (!relocsOnly)
node.InitializeOffsetFromBeginningOfArray(builder.CountBytes);
node.InitializeOffsetFromBeginningOfArray(builder.CountBytes);

int initialOffset = builder.CountBytes;
sbomer marked this conversation as resolved.
Show resolved Hide resolved
node.EncodeData(ref builder, factory, relocsOnly);
int objectSize = builder.CountBytes - initialOffset;
int minimumObjectSize = EETypeNode.GetMinimumObjectSize(factory.TypeSystemContext);
if (objectSize < minimumObjectSize)
{
builder.EmitZeros(minimumObjectSize - objectSize);
}

if (node is ISymbolDefinitionNode)
{
builder.AddSymbol((ISymbolDefinitionNode)node);
Expand All @@ -38,8 +60,20 @@ protected override void GetElementDataForNodes(ref ObjectDataBuilder builder, No
// Terminate with a null pointer as expected by the GC
AlignNextObject(ref builder, factory);
builder.EmitZeroPointer();

_endSymbol.SetSymbolOffset(builder.CountBytes);
builder.AddSymbol(_endSymbol);

return builder.ToObjectData();
}

protected override ObjectNodeSection GetDehydratedSection(NodeFactory factory) => ObjectNodeSection.DataSection;
protected override string GetName(NodeFactory factory) => this.GetMangledName(factory.NameMangler);

public override int ClassCode => -1771336339;

public override bool IsShareable => false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably missed it, but I couldn't find any references to this. What does it do?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a (mostly test-only) mode where each managed assembly can be compiled into a separate object file, and then linked into an EXE by the linker.

In this mode, some nodes can end up being duplicated (e.g. List<int>.Add) because we don't know if any other object file could be defining that (the compilation is done in isolation). We mark such nodes as shareable. Object writing will place them into a COMDAT section and that allows linker to pick any definition (it would error out otherwise).


public override bool StaticDependenciesAreComputed => true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace ILCompiler.DependencyAnalysis
/// Represents a frozen object that is statically preallocated within the data section
/// of the executable instead of on the GC heap.
/// </summary>
public class FrozenObjectNode : EmbeddedObjectNode, ISymbolDefinitionNode
public sealed class FrozenObjectNode : EmbeddedObjectNode, ISymbolDefinitionNode
{
private readonly MetadataType _owningType;
private readonly TypePreinit.ISerializableReference _data;
Expand Down Expand Up @@ -58,20 +58,11 @@ int ISymbolDefinitionNode.Offset

public override void EncodeData(ref ObjectDataBuilder dataBuilder, NodeFactory factory, bool relocsOnly)
{
int initialOffset = dataBuilder.CountBytes;

// Sync Block
dataBuilder.EmitZeroPointer();

// byte contents
_data.WriteContent(ref dataBuilder, this, factory);

int objectSize = dataBuilder.CountBytes - initialOffset;
int minimumObjectSize = EETypeNode.GetMinimumObjectSize(factory.TypeSystemContext);
if (objectSize < minimumObjectSize)
{
dataBuilder.EmitZeros(minimumObjectSize - objectSize);
}
}

protected override string GetName(NodeFactory factory) => this.GetMangledName(factory.NameMangler);
Expand All @@ -95,11 +86,6 @@ public override IEnumerable<DependencyListEntry> GetStaticDependencies(NodeFacto
return dependencies;
}

protected override void OnMarked(NodeFactory factory)
{
factory.FrozenSegmentRegion.AddEmbeddedObject(this);
}

public override int ClassCode => 1789429316;

public override int CompareToImpl(ISortableNode other, CompilerComparer comparer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace ILCompiler.DependencyAnalysis
{
public class FrozenStringNode : EmbeddedObjectNode, ISymbolDefinitionNode
public sealed class FrozenStringNode : EmbeddedObjectNode, ISymbolDefinitionNode
{
private string _data;
private int _syncBlockSize;
Expand Down Expand Up @@ -59,8 +59,6 @@ private static IEETypeNode GetEETypeNode(NodeFactory factory)

public override void EncodeData(ref ObjectDataBuilder dataBuilder, NodeFactory factory, bool relocsOnly)
{
int initialOffset = dataBuilder.CountBytes;

dataBuilder.EmitZeroPointer(); // Sync block

dataBuilder.EmitPointerReloc(GetEETypeNode(factory));
Expand All @@ -74,13 +72,6 @@ public override void EncodeData(ref ObjectDataBuilder dataBuilder, NodeFactory f

// Null-terminate for friendliness with interop
dataBuilder.EmitShort(0);

int objectSize = dataBuilder.CountBytes - initialOffset;
int minimumObjectSize = EETypeNode.GetMinimumObjectSize(factory.TypeSystemContext);
if (objectSize < minimumObjectSize)
{
dataBuilder.EmitZeros(minimumObjectSize - objectSize);
}
}

protected override string GetName(NodeFactory factory) => this.GetMangledName(factory.NameMangler);
Expand All @@ -93,11 +84,6 @@ public override IEnumerable<DependencyListEntry> GetStaticDependencies(NodeFacto
};
}

protected override void OnMarked(NodeFactory factory)
{
factory.FrozenSegmentRegion.AddEmbeddedObject(this);
}

public override int ClassCode => -1733946122;

public override int CompareToImpl(ISortableNode other, CompilerComparer comparer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1122,10 +1122,7 @@ public string GetSymbolAlternateName(ISymbolNode node)
"__DispatchMapTableEnd",
new SortableDependencyNode.ObjectNodeComparer(CompilerComparer.Instance));

public ArrayOfEmbeddedDataNode<EmbeddedObjectNode> FrozenSegmentRegion = new ArrayOfFrozenObjectsNode<EmbeddedObjectNode>(
"__FrozenSegmentRegionStart",
"__FrozenSegmentRegionEnd",
new SortableDependencyNode.EmbeddedObjectNodeComparer(CompilerComparer.Instance));
public ArrayOfFrozenObjectsNode FrozenSegmentRegion = new ArrayOfFrozenObjectsNode();

internal ModuleInitializerListNode ModuleInitializerList = new ModuleInitializerListNode();

Expand Down Expand Up @@ -1158,7 +1155,7 @@ public virtual void AttachToDependencyGraph(DependencyAnalyzerBase<NodeFactory>
ReadyToRunHeader.Add(ReadyToRunSectionType.EagerCctor, EagerCctorTable, EagerCctorTable.StartSymbol, EagerCctorTable.EndSymbol);
ReadyToRunHeader.Add(ReadyToRunSectionType.TypeManagerIndirection, TypeManagerIndirection, TypeManagerIndirection);
ReadyToRunHeader.Add(ReadyToRunSectionType.InterfaceDispatchTable, DispatchMapTable, DispatchMapTable.StartSymbol);
ReadyToRunHeader.Add(ReadyToRunSectionType.FrozenObjectRegion, FrozenSegmentRegion, FrozenSegmentRegion.StartSymbol, FrozenSegmentRegion.EndSymbol);
ReadyToRunHeader.Add(ReadyToRunSectionType.FrozenObjectRegion, FrozenSegmentRegion, FrozenSegmentRegion, FrozenSegmentRegion.EndSymbol);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a "ReadyToRunHeader"? Is this documented anywhere?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a data structure similar to the ReadyToRun header in R2R images. There might be a plan to unify aspects of this format at some point. The things pointed from the header are currently pretty disjoint.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The structure of the header is doc'd at https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/readytorun-format.md#readytorun_header. The main difference starts at READYTORUN_SECTION where the section type IDs are different for NativeAOT. Also the way to get to the header from "outside the image" doesn't exist. The address is inlined in code and not pointed by PE headers.

ReadyToRunHeader.Add(ReadyToRunSectionType.ModuleInitializerList, ModuleInitializerList, ModuleInitializerList, ModuleInitializerList.EndSymbol);

var commonFixupsTableNode = new ExternalReferencesTableNode("CommonFixupsTable", this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public abstract class MetadataManager : ICompilationRootProvider
private readonly SortedSet<MethodDesc> _reflectableMethods = new SortedSet<MethodDesc>(TypeSystemComparer.Instance);
private readonly SortedSet<GenericDictionaryNode> _genericDictionariesGenerated = new SortedSet<GenericDictionaryNode>(CompilerComparer.Instance);
private readonly SortedSet<IMethodBodyNode> _methodBodiesGenerated = new SortedSet<IMethodBodyNode>(CompilerComparer.Instance);
private readonly SortedSet<EmbeddedObjectNode> _frozenObjects = new SortedSet<EmbeddedObjectNode>(CompilerComparer.Instance);
private readonly SortedSet<TypeGVMEntriesNode> _typeGVMEntries
= new SortedSet<TypeGVMEntriesNode>(Comparer<TypeGVMEntriesNode>.Create((a, b) => TypeSystemComparer.Instance.Compare(a.AssociatedType, b.AssociatedType)));
private readonly SortedSet<DefType> _typesWithDelegateMarshalling = new SortedSet<DefType>(TypeSystemComparer.Instance);
Expand Down Expand Up @@ -272,6 +273,16 @@ protected virtual void Graph_NewMarkedNode(DependencyNodeCore<NodeFactory> obj)
{
_templateMethodEntries.Add(templateMethodEntry);
}

if (obj is FrozenObjectNode frozenObj)
{
_frozenObjects.Add(frozenObj);
}

if (obj is FrozenStringNode frozenStr)
{
_frozenObjects.Add(frozenStr);
}
}

protected virtual bool AllMethodsCanBeReflectable => false;
Expand Down Expand Up @@ -689,6 +700,11 @@ public IEnumerable<MethodDesc> GetReflectableMethods()
return _reflectableMethods;
}

public IEnumerable<EmbeddedObjectNode> GetFrozenObjects()
{
return _frozenObjects;
}

internal IEnumerable<IMethodBodyNode> GetCompiledMethodBodies()
{
return _methodBodiesGenerated;
Expand Down