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

Add generation of structured object file dumps #73913

Merged
merged 1 commit into from
Aug 15, 2022

Conversation

MichalStrehovsky
Copy link
Member

The compiler can currently emit an XML file with everything that was emitted into the output object file. This works and it's easily human readable. But the captured information is not very structured and cannot be used to generate aggregate information like 'this assembly contributed X bytes".

This adds another dumper format similar to what we do for MIBC and others: it's ECMA-335 based and uses ldtoken/ldstr/ldc to encode the data.

This can be used to build better tools. There's currently none but ILDASM, but it would be good to have it for 7.0. Good hackathon project.

Cc @dotnet/ilc-contrib

The compiler can currently emit an XML file with everything that was emitted into the output object file. This works and it's easily human readable. But the captured information is not very structured and cannot be used to generate aggregate information like 'this assembly contributed X bytes".

This adds another dumper format similar to what we do for MIBC and others: it's ECMA-335 based and uses ldtoken/ldstr/ldc to encode the data.

This can be used to build better tools. There's currently none but ILDASM, but it would be good to have it for 7.0. Good hackathon project.
@Suchiman
Copy link
Contributor

Suchiman commented Aug 14, 2022

I've played around with this a bit, for the smallest possible Hello World (967kb), i could get following numbers out (which is sums up to only half of the executable size):

// ********** Types Total Size 101.020
System.Private.CoreLib                    94.464
System.Private.TypeLoader                  4.296
System.Private.DisabledReflection          1.264
System.Console                               964
NativeConsole                                 32
// **********

// ********** Methods Total Size 253.564
System.Private.CoreLib                   235.602
System.Console                             7.752
System.Private.TypeLoader                  4.763
System.Private.DisabledReflection          4.010
System.Private.CompilerGenerated           1.078
NativeConsole                                359
// **********

// ********** Size By Namespace
System                                   108.721
System.Text                               48.384
System.Collections.Generic                43.342
System.Numerics                           28.672
System.Runtime                            18.460
System.Threading                          16.551
System.Globalization                      12.994
System.Runtime.CompilerServices           12.280
System.IO                                 10.968
System.Buffers                             8.751
Internal.Runtime.TypeLoader                4.045
Interop                                    3.895
Internal.Runtime                           3.837
System.Collections.Concurrent              3.706
Entry[]                                    3.264
Internal.Runtime.CompilerHelpers           2.640
System.Text.Unicode                        2.525
System.Diagnostics                         2.342
System.Collections                         1.966
System.Reflection                          1.946
Internal.Reflection                        1.809
System.Runtime.Loader                      1.426
System.Runtime.InteropServices             1.344
System.Threading.Tasks                     1.189
Internal.TypeSystem                          864
Internal.NativeFormat                        819
Internal.DeveloperExperience                 786
Internal.Runtime.Augments                    619
Internal.Reflection.Core.NonPortable         494
Internal                                     478
<Module>                                     423
Internal.Metadata.NativeFormat               421
Internal.Reflection.Augments                 420
PerCoreLockedStacks[]                        352
LockedStack[]                                352
ThreadLocalArray[]                           336
Internal.CompilerGenerated                   335
Boxed_Enumerator                             291
Microsoft.Win32.SafeHandles                  260
Boxed_Entry                                  235
System.Diagnostics.Tracing                   214
GenericMethodEntry[]                         176
GenericTypeEntry[]                           176
NamedTypeLookupResult[]                      176
ThreadLocalNodeFinalizationHelper[]          176
Cctor[][]                                    176
Internal.Runtime.CompilerServices            168
Bucket[]                                     168
Cctor[]                                      168
BlockingRecord[]                             168
System.Runtime.Serialization                  96
System.Runtime.ExceptionServices              80
System.Runtime.ConstrainedExecution           68
System.Reflection.Runtime.General             32
// **********

// ********** Blobs Total Size 118.773
ArrayOfFrozenObjects                      40.888
FieldRvaData                              32.893
Blob                                       9.448
ExternalReferencesTable                    6.000
GenericComposition                         4.995
GenericTypesHashtable                      3.779
MethodReadOnlyData                         2.924
ReadyToRunHelper                           2.147
BlockReflectionTypeMap                     1.821
TypeGenericDictionary                      1.624
ArrayOfEmbeddedPointers                    1.120
InterfaceDispatchMap                       1.042
NonGCStatics                                 898
GCStatics                                    880
DefaultConstructorMap                        873
UnboxingStub                                 864
ReadyToRunHeader                             808
EETypeOptionalFields                         748
InterfaceDispatchCellSection                 688
MethodGenericDictionary                      664
GenericMethodsHashtable                      618
GCStaticEEType                               552
NativeLayoutInfo                             515
GCStaticsPreInitData                         448
StaticsInfoHashtable                         230
ArrayMap                                     224
ReadyToRunGenericLookupFromType              216
FatFunctionPointer                           192
ClassConstructorContextMap                   184
TypeThreadStaticIndex                        160
MethodAssociatedData                          95
ArrayOfEmbeddedData                           80
GenericTypesTemplateMap                       33
ReadyToRunGenericLookupFromDictionary         29
TentativeInstanceMethod                       25
TypeManagerIndirection                        16
GenericMethodsTemplateMap                      9
ModulesSection                                 8
Metadata                                       5
ResourceIndex                                  3
TypeMetadataMap                                3
ReflectionInvokeMap                            3
DelegateMarshallingStubMap                     3
StructMarshallingStubMap                       3
ReflectionFieldMap                             3
ExactMethodInstantiations                      3
GenericVirtualMethodTable                      3
InterfaceGenericVirtualMethodTable             3
ReflectionVirtualInvokeMap                     3
ResourceData                                   0
StackTraceMethodMapping                        0
ModuleInitializerList                          0
// **********

Code:

using Mono.Cecil;
using Mono.Cecil.Rocks;

namespace NativeAOTSizeAnalyzer;

internal class Program
{
    static void Main(string[] args)
    {
        var asm = AssemblyDefinition.ReadAssembly(@"something.mstat");
        var globalType = (TypeDefinition)asm.MainModule.LookupToken(0x02000001);

        var types = globalType.Methods.First(x => x.Name == "Types");
        var typeStats = GetTypes(types).ToList();
        var typeSize = typeStats.Sum(x => x.Size);
        var typesByModules = typeStats.GroupBy(x => x.Type.Scope).Select(x => new { x.Key.Name, Sum = x.Sum(x => x.Size) }).ToList();
        Console.WriteLine($"// ********** Types Total Size {typeSize:n0}");
        foreach (var m in typesByModules.OrderByDescending(x => x.Sum))
        {
            Console.WriteLine($"{m.Name,-40} {m.Sum,7:n0}");
        }
        Console.WriteLine($"// **********");

        Console.WriteLine();

        var methods = globalType.Methods.First(x => x.Name == "Methods");
        var methodStats = GetMethods(methods).ToList();
        var methodSize = methodStats.Sum(x => x.Size + x.GcInfoSize + x.EhInfoSize);
        var methodsByModules = methodStats.GroupBy(x => x.Method.DeclaringType.Scope).Select(x => new { x.Key.Name, Sum = x.Sum(x => x.Size + x.GcInfoSize + x.EhInfoSize) }).ToList();
        Console.WriteLine($"// ********** Methods Total Size {methodSize:n0}");
        foreach (var m in methodsByModules.OrderByDescending(x => x.Sum))
        {
            Console.WriteLine($"{m.Name,-40} {m.Sum,7:n0}");
        }
        Console.WriteLine($"// **********");

        Console.WriteLine();

        string FindNamespace(TypeReference type)
        {
            var current = type;
            while (true)
            {
                if (!String.IsNullOrEmpty(current.Namespace))
                {
                    return current.Namespace;
                }

                if (current.DeclaringType == null)
                {
                    return current.Name;
                }

                current = current.DeclaringType;
            }
        }

        var methodsByNamespace = methodStats.Select(x => new TypeStats { Type = x.Method.DeclaringType, Size = x.Size + x.GcInfoSize + x.EhInfoSize }).Concat(typeStats).GroupBy(x => FindNamespace(x.Type)).Select(x => new { x.Key, Sum = x.Sum(x => x.Size) }).ToList();
        Console.WriteLine($"// ********** Size By Namespace");
        foreach (var m in methodsByNamespace.OrderByDescending(x => x.Sum))
        {
            Console.WriteLine($"{m.Key,-40} {m.Sum,7:n0}");
        }
        Console.WriteLine($"// **********");

        Console.WriteLine();

        var blobs = globalType.Methods.First(x => x.Name == "Blobs");
        var blobStats = GetBlobs(blobs).ToList();
        var blobSize = blobStats.Sum(x => x.Size);
        Console.WriteLine($"// ********** Blobs Total Size {blobSize:n0}");
        foreach (var m in blobStats.OrderByDescending(x => x.Size))
        {
            Console.WriteLine($"{m.Name,-40} {m.Size,7:n0}");
        }
        Console.WriteLine($"// **********");
    }

    public static IEnumerable<TypeStats> GetTypes(MethodDefinition types)
    {
        types.Body.SimplifyMacros();
        var il = types.Body.Instructions;
        for (int i = 0; i + 2 < il.Count; i += 2)
        {
            var type = (TypeReference)il[i + 0].Operand;
            var size = (int)il[i + 1].Operand;
            yield return new TypeStats
            {
                Type = type,
                Size = size
            };
        }
    }

    public static IEnumerable<MethodStats> GetMethods(MethodDefinition methods)
    {
        methods.Body.SimplifyMacros();
        var il = methods.Body.Instructions;
        for (int i = 0; i + 4 < il.Count; i += 4)
        {
            var method = (MethodReference)il[i + 0].Operand;
            var size = (int)il[i + 1].Operand;
            var gcInfoSize = (int)il[i + 2].Operand;
            var ehInfoSize = (int)il[i + 3].Operand;
            yield return new MethodStats
            {
                Method = method,
                Size = size,
                GcInfoSize = gcInfoSize,
                EhInfoSize = ehInfoSize
            };
        }
    }

    public static IEnumerable<BlobStats> GetBlobs(MethodDefinition blobs)
    {
        blobs.Body.SimplifyMacros();
        var il = blobs.Body.Instructions;
        for (int i = 0; i + 2 < il.Count; i += 2)
        {
            var name = (string)il[i + 0].Operand;
            var size = (int)il[i + 1].Operand;
            yield return new BlobStats
            {
                Name = name,
                Size = size
            };
        }
    }
}

public class TypeStats
{
    public TypeReference Type { get; set; }
    public int Size { get; set; }
}

public class MethodStats
{
    public MethodReference Method { get; set; }
    public int Size { get; set; }
    public int GcInfoSize { get; set; }
    public int EhInfoSize { get; set; }
}

public class BlobStats
{
    public string Name { get; set; }
    public int Size { get; set; }
}

Edit (2023-01-23 @MichalStrehovsky): updated the parser to account for the file format change in #75328.

@Suchiman
Copy link
Contributor

Suchiman commented Aug 14, 2022

Btw. dunno if you're aware, i've just discovered https://devblogs.microsoft.com/performance-diagnostics/sizebench-a-new-tool-for-analyzing-windows-binary-size/ which seems quite neat!

Looks like 109KB of these 967KB are just bitonic sort / vxsort in the GC...

@MichalStrehovsky
Copy link
Member Author

I've played around with this a bit

Nice tool!!!

which is sums up to only half of the executable size

Yeah, this is more meaningful for bigger apps than the absolute minimal app. The absolute minimal app is dominated by the native parts of the binary (GC, vxsort tables, etc.)

One thing .mstat doesn't capture is relocations - the NativeAOT data structures are pointer-heavy, so there's a lot of data in the .reloc section that is fixups for things in .mstat. But the size is roughly proportional.

Btw. dunno if you're aware, i've just discovered https://devblogs.microsoft.com/performance-diagnostics/sizebench-a-new-tool-for-analyzing-windows-binary-size/ which seems quite neat!

Oh nice, I didn't know they made it public!

Looks like 109KB of these 967KB are just bitonic sort / vxsort in the GC...

Yeah, I disable all that in bflat. It's one of the reasons why it's smaller than stock NativeAOT. We build two flavors of the GC for NativeAOT - one that only has the workstation GC, and one that has server. I thought about disabling vxsort for the workstation one, but I don't have any data to base the decision on (besides size). We want to have comparable perf between CoreCLR-JIT and CoreCLR-NativeAOT. We could build yet another flavor of the GC, but we already have 3 (wks, server, server+guardcf) and we need to stop somewhere.

@jkotas
Copy link
Member

jkotas commented Aug 15, 2022

We could build yet another flavor of the GC, but we already have 3 (wks, server, server+guardcf) and we need to stop somewhere.

A potential solution for this is to add an option for building GC from sources as part of application publish, similar to what's done in #72896 for ICU shims. It would allow you to build the GC with any custom settings you wish.

@MichalStrehovsky
Copy link
Member Author

@sbomer @LakshanF could you have a look?

Copy link
Contributor

@tlakollo tlakollo left a comment

Choose a reason for hiding this comment

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

LGTM! Thank you :D
:shipit:

@MichalStrehovsky MichalStrehovsky merged commit 00f34fb into dotnet:main Aug 15, 2022
@MichalStrehovsky MichalStrehovsky deleted the mstat branch August 15, 2022 21:55
@ghost ghost locked as resolved and limited conversation to collaborators Sep 15, 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