Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Remove Generic<T> Exception code repetition #6634

Closed
wants to merge 24 commits into from

Conversation

benaadams
Copy link
Member

@benaadams benaadams commented Aug 5, 2016

With the introduction of #6103 "Do not inline methods that never return"

These changes reduce the code gen size of the jitted methods (regular and generic * N) for Array and ArraySegment. (And a bunch of other generic types)

Also results in a 55kB reduction int the size of System.Private.CoreLib native (Windows_NT.x64.Release).

Pre  System.Private.CoreLib.dll 2,346,496 bytes
Post System.Private.CoreLib.dll 2,335,744 bytes
Pre  System.Private.CoreLib.ni.dll 10,913,280 bytes
Post System.Private.CoreLib.ni.dll 10,857,472 bytes

55,808 byte reduction

Total bytes of diff: -44551 (-1.44 % of base)
    diff is an improvement.

Total byte diff includes 562 bytes from reconciling methods
        Base had    1 unique methods,       76 unique bytes
        Diff had    9 unique methods,      638 unique bytes

Top file improvements by size (bytes):
      -44551 : System.Private.CoreLib.dasm (-1.44 % of base)

1 total files with size differences.

Top method regessions by size (bytes):
         418 : System.Private.CoreLib.dasm - CustomAttributeData:.ctor(ref,struct):this
         289 : System.Private.CoreLib.dasm - CustomAttributeData:Init(ref):this
         234 : System.Private.CoreLib.dasm - TimeSpan:TryParseExact(ref,ref,ref,byref):bool
         229 : System.Private.CoreLib.dasm - DateTimeParse:ParseByFormat(byref,byref,byref,ref,byref):bool
         187 : System.Private.CoreLib.dasm - DateTimeFormat:FormatCustomized(struct,ref,ref,struct):ref

Top method improvements by size (bytes):
       -3336 : System.Private.CoreLib.dasm - Array:Sort(ref,int,int,ref)
       -2396 : System.Private.CoreLib.dasm - Array:LastIndexOf(ref,struct,int,int):int
       -1677 : System.Private.CoreLib.dasm - Array:IndexOf(ref,struct,int,int):int
       -1251 : System.Private.CoreLib.dasm - Array:BinarySearch(ref,int,int,struct,ref):int
        -884 : System.Private.CoreLib.dasm - Comparer`1:Create(ref):ref

389 total methods with size differences.

One of the big wins, other than the included throw code is smaller (21 bytes of il to 7 bytes), is it prevents all the inlining of Environment:GetResourceString(ref):ref e.g.

Pre

Inlines into 060027E0 ArraySegment`1:.ctor(ref,int,int):this
  [0 IL=0008 TR=000119 06002512] [FAILED: unprofitable inline] ArgumentNullException:.ctor(ref):this
  [1 IL=0028 TR=000091 060034F5] [below ALWAYS_INLINE size] Environment:GetResourceString(ref):ref
    [0 IL=0001 TR=000128 060034F4] [FAILED: unprofitable inline] Environment:GetResourceStringLocal(ref):ref
  [0 IL=0033 TR=000102 06002AEE] [FAILED: unprofitable inline] ArgumentOutOfRangeException:.ctor(ref,ref):this
  [2 IL=0053 TR=000069 060034F5] [below ALWAYS_INLINE size] Environment:GetResourceString(ref):ref
    [0 IL=0001 TR=000136 060034F4] [FAILED: unprofitable inline] Environment:GetResourceStringLocal(ref):ref
  [0 IL=0058 TR=000080 06002AEE] [FAILED: unprofitable inline] ArgumentOutOfRangeException:.ctor(ref,ref):this
  [3 IL=0077 TR=000048 060034F5] [below ALWAYS_INLINE size] Environment:GetResourceString(ref):ref
    [0 IL=0001 TR=000144 060034F4] [FAILED: unprofitable inline] Environment:GetResourceStringLocal(ref):ref
  [0 IL=0082 TR=000059 06002E5A] [FAILED: unprofitable inline] ArgumentException:.ctor(ref):this
Budget: initialTime=390, finalTime=390, initialBudget=3900, currentBudget=3900
Budget: initialSize=2639, finalSize=2639

Post

Inlines into 06002A5B ArraySegment`1:.ctor(ref,int,int):this
  [0 IL=0004 TR=000067 06000C6F] [FAILED: does not return] ThrowHelper:ThrowArgumentNullException(int)
  [0 IL=0016 TR=000061 06000C74] [FAILED: noinline per IL/cached result] ThrowHelper:ThrowArgumentOutOfRangeException(int,int)
  [0 IL=0028 TR=000054 06000C74] [FAILED: noinline per IL/cached result] ThrowHelper:ThrowArgumentOutOfRangeException(int,int)
  [0 IL=0043 TR=000048 06000C6D] [FAILED: noinline per IL/cached result] ThrowHelper:ThrowArgumentException(int)
Budget: initialTime=270, finalTime=270, initialBudget=2700, currentBudget=2700
Budget: initialSize=1727, finalSize=1727

Which will be repeated for each new Generic<struct> or Method<struct>(...) flavour.

throw new ArgumentNullException("elementType");
}

private static void ThrowArgumenCountOutOfRange()
Copy link
Member

Choose a reason for hiding this comment

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

Typo Argumen

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

@benaadams
Copy link
Member Author

benaadams commented Aug 6, 2016

Assume fixing a typo didn't break the tests O_o

throw new ArgumentException(Environment.GetResourceString("Arg_MustBeType"), "elementType");
}

private static void ThrowArgumentOutOfRangeNeedNonNegNum(string parmName)
Copy link

Choose a reason for hiding this comment

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

Nit: parmName -> paramName

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

@AndyAyersMS
Copy link
Member

Unfortunately this change breaks the way that Internal:CommonlyUsedGenericInstantiations is used to force selected generic instantiations during the crossgen of the core library.

        static void CommonlyUsedGenericInstantiations()
        {
            System.Array.Sort<double>(null);
            System.Array.Sort<int>(null);
            System.Array.Sort<IntPtr>(null);
            new ArraySegment<byte>(new byte[1], 0, 0);
           ...

resulting asm with this change:

_M1895_IG01:
       sub      rsp, 120
       xor      rax, rax
       mov      qword ptr [rsp+68H], rax
       mov      qword ptr [rsp+70H], rax
G_M1895_IG02:
       call     Array:ThrowArrayNullException()
       int3

The jit now can inline the first Sort call and propagate the null and realize that the method will throw an exception, and so the jit dead codes the remainder of the method. While this optimization is "correct", the upshot is the referenced generic instantiations do not get created during crossgen. I believe we can just mark this method as no optimization and get back to the desired behavior and protect ourselves from future jit cleverness. Fixing this will cut down the size savings from this change, perhaps by 50% or so...

@benaadams
Copy link
Member Author

Not sure I understand, wouldn't the previous behaviour have the same effect?

public static void Sort<T>(T[] array)
{
    if (array == null)
        throw new ArgumentNullException("array");
    Contract.EndContractBlock();
    Sort<T>(array, 0, array.Length, null);
}

Should I back out the ThrowArrayNullException() call for this method?

@benaadams
Copy link
Member Author

Also is it worth adding some Array.Empty to that list? e.g.

System.Array.Empty<byte>();
System.Array.Empty<char>();
System.Array.Empty<int>();
System.Array.Empty<uint>();
System.Array.Empty<long>();
System.Array.Empty<ulong>();
System.Array.Empty<object>();

@benaadams
Copy link
Member Author

Best of both worlds?

Array.Sort<double>(Array.Empty<double>());
Array.Sort<int>(Array.Empty<int>());
Array.Sort<IntPtr>(Array.Empty<IntPtr>());

@benaadams
Copy link
Member Author

Adding the Array.Empty() Pushes it back up to the pre-size on disk 10,913,280 bytes

@benaadams
Copy link
Member Author

Removed some more repeated exceptions to functions specically in Search path, down to 10,889,216 bytes

So saving 24064 bytes on disk or 24kB

@@ -2636,6 +2636,47 @@ public Object Clone()
[System.Security.SecuritySafeCritical] // auto-generated
[MethodImplAttribute(MethodImplOptions.InternalCall)]
public extern void Initialize();


private static void ThrowArrayNullException()
Copy link

Choose a reason for hiding this comment

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

You should look at the existing ThrowHelper class and use that whenever possible. It's not very useful if we keep adding such methods all other the place.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed all the calls to ThrowHelper rather than the statics

@benaadams
Copy link
Member Author

Should be able to push a lot of this to ThrowHelper

@@ -45,7 +45,7 @@ public abstract class Array : ICloneable, IList, IStructuralComparable, IStructu
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
public static void Resize<T>(ref T[] array, int newSize) {
if (newSize < 0)
throw new ArgumentOutOfRangeException("newSize", Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum"));
ThrowArgumentOutOfRangeNeedNonNegNum(nameof(newSize));
Copy link

Choose a reason for hiding this comment

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

Use of constant strings results in an additional call, that's why ThrowHelper passes the argument as an enum instead.

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 diffs are a bit easier to compare with ThrowHelper also 😄

@benaadams
Copy link
Member Author

benaadams commented Aug 6, 2016

Down to 10,887,168 bytes on disk with ThrowHelper saving 26112 bytes or 25kB

Also now none of the generic methods throw directly.

@benaadams benaadams force-pushed the array-exceptions branch 2 times, most recently from 7163100 to 385b3b6 Compare August 6, 2016 06:03
@benaadams
Copy link
Member Author

Down to 10,885,632 bytes with ArraySegment; 27,648 byte saving

@benaadams
Copy link
Member Author

@dotnet-bot retest Windows_NT x64 Release Priority 1 Build and Test

@AndyAyersMS
Copy link
Member

Circling back a bit to my note on the change to codegen for CommonlyUsedGenericInstantiations: The refactoring you did had the side effect of making the array sort methods look like good inline candidates. Previously they weren't. So it uncovered an implicit dependence on the jit's behavior. The mechanism is fragile; we could just as easily have run into this in other ways

BEFORE

Inlines into 06000BE1 Internal:CommonlyUsedGenericInstantiations()
  [0 IL=0001 TR=000002 06002B6B] [FAILED: unprofitable inline] Array:Sort(ref)
  [0 IL=0007 TR=000006 06002B6B] [FAILED: unprofitable inline] Array:Sort(ref)
  [0 IL=0013 TR=000010 06002B6B] [FAILED: unprofitable inline] Array:Sort(ref)
  [0 IL=0026 TR=000029 060027E0] [FAILED: too many il bytes] ArraySegment`1:.ctor(ref,int,int):this
  ...

AFTER

Inlines into 06000BE1 Internal:CommonlyUsedGenericInstantiations()
  [1 IL=0001 TR=000002 06002B6B] [profitable inline] Array:Sort(ref)
    [0 IL=0003 TR=000746 06002B76] [FAILED: does not return] Array:ThrowArrayNullException()
    [0 IL=0014 TR=000755 06002B71] [FAILED: too many il bytes] Array:Sort(ref,int,int,ref)
  [2 IL=0007 TR=000006 06002B6B] [profitable inline] Array:Sort(ref)
    [0 IL=0003 TR=000767 06002B76] [FAILED: does not return] Array:ThrowArrayNullException()
    [0 IL=0014 TR=000776 06002B71] [FAILED: too many il bytes] Array:Sort(ref,int,int,ref)
  [3 IL=0013 TR=000010 06002B6B] [profitable inline] Array:Sort(ref)
    [0 IL=0003 TR=000788 06002B76] [FAILED: does not return] Array:ThrowArrayNullException()
    [0 IL=0014 TR=000797 06002B71] [FAILED: too many il bytes] Array:Sort(ref,int,int,ref)
  [0 IL=0026 TR=000029 060027E0] [FAILED: too many il bytes] ArraySegment`1:.ctor(ref,int,int):this
 ...

@benaadams
Copy link
Member Author

benaadams commented Aug 6, 2016

Would that suggest it would be sensible to go straight for Array:Sort(ref,int,int,ref) in Internal:CommonlyUsedGenericInstantiations and leave Array:Sort(ref)?

@benaadams
Copy link
Member Author

It also drops it a few more bytes to 10,884,608 bytes (1024 more?)

@AndyAyersMS
Copy link
Member

I believe the intent of CommonlyUsedGenericInstantiations is that for generic methods the most commonly used method should be the one that is instantiated. This will transitively cause instantiations of the methods it invokes.

The top-level Sort calls are getting inlined. @jkotas can correct me here but I believe that means these Sort methods will not get instantiated as expected, since the instantiation closure process is driven by "fixup" references left in the final code stream.

So my preference would be mark CommonlyUsedGenericInstantiations with [MethodImplAttribute(MethodImplOptions.NoOptimization)], along with a comment indicating why we don't want the jit to optimize this method, and revert it back to calling the simpler Sort methods that were there initially.

I am not sure if the empty array variants are used often enough to warrant forced instantiation, but likely it is fairly harmless to add them.

The change to consistently use ThrowHelper is a change for the better; however it will sometimes inhibit laying out the code so that the throwing methods are moved to the end of the method, since the jit has no way of transitively deducing the no return property. There are also cases (both before and after your changes) where the initial part of the ThrowHelper call chain gets inlined; this seems somewhat undesirable, to the point where I'd consider marking all the entry point methods in the ThrowHelper class as no inline. However doing that would inhibit even more cases where we get the beneficial layout.

Sorting all this out is going to take some work, so for now I'd just fix the issue with CommonlyUsedGenericInstantiations.

You can see the range of behaviors on display in Array:CreateInstance(ref,ref):ref after your change:

G_M62283_IG01:
       push     rdi
       push     rsi
       push     rbp
       push     rbx
       sub      rsp, 40
       xor      rax, rax
       mov      qword ptr [rsp+20H], rax
       mov      rsi, rdx
G_M62283_IG02:
       test     rcx, rcx
       je       G_M62283_IG16
G_M62283_IG03:
       test     rsi, rsi
       je       G_M62283_IG17
G_M62283_IG04:
       mov      edi, dword ptr [rsi+8]
       test     edi, edi
       je       G_M62283_IG18
G_M62283_IG05:
       mov      rax, qword ptr [rcx]
       mov      rax, qword ptr [rax+152]
       call     gword ptr [rax+40]Type:get_UnderlyingSystemType():ref:this
       mov      rdx, rax
       lea      rcx, [(reloc)]
       call     CORINFO_HELP_ISINSTANCEOFCLASS
       mov      rbx, rax
       test     rbx, rbx
       jne      SHORT G_M62283_IG06
       mov      ecx, 49
       mov      edx, 27
       call     ThrowHelper:ThrowArgumentException(int,int)     // not deduced no-return
G_M62283_IG06:
       xor      ebp, ebp
       test     edi, edi
       jle      SHORT G_M62283_IG09
G_M62283_IG07:
       movsxd   rcx, ebp
       cmp      dword ptr [rsi+4*rcx+16], 0
       jl       G_M62283_IG19
G_M62283_IG08:
       inc      ebp
       cmp      edi, ebp
       jg       SHORT G_M62283_IG07
G_M62283_IG09:
       test     edi, edi
       jne      SHORT G_M62283_IG11
G_M62283_IG10:
       xor      rcx, rcx
       mov      bword ptr [rsp+20H], rcx
       jmp      SHORT G_M62283_IG12
G_M62283_IG11:
       cmp      edi, 0
       jbe      G_M62283_IG20
       add      rsi, 16
       mov      bword ptr [rsp+20H], rsi
G_M62283_IG12:
       mov      rcx, rbx
       mov      rax, qword ptr [rbx]
       mov      rax, qword ptr [rax+88]
       call     gword ptr [rax+24]Type:get_TypeHandle():struct:this
       test     rax, rax
       jne      SHORT G_M62283_IG13
       xor      rcx, rcx
       jmp      SHORT G_M62283_IG14
G_M62283_IG13:
       mov      rcx, qword ptr [rax+24]
G_M62283_IG14:
       mov      edx, edi
       mov      r8, bword ptr [rsp+20H]
       xor      r9, r9
       call     Array:InternalCreate(long,int,long,long):ref
       nop      
G_M62283_IG15:
       add      rsp, 40
       pop      rbx
       pop      rbp
       pop      rsi
       pop      rdi
       ret      
************** Beginning of cold code **************
G_M62283_IG16:
       mov      ecx, 27
       call     ThrowHelper:ThrowArgumentNullException(int)    // moved, deduced no return
G_M62283_IG17:
       mov      ecx, 34
       call     ThrowHelper:ThrowArgumentNullException(int)   // moved, deduced no return
G_M62283_IG18:
       lea      rcx, [(reloc)]
       call     CORINFO_HELP_NEWSFAST
       mov      rbp, rax
       mov      ecx, 0x10072
       call     CORINFO_HELP_STRCNS_CURRENT_MODULE
       mov      rcx, rax
       call     Environment:GetResourceStringLocal(ref):ref
       mov      rdx, rax
       mov      rcx, rbp
       call     ArgumentException:.ctor(ref):this                        // moved,  inlined
       mov      rcx, rbp
       call     CORINFO_HELP_THROW
G_M62283_IG19:
       lea      rcx, [(reloc)]
       call     CORINFO_HELP_NEWSFAST
       mov      rsi, rax
       mov      ecx, 0x1009C
       call     CORINFO_HELP_STRCNS_CURRENT_MODULE
       mov      rdi, rax
       mov      dword ptr [rsi+8], ebp
       mov      ecx, 847
       call     CORINFO_HELP_STRCNS_CURRENT_MODULE
       mov      r8, rax
       mov      rcx, rdi
       mov      rdx, rsi
       call     String:Concat(ref,ref,ref):ref
       mov      rsi, rax
       lea      rcx, [(reloc)]
       call     CORINFO_HELP_NEWSFAST
       mov      rdi, rax
       mov      ecx, 0x4D2F
       call     CORINFO_HELP_STRCNS_CURRENT_MODULE
       mov      rcx, rax
       call     Environment:GetResourceStringLocal(ref):ref
       mov      rbx, rax
       mov      rcx, rdi
       call     Exception:Init():this
       lea      rcx, bword ptr [rdi+32]
       mov      rdx, rbx
       call     CORINFO_HELP_ASSIGN_REF
       mov      dword ptr [rdi+132], 0xD1FFAB1E
       lea      rcx, bword ptr [rdi+144]
       mov      rdx, rsi
       call     CORINFO_HELP_ASSIGN_REF
       mov      dword ptr [rdi+132], 0xD1FFAB1E
       mov      dword ptr [rdi+132], 0xD1FFAB1E
       mov      rcx, rdi
       call     CORINFO_HELP_THROW                                 //  moved, inlined
       int3     
G_M62283_IG20:
       call     CORINFO_HELP_RNGCHKFAIL
       int3 

@benaadams
Copy link
Member Author

Marked as such.

Up a bit to 10,885,120 bytes so 28,160 bytes saving

@benaadams
Copy link
Member Author

@AndyAyersMS thank you for working though this with me. Are the various outputs from jitutils? (dasm, and inlining choices)

@benaadams
Copy link
Member Author

There are a couple throws I haven't moved out to the ThrowHelper in non-generic methods (for example in CreateInstance). Shall I do those also?

There are a couple oddities:

ThrowHelper.ThrowArgumentOutOfRangeException(
    length1 < 0 ? ExceptionArgument.length1 : ExceptionArgument.length2, 
    ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);

Which I could split two 2 ifs?

It would only leave the 3 which have variable parameter names e.g.

throw new ArgumentOutOfRangeException("lengths[" + i + ']', 
    Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum"));

Which I could move out to a static

static void ThrowLengthOutOfRange(int parameterNumber)
{
    throw new ArgumentOutOfRangeException("lengths[" + parameterNumber + ']', 
        Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum"));
}

Then see how it looks?

@benaadams
Copy link
Member Author

Rebased

@benaadams
Copy link
Member Author

@dotnet-bot test Windows_NT x86 ryujit Checked Build and Test

@benaadams
Copy link
Member Author

Closing this as I think most of the big individual ones are done

The remaining ones in the concurrent collections are in the interface path (hopefully not common use)

@benaadams benaadams closed this Sep 7, 2016
@benaadams benaadams deleted the array-exceptions branch March 27, 2018 05:11
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.

7 participants