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

Optimize some SegmentedArrayHelper members for JIT #67558

Merged
merged 7 commits into from
Apr 21, 2023

Conversation

EgorBo
Copy link
Member

@EgorBo EgorBo commented Mar 29, 2023

These properties pop up in perf traces while it is expected that they should be folded to constants. There are two problems here:

  1. AggressiveOptimization ((MethodImplOptions)512) attribute often ruins static readonly related optimizations because JIT is forced to compile a method straight to Tier1 so some types might not be statically initialized yet.
    Example:
class MyProg
{
    static readonly int Size = int.Parse("42");

    [MethodImpl(MethodImplOptions.NoInlining | 
                MethodImplOptions.AggressiveOptimization)]
    static int GetSize() => Size; 

    static void Main()
    {
        for (int i = 0; i < 100; i++)
        {
            GetSize();
            Thread.Sleep(16);
        }
    }
}

Codegen for GetSize function:

; Assembly listing for method MyProg:GetSize():int
       4883EC28             sub      rsp, 40
       48B9F045ABE3F87F0000 mov      rcx, 0x7FF8E3AB45F0
       BA06000000           mov      edx, 6
       E86865865F           call     CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE
       8B052EF01C00         mov      eax, dword ptr [(reloc 0x7ff8e3ab462c)]
       4883C428             add      rsp, 40
       C3                   ret

Now, if I remove AggressiveOptimization and run it again:

; Assembly listing for method MyProg:GetSize():int
       B82A000000           mov      eax, 42
       C3                   ret 

As a bonus, JIT won't waste time on jitting these functions during startup as R2R is expected to be picked up (it doesn't exist for methods with AggressiveOptimization).

  1. The next issue is a bit more complicated - if your input type is a struct with a shared generic type, e.g.
public struct MyStruct<T>
{
    public T t1;
    public T t2;
}

Then ValueTypeSegmentHelper<T>.field can't be inlined as is (needs dictionary runtime lookup and jit gives up on those). I moved SegmentShift and OffsetMask to a non generic type since those don't depend on T, but I can't do the same for SegmentSize.

As I workaround we can record which sizes are the most popular and add fast paths probably. Or use a dictionary, cc @jkotas

Here is a minimal repro:

using System.Runtime.CompilerServices;

public struct MyStruct<T>
{
    public T t1;
    public T t2;
}

static class Program
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    static int Foo<T>()
    {
        return SegmentedArrayHelper.GetSegmentSize<T>();
    }

    static void Main()
    {
        for (int i = 0; i < 100; i++)
        {
            Foo<MyStruct<string>>();
            Thread.Sleep(15);
        }
    }
}

internal static class SegmentedArrayHelper
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    internal static int GetSegmentSize<T>()
    {
        if (Unsafe.SizeOf<T>() == Unsafe.SizeOf<object>())
        {
            return ReferenceTypeSegmentHelper.SegmentSize;
        }
        else
        {
            return ValueTypeSegmentSizeHelper<T>.SegmentSize;
        }
    }

    private static class ValueTypeSegmentSizeHelper<T>
    {
        public static readonly int SegmentSize = CalculateSegmentSize(Unsafe.SizeOf<T>());

        private static int CalculateSegmentSize(int elementSize)
        {
            const int Threshold = 85000;
            var segmentSize = 2;
            while (ArraySize(elementSize, segmentSize << 1) < Threshold)
            {
                segmentSize <<= 1;
            }
            return segmentSize;
            static int ArraySize(int elementSize, int segmentSize)
            {
                return (2 * IntPtr.Size) + (elementSize * segmentSize);
            }
        }
    }

    private static class ReferenceTypeSegmentHelper
    {
        public static readonly int SegmentSize = CalculateSegmentSize(IntPtr.Size);

        private static int CalculateSegmentSize(int elementSize)
        {
            const int Threshold = 85000;
            var segmentSize = 2;
            while (ArraySize(elementSize, segmentSize << 1) < Threshold)
            {
                segmentSize <<= 1;
            }
            return segmentSize;
            static int ArraySize(int elementSize, int segmentSize)
            {
                return (2 * IntPtr.Size) + (elementSize * segmentSize);
            }
        }
    }
}

Codegen for Foo:

; Assembly listing for method Program:Foo[MyStruct`1[System.__Canon]]():int
G_M32814_IG01:              
       4883EC28             sub      rsp, 40
       48894C2420           mov      qword ptr [rsp+20H], rcx
G_M32814_IG02:              
       488B5138             mov      rdx, qword ptr [rcx+38H]
       488B5210             mov      rdx, qword ptr [rdx+10H]
       4885D2               test     rdx, rdx
       7402                 je       SHORT G_M32814_IG04
G_M32814_IG03:             
       EB12                 jmp      SHORT G_M32814_IG05
G_M32814_IG04:            
       48BA587CEEE3F87F0000 mov      rdx, 0x7FF8E3EE7C58      ; global ptr
       E8B901525F           call     CORINFO_HELP_RUNTIMEHANDLE_METHOD
       488BD0               mov      rdx, rax
G_M32814_IG05:              
       488BCA               mov      rcx, rdx
       FF15CDE25F00         call     [SegmentedArrayHelper:GetSegmentSize[MyStruct`1[System.__Canon]]():int]
       90                   nop      
G_M32814_IG06:              
       4883C428             add      rsp, 40
       C3                   ret

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-IDE untriaged Issues and PRs which have not yet been triaged by a lead labels Mar 29, 2023
@ghost ghost added the Community The pull request was submitted by a contributor who is not a Microsoft employee. label Mar 29, 2023
internal const MethodImplOptions FastPathMethodImplOptions = MethodImplOptions.AggressiveInlining | (MethodImplOptions)512;

[MethodImpl(FastPathMethodImplOptions)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int GetSegmentSize<T>()
{
if (Unsafe.SizeOf<T>() == Unsafe.SizeOf<object>())
Copy link
Member

Choose a reason for hiding this comment

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

if (sizeof(T) == sizeof(object)) would be better, in particular on .NET Framework where Unsafe.SizeOf is not an intrinsic.

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 complains on error CS0208: Cannot take the address of, get the size of, or declare a pointer to a managed type ('T') that I can't suppress 🤔

Copy link
Member

Choose a reason for hiding this comment

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

Is it building with recent Roslyn? The ability to suppress this was added not too long ago.

Copy link
Member Author

Choose a reason for hiding this comment

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

I assume it builds with the bootstrap SDK which is 8.0.100-preview.1.23115.2

Copy link
Member

Choose a reason for hiding this comment

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

It is possible to suppress this in .NET 7 SDK. For example, this compiles fine with .NET 7 SDK:

internal unsafe static int GetSegmentSize<T>()
{
#pragma warning disable CS8500 // Takes a pointer to a managed type
    return (sizeof(T) == sizeof(object)) ? 1 : -1;
#pragma warning restore CS8500
}

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I've already tried that in the Roslyn repo and but it still complained during build, I'll try again tomorrow

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, still complains, perhaps someone from Roslyn can help? Although, I think I fixed the main perf issue in this PR already

@JulieLeeMSFT
Copy link
Member

JulieLeeMSFT commented Mar 29, 2023

Fixes dotnet/runtime#84094

@EgorBo
Copy link
Member Author

EgorBo commented Mar 29, 2023

@jkotas it seems that the generic path where it doesn't inline is fairly popular (I ran Roslyn tests). But the object sizes were mostly 24 or 40 so we can either hard-code segment sizes for these or rewrite size calculation logic to be foldable at jit-time always without static readonly (if we can rely on .NET framework)

@EgorBo
Copy link
Member Author

EgorBo commented Mar 29, 2023

segmentSize = 1 << BitOperations.Log2((uint)((85000 / elementSize) - IntPtr.Size * 2));

seems to match existing logic. Although, not sure Log2 is constant folded on .NET Framework (or does it even exist there. UPD - it does not. Well, at least it is folded on .NET 8)

PS: the existing formula seems to be a bit inaccurate, the layout of array is [PtrSize header][PtrSize pMt][8b bounds/sizes][data]

@sharwell
Copy link
Member

@dotnet/roslyn-compiler I'll submit a follow-up pull request to manually constant-fold the results for the most common item sizes (4, 8, 24, and 40), and also fix the logic as mentioned in #67558 (comment).

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 7)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-IDE Community The pull request was submitted by a contributor who is not a Microsoft employee. untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants