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

Conversation

MichalStrehovsky
Copy link
Member

Saves 1% in size on a Hello World on Windows, more on Linux because we're getting rid of full pointers.

  • Instead of using ArrayOfEmbeddedDataNode as the base for the frozen object region, derive from DehydratableObjectNode directly. The base class wasn't really saving us much work over ObjectNode anyway.
  • Move the "align object to minimum size" code to the central location while I'm touching it.
  • Keep track of all frozen objects in the MetadataManager. This is the general pattern we follow elsewhere. The old pattern where the frozen object adds itself from the callback also works, but this matches the approach we use for other summarized node kinds.

Cc @dotnet/ilc-contrib

Saves 1% in size on a Hello World on Windows, more on Linux because we're getting rid of full pointers.

* Instead of using `ArrayOfEmbeddedDataNode` as the base for the frozen object region, derive from `DehydratableObjectNode` directly. The base class wasn't really saving us much work over `ObjectNode` anyway.
* Move the "align object to minimum size" code to the central location while I'm touching it.
* Keep track of all frozen objects in the `MetadataManager`. This is the general pattern we follow elsewhere. The old pattern where the frozen object adds itself from the callback also works, but this matches the approach we use for other summarized node kinds.
@ghost
Copy link

ghost commented Nov 22, 2022

Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas
See info in area-owners.md if you want to be subscribed.

Issue Details

Saves 1% in size on a Hello World on Windows, more on Linux because we're getting rid of full pointers.

  • Instead of using ArrayOfEmbeddedDataNode as the base for the frozen object region, derive from DehydratableObjectNode directly. The base class wasn't really saving us much work over ObjectNode anyway.
  • Move the "align object to minimum size" code to the central location while I'm touching it.
  • Keep track of all frozen objects in the MetadataManager. This is the general pattern we follow elsewhere. The old pattern where the frozen object adds itself from the callback also works, but this matches the approach we use for other summarized node kinds.

Cc @dotnet/ilc-contrib

Author: MichalStrehovsky
Assignees: -
Labels:

area-NativeAOT-coreclr

Milestone: -

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).

@@ -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.

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).

@MichalStrehovsky MichalStrehovsky merged commit 849e9b3 into dotnet:main Nov 30, 2022
@MichalStrehovsky MichalStrehovsky deleted the frozenDehydrate branch November 30, 2022 05:49
@ghost ghost locked as resolved and limited conversation to collaborators Dec 30, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants