dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.27k stars 4.73k forks source link

Make Type.IsPrimitive a JIT intrinsic (and JIT time constant) #95929

Closed Sergio0694 closed 10 months ago

Sergio0694 commented 11 months ago

Overview

We're currently working on AOT support for CsWinRT, and as part of that we're also looking into ways to reduce the binary size footprint, which is currently very large even for very simple applications. One issue we've noticed in particular is the static constructor in the Marshaler<T> type which is quite large for value types (we've started doing some work in that area here, but there's plenty more to do). This is because the linker is not able to properly handle all branches, so it just preserves everything, which then ends up compiling a whole bunch of useless code, as can be seen when opening the MSTAT files with sizoscope.

It would be beneficial to use Type.IsPrimitive as a JIT constant, as we could use that to at least skip the (currently reflection based) path checking for ABI types, which is just completely breaking the flow analysis of the linker. We could then at least avoid that generic size explosion for all primitive types. In general, the more properties on Type were JIT time constants, the we could inform the linker in these cases, but I IsPrimitive seems like a good place to start, since there's plenty of (commonly used) value types there, and we already know none of them will ever have an ABI type anyway.

Codegen

[JitGeneric(typeof(int))]
static bool Test<T>() => typeof(T).IsPrimitive;

Current codegen on .NET 8 x64 (sharplab):

Program.<<Main>$>g__Test|0_0[[System.Int32, System.Private.CoreLib]]()
    L0000: sub rsp, 0x28
    L0004: mov rcx, 0x158e6ad3660
    L000e: call 0x00007ffb5ea7e7a0
    L0013: mov ecx, 1
    L0018: shlx eax, ecx, eax
    L001d: test eax, 0x3003ffc
    L0022: setne al
    L0025: movzx eax, al
    L0028: add rsp, 0x28
    L002c: ret

Expected codegen on .NET 8 x64:

Program.<<Main>$>g__Test|0_0[[System.Int32, System.Private.CoreLib]]()
    L0000: mov eax, 1
    L0005: ret

cc. @jkoritzinsky @MichalStrehovsky @EgorBo

ghost commented 11 months ago

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

Issue Details
### Overview We're currently working on AOT support for [CsWinRT](https://github.com/microsoft/CsWinRT), and as part of that we're also looking into ways to reduce the binary size footprint, which is currently very large even for very simple applications. One issue we've noticed in particular is the static constructor in the `Marshaler` type which is quite large for value types (we've started doing some work in that area [here](https://github.com/microsoft/CsWinRT/pull/1413), but there's plenty more to do). This is because the linker is not able to properly handle all branches, so it just preserves everything, which then ends up compiling a whole bunch of useless code, as can be seen when opening the MSTAT files with sizoscope. It would be beneficial to use `Type.IsPrimitive` as a JIT constant, as we could use that to at least skip the (currently reflection based) path checking for ABI types, which is just completely breaking the flow analysis of the linker. We could then at least avoid that generic size explosion for all primitive types. In general, the more properties on `Type` were JIT time constants, the we could inform the linker in these cases, but I `IsPrimitive` seems like a good place to start, since there's plenty of (commonly used) value types there, and we already know none of them will ever have an ABI type anyway. ### Codegen ```csharp [JitGeneric(typeof(int))] static bool Test() => typeof(T).IsPrimitive; ``` **Current codegen on .NET 8 x64 ([sharplab](https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEBDAzgWwB8ABABgAIBlAC2ygAcAZbYAOgCUBXAOwwEt8MANwBYAFDiA2gCk+GAOIxuMKHzAAKDAE96MCADN1fXgEoTAXXHEAjEnLAIEADbkAKjFwYAPK4B86k3IAXl9ybV0DdVcTVgBJXAAFVXw5PgA3YSA=))**: ```asm Program.<
$>g__Test|0_0[[System.Int32, System.Private.CoreLib]]() L0000: sub rsp, 0x28 L0004: mov rcx, 0x158e6ad3660 L000e: call 0x00007ffb5ea7e7a0 L0013: mov ecx, 1 L0018: shlx eax, ecx, eax L001d: test eax, 0x3003ffc L0022: setne al L0025: movzx eax, al L0028: add rsp, 0x28 L002c: ret ``` **Expected codegen on .NET 8 x64**: ```asm Program.<
$>g__Test|0_0[[System.Int32, System.Private.CoreLib]]() L0000: mov eax, 1 L0005: ret ``` cc. @jkoritzinsky @MichalStrehovsky @EgorBo
Author: Sergio0694
Assignees: -
Labels: `area-CodeGen-coreclr`, `area-Tools-ILLink`
Milestone: -
jkotas commented 11 months ago

Where is the IsPrimitive check in CsWinRT that you expect this optimization to help with?

Sergio0694 commented 11 months ago

We're not using it yet, but @jkoritzinsky suggested we could add a branch checking that here so that all primitive value types would be covered, and wouldn't cause the linker to preserve and generate code for everything because it can't statically resolve what the result of that helper type logic is doing. We're basically looking for a way of statically being able to tell if an unconstrained T type argument is blittable or not.

jkotas commented 11 months ago

linker

Do you mean IL linker? IL linker does not analyze instantiated generics.

If you mean AOT compiler, you can replace the IsPrimitive check with inlined check for all primitive types typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte) || ... (types checks like this are treated as intrinsics) and see whether it is going to produce the expected results.

MichalPetryka commented 11 months ago

We're basically looking for a way of statically being able to tell if an unconstrained T type argument is blittable or not.

Wouldn't an intrinsic like this:

public class Assembly
{
    public bool IsBlittable(Type type);
}

make more sense for this? It being on Assembly would let it respect DisableRuntimeMarshalling and such.