Skip to content
This repository has been archived by the owner on Nov 1, 2020. It is now read-only.

Get CoreRT Hello World selfcontained binary under 1 MB #7962

Merged
merged 4 commits into from
Feb 3, 2020

Conversation

MichalStrehovsky
Copy link
Member

@MichalStrehovsky MichalStrehovsky commented Feb 2, 2020

This has actually two things. The actual fix to make self-contained CoreRT Hello World less than 1 MB, and a bunch of tools that helped me find the fix. I’ve had the tools for a while but they don’t get that much use when they only exist on my machine.

The tools

I compiled an empty console app with CoreRT. I used the existing <IlcGenerateMapFile>true</IlcGenerateMapFile> property to create a map.xml file in the intermediate directory to lists all the things we compiled. I wondered why DateTime formatting is needed for an empty app.

The existing map file is good to see what was generated, but not why it was generated.

The compiler builds a dependency graph internally that captures the whys. There has been a switch to dump the dependency graph into an XML file for a while - it just wasn’t publicly exposed. In this pull request I’m exposing it as <IlcGenerateDgmlFile>true</ IlcGenerateDgmlFile>. This generates up to two dgml.xml files in the intermediate directory (two if we’re running an optimized build with the scanner - in that case, the scan.dgml.xml file tends to be more useful).

The next thing is interpreting the dependency graph. I wrote a tool for that some time ago, now I’m checking it in.

The input to the tool is the DGML file and name of a node of interest. The output is the list of reasons why that node was include.

It looks like this for the DateTime formatting case:

() S_P_CoreLib_System_DateTimeFormat__FormatHebrewMonthName
  (call) S_P_CoreLib_System_DateTimeFormat__FormatCustomized
    (call) S_P_CoreLib_System_DateTimeFormat__FormatStringBuilder
      (call) S_P_CoreLib_System_DateTimeFormat__TryFormat_0
        (call) S_P_CoreLib_System_DateTimeFormat__TryFormat
          (call) S_P_CoreLib_System_DateTime__TryFormat
            (reloc) unbox_S_P_CoreLib_System_DateTime__TryFormat
              (Virtual method) (??_7Boxed_S_P_CoreLib_System_DateTime@@6B@ constructed, VirtualMethodUse [S.P.CoreLib]System.DateTime.TryFormat(Span`1<char>,int32&,ReadOnlySpan`1<char>,IFormatProvider))
                (Primary) ??_7Boxed_S_P_CoreLib_System_DateTime@@6B@ constructed
                  (ldtoken) S_P_CoreLib_System_CommonRuntimeTypes___cctor
                    (reloc) S_P_CoreLib_System_CommonRuntimeTypes::__NONGCSTATICS
                      (reloc) __GetGCStaticBase_S_P_CoreLib_System_CommonRuntimeTypes
                        (ldsfld) S_P_CoreLib_System_CommonRuntimeTypes__get_Boolean
                          (call) S_P_CoreLib_Internal_Runtime_Augments_RuntimeAugments__GetEnumUnderlyingType
                            (call) S_P_DisabledReflection_Internal_Reflection_ReflectionCoreCallbacksImplementation__GetEnumInfo
                              (Virtual method) (??_7S_P_DisabledReflection_Internal_Reflection_ReflectionCoreCallbacksImplementation@@6B@ constructed, VirtualMethodUse [S.P.CoreLib]Internal.Reflection.Augments.ReflectionCoreCallbacks.GetEnumInfo(Type))
                                (Primary) ??_7S_P_DisabledReflection_Internal_Reflection_ReflectionCoreCallbacksImplementation@@6B@ constructed
                                  (newobj) S_P_DisabledReflection_Internal_Runtime_CompilerHelpers_LibraryInitializer__InitializeLibrary
                                    (call) HelloWorld__Module___StartupCodeMain
                                      () Startup Code Main Method
                                (Secondary) VirtualMethodUse [S.P.CoreLib]Internal.Reflection.Augments.ReflectionCoreCallbacks.GetEnumInfo(Type)
                                  (callvirt) S_P_CoreLib_System_Enum__GetEnumInfo
                                    (call) S_P_CoreLib_System_Enum__InternalFormat
                                      (call) S_P_CoreLib_System_Enum__ToString
                                        (Virtual method) (??_7S_P_CoreLib_System_Enum@@6B@ constructed, VirtualMethodUse object.ToString())
                                          (Primary) ??_7S_P_CoreLib_System_Enum@@6B@ constructed
                                            (reloc) ??_7Boxed_S_P_CoreLib_System_RuntimeExceptionHelpers_RhFailFastReason@@6B@ constructed
                                              (callvirt) S_P_CoreLib_System_RuntimeExceptionHelpers__RuntimeFailFast
                                                () Runtime export
                                          (Secondary) VirtualMethodUse object.ToString()
                                            (callvirt) S_P_CoreLib_System_RuntimeExceptionHelpers__RuntimeFailFast
                                              () Runtime export
                (Secondary) VirtualMethodUse [S.P.CoreLib]System.DateTime.TryFormat(Span`1<char>,int32&,ReadOnlySpan`1<char>,IFormatProvider)
                  (Interface method) (??_7Boxed_S_P_CoreLib_System_DateTime@@6B@ constructed, VirtualMethodUse [S.P.CoreLib]System.ISpanFormattable.TryFormat(Span`1<char>,int32&,ReadOnlySpan`1<char>,IFormatProvider))
                    (Primary) ??_7Boxed_S_P_CoreLib_System_DateTime@@6B@ constructed
                      (ldtoken) S_P_CoreLib_System_CommonRuntimeTypes___cctor
                        (reloc) S_P_CoreLib_System_CommonRuntimeTypes::__NONGCSTATICS
                          (reloc) __GetGCStaticBase_S_P_CoreLib_System_CommonRuntimeTypes
                            (ldsfld) S_P_CoreLib_System_CommonRuntimeTypes__get_Boolean
                              (call) S_P_CoreLib_Internal_Runtime_Augments_RuntimeAugments__GetEnumUnderlyingType
                                (call) S_P_DisabledReflection_Internal_Reflection_ReflectionCoreCallbacksImplementation__GetEnumInfo
                                  (Virtual method) (??_7S_P_DisabledReflection_Internal_Reflection_ReflectionCoreCallbacksImplementation@@6B@ constructed, VirtualMethodUse [S.P.CoreLib]Internal.Reflection.Augments.ReflectionCoreCallbacks.GetEnumInfo(Type))
                                    (Primary) ??_7S_P_DisabledReflection_Internal_Reflection_ReflectionCoreCallbacksImplementation@@6B@ constructed
                                      (newobj) S_P_DisabledReflection_Internal_Runtime_CompilerHelpers_LibraryInitializer__InitializeLibrary
                                        (call) HelloWorld__Module___StartupCodeMain
                                          () Startup Code Main Method
                                    (Secondary) VirtualMethodUse [S.P.CoreLib]Internal.Reflection.Augments.ReflectionCoreCallbacks.GetEnumInfo(Type)
                                      (callvirt) S_P_CoreLib_System_Enum__GetEnumInfo
                                        (call) S_P_CoreLib_System_Enum__InternalFormat
                                          (call) S_P_CoreLib_System_Enum__ToString
                                            (Virtual method) (??_7S_P_CoreLib_System_Enum@@6B@ constructed, VirtualMethodUse object.ToString())
                                              (Primary) ??_7S_P_CoreLib_System_Enum@@6B@ constructed
                                                (reloc) ??_7Boxed_S_P_CoreLib_System_RuntimeExceptionHelpers_RhFailFastReason@@6B@ constructed
                                                  (callvirt) S_P_CoreLib_System_RuntimeExceptionHelpers__RuntimeFailFast
                                                    () Runtime export
                                              (Secondary) VirtualMethodUse object.ToString()
                                                (callvirt) S_P_CoreLib_System_RuntimeExceptionHelpers__RuntimeFailFast
                                                  () Runtime export
                    (Secondary) VirtualMethodUse [S.P.CoreLib]System.ISpanFormattable.TryFormat(Span`1<char>,int32&,ReadOnlySpan`1<char>,IFormatProvider)
                      (Interface method use) __InterfaceDispatchCell_S_P_CoreLib_System_ISpanFormattable__TryFormat
                        (callvirt) S_P_CoreLib_System_Text_ValueStringBuilder__AppendFormatHelper
                          (call) String__FormatHelper
                            (call) String__Format_1
                              (call) S_P_CoreLib_System_RuntimeExceptionHelpers__RuntimeFailFast
                                () Runtime export

The reason boils down to pretty much this:

The CommonRuntimeTypes class has a typeof reference to DateTime. This means the compiler needs to assume DateTime was boxed (we don’t do precise analysis of what happened to the typeof and one might just pass the result of the typeof to FormatterServices.GetUninitializedObject or reflection, which means an instance of the type could get allocated even without an explicit box anywhere in the code).

Okay, so DateTime is considered boxed, what’s the big deal?

The reason number 2 ("Secondary") listed in the dump is that ISpanFormattable.TryFormat is called somewhere. Since DateTime implements ISpanFormattable, we need to generate the implementations for that because bad things would happen at runtime if someone called ISpanFormattable methods on the boxed DateTime and we don’t have the code for that.

The fix

To break this kind of dependency (virtual/interface method call), we need to break one of the two reasons - either get rid of the object allocation, or get rid of the interface call.

Getting rid of the allocation is pretty straightforward - don’t reference DateTime from the CommonRuntimeTypes class. That type is used in codepaths that are more rare.

Makes the AOT compiled empty program go from 1,113,600 bytes to 962,560 bytes.

@@ -31,8 +31,8 @@ internal static class CommonRuntimeTypes
internal static Type IntPtr { get { return s_intPtr; } }
internal static Type Single { get { return s_single; } }
internal static Type Double { get { return s_double; } }
internal static Type Decimal { get { return s_decimal; } }
internal static Type DateTime { get { return s_datetime; } }
internal static Type Decimal { get { return NotSoCommonTypes.s_decimal; } }
Copy link
Member

Choose a reason for hiding this comment

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

It would be better to deleted these and convert the few places that use it to use typeof, e.g. if (type == typeof(Decimal)).

Caching these in statics is actually a deoptimization.

The JIT has optimization for operator== on System.Type that kicks in when one of the sides is typeof. It should compile this into a simple vtable compare.

Copy link
Member

Choose a reason for hiding this comment

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

It would be nice to port the fix for this deoptimization to CoreCLR as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

I take it, the comment at the top of this class only applies for .NET Native?

Copy link
Member Author

@MichalStrehovsky MichalStrehovsky Feb 2, 2020

Choose a reason for hiding this comment

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

I actually have a hard time finding the places where RyuJIT would be able to optimize the type comparison - most of these are used in deep reflection code where we have a System.Type from who knows where that we want to compare to a well known one, or we want to grab a specific System.Type to return.

I think typeof is still more expensive on CoreRT than on CoreCLR. It's at minimum a hashtable lookup. CoreCLR can do it faster for some reason.

Copy link
Member

Choose a reason for hiding this comment

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

CommonTypes.Decimal is used in a single place in

If you change this if (type == typeof(Decimal)), the RyuJIT should optimize it to a simple cmp instruction (look for CORINFO_INTRINSIC_TypeEQ).

I am not suggestion to get rid of this class completely. I agree that it is not straightforward to get rid of this completely. I am just suggestion to get rid of the ones you are moving to NotSoCommonTypes.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Hmmm, the optimization is not as good as I though. It is not able to get rid of the materializing of the Type object.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmmm, the optimization is not as good as I though. It is not able to get rid of the materializing of the Type object.

We could probably come up with a scheme for RyuJIT to be able optimize this.

typeof(X) expands to a set of calls "load native TypeHandle → get RuntimeTypeHandle from native TypeHandle -> Type.GetTypeFromHandle. RyuJIT optimizes this entire sequence into a single CORINFO_HELP_TYPEHANDLE_TO_RUNTIMETYPE helper call that takes the native TypeHandle as an argument.

RyuJIT can then optimize this to a simple compare if the other side of the comparison is a another typeof or someObject.GetType(). But that's not the case here.

If we could add a helper to compare native TypeHandle with an arbitrary System.Type, we could have RyuJIT call that in that case. That would help avoid materializing the System.Type from the native TypeHandle.

I could try to prototype that.

Copy link
Member Author

Choose a reason for hiding this comment

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

dotnet/runtime#31686 is the JIT fix to optimize the pattern and make a lot of CommonRuntimeTypes obsolete.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants