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

JIT struct work planned for .NET 9 #93105

Closed
5 of 10 tasks
jakobbotsch opened this issue Oct 6, 2023 · 3 comments
Closed
5 of 10 tasks

JIT struct work planned for .NET 9 #93105

jakobbotsch opened this issue Oct 6, 2023 · 3 comments
Assignees
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI User Story A single user-facing feature. Can be grouped under an epic.
Milestone

Comments

@jakobbotsch
Copy link
Member

jakobbotsch commented Oct 6, 2023

This issue captures the planned work items for .NET 9 with respect to struct improvements. This list is expected to change throughout the release cycle according to ongoing planning and discussions, with possible additions and subtractions to the scope.

Expanding the scope of physical promotion

.NET 8 added physical promotion, which removes many of the limitations of the existing promotion pass. Physical promotion has the following pros over the regular promotion pass:

  • It can partially promote only the struct fields that are used in the function
  • It can promote structs with arbitrary number of fields
  • It supports recursive promotion of structs
  • Physically promoted fields are always normalize-on-load
  • Physically promoted fields can always be enregistered; it does not have the notion of dependent promotion

Long term, we would like physical promotion to replace the regular promotion pass entirely. We do NOT expect this to happen in .NET 9. However, we do expect to make progress towards this goal in .NET 9.

ABI handling

The main limitation that stops physical promotion from replacing regular promotion is currently that it lacks the support around ABI boundaries that regular promotion has. This comes out of the fact that our multireg support is tied very directly into the existing promotion mechanism.
Thus, we expect to work on some of the following items in .NET 9:

The FIELD_LIST items are expected to be easier than the last two items (this is related to the fact that we do not have a good way of representing multiply-defined things in JIT IR).

Reducing scope of dependent promotion

We also expect to switch some cases where we can predict dependent promotion to be handled by physical promotion instead. This is generally expected to have good CQ benefits as it allows fields to stay in registers.

Miscellaneous work items

Moved out of .NET 9

Struct copy propagation

The JIT has to create a temporary when constructing structs on the off chance that the constructor observes address of this. However, practically no struct constructor depends on this, and we are typically able to prove this after inlining. In many cases, however, we still end up with subpar CQ due to the copy.

This work item is about introducing a pass to get rid of copies in the common case where we inlined and proved them to be unnecessary.

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Oct 6, 2023
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Oct 6, 2023
@ghost
Copy link

ghost commented Oct 6, 2023

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

Issue Details

This issue captures the planned work items for .NET 9 with respect to struct improvements. This list is expected to change throughout the release cycle according to ongoing planning and discussions, with possible additions and subtractions to the scope.

Expanding the scope of physical promotion

.NET 8 added physical promotion, which removes many of the limitations of the existing promotion pass. Physical promotion has the following pros over the regular promotion pass:

  • It can partially promote only the struct fields that are used in the function
  • It can promote structs with arbitrary number of fields
  • It supports recursive promotion of structs
  • Physically promoted fields are always normalize-on-load
  • Physically promoted fields can always be enregistered; it does not have the notion of dependent promotion

Long term, we would like physical promotion to replace the regular promotion pass entirely. We do NOT expect this to happen in .NET 9. However, we do expect to make progress towards this goal in .NET 9.

ABI handling

The main limitation that stops physical promotion from replacing regular promotion is currently that it lacks the support around ABI boundaries that regular promotion has. This comes out of the fact that our multireg support is tied very directly into the existing promotion mechanism.
Thus, we expect to work on some of the following items in .NET 9:

  • (Q4) Support producing FIELD_LIST in physical promotion for call arguments
  • ('24) Support producing FIELD_LIST in physical promotion for returned values
  • ('24) Support FIELD_LIST for returns in the backend
  • (Stretch) Allow struct fields of physically promoted parameters to stay in registers (experiment: JIT: [experiment] Add an explicit IR representation for parameter definitions #92026)
  • (Stretch) Allow struct fields of physically promoted call returns to stay in registers

The FIELD_LIST items are expected to be easier than the last two items (this is related to the fact that we do not have a good way of representing multiply-defined things in JIT IR).

Reducing scope of dependent promotion

We also expect to switch some cases where we can predict dependent promotion to be handled by physical promotion instead. This is generally expected to have good CQ benefits as it allows fields to stay in registers.

  • ('24) Handle retbuf definitions via physical promotion
  • ('24) Handle structs with holes via physical promotion

To accomplish the first item we will likely reorder regular promotion and local morph. This means introducing another walk over locals somewhere after regular promotion to replace LCL_FLDs that refer to regular promoted fields. However, it also means we can avoid promoting address exposed structs.

Struct copy propagation

The JIT has to create a temporary when constructing structs on the off chance that the constructor observes address of this. However, practically no struct constructor depends on this, and we are typically able to prove this after inlining. In many cases, however, we still end up with subpar CQ due to the copy.

This work item is about introducing a pass to get rid of copies in the common case where we inlined and proved them to be unnecessary.

Miscellaneous work items

Author: jakobbotsch
Assignees: -
Labels:

area-CodeGen-coreclr

Milestone: -

@jakobbotsch jakobbotsch removed the untriaged New issue has not been triaged by the area owner label Oct 6, 2023
@jakobbotsch jakobbotsch added this to the 9.0.0 milestone Oct 6, 2023
@JulieLeeMSFT JulieLeeMSFT added the User Story A single user-facing feature. Can be grouped under an epic. label Oct 6, 2023
@jakobbotsch
Copy link
Member Author

A couple of user examples where dependent promotion harms CQ:

using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;

public static class Tests
{
    public static MyVec2 TestAdd1(MyVec2 v1, MyVec2 v2) => MyVec2.Add1(v1, v2);
    public static MyVec2 TestAdd2(MyVec2 v1, MyVec2 v2) => MyVec2.Add2(v1, v2);
}

public struct MyVec2
{
    public double X, Y;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static MyVec2 Add1(MyVec2 left, MyVec2 right)
    {
        var tmp = Unsafe.As<MyVec2, Vector128<double>>(ref left) + Unsafe.As<MyVec2, Vector128<double>>(ref right);
        return Unsafe.As<Vector128<double>, MyVec2>(ref tmp);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static MyVec2 Add2(MyVec2 left, MyVec2 right)
    {
        var tmp = Unsafe.BitCast<MyVec2, Vector128<double>>(left) + Unsafe.BitCast<MyVec2, Vector128<double>>(right);
        return Unsafe.BitCast<Vector128<double>, MyVec2>(tmp);
    }
}

CQ of Add1 is worse than Add2 because Add2 introduces some natural copies that leads to avoided dependent promotion.

using System;
using System.Runtime.CompilerServices;
using System.Numerics;
using System.Runtime.Intrinsics;


public static class Tests
{
    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    private static Vector2<double> ByRefOperatorAdd(Vector2<double> v1, Vector2<double> v2)
    {
        return v1 + v2;
    }
    
    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    private static Vector2<double> ByRefMethodAdd(Vector2<double> v1, Vector2<double> v2)
    {
        return Vector2<double>.Add(in v1, in v2);
    }
    
    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    private static Vector2<double> ByValMethodAdd(Vector2<double> v1, Vector2<double> v2)
    {
        return Vector2<double>.Add2(v1, v2);
    }
    
    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    private static Vector2<double> ByRefMethodAdd2(Vector2<double> v1, Vector2<double> v2)
    {
        return Vector2<double>.Add3(in v1, in v2);
    }
    
    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    private static Vector2<double> ByValMethodAdd2(Vector2<double> v1, Vector2<double> v2)
    {
        return Vector2<double>.Add4(v1, v2);
    }
}

public static class Extensions
{
    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public static ref readonly Vector128<T> AsVector128<T>(in this Vector2<T> vector) where T : unmanaged, INumber<T>
    {
        return ref Unsafe.As<Vector2<T>, Vector128<T>>(ref Unsafe.AsRef(in vector));
    }
    
    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public static ref readonly Vector2<T> AsVector2<T>(in this Vector128<T> vector) where T : unmanaged, INumber<T>
    {
        return ref Unsafe.As<Vector128<T>, Vector2<T>>(ref Unsafe.AsRef(in vector));
    }
}

public readonly struct Vector2<T> where T : unmanaged, INumber<T>
{
    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public static Vector2<T> operator +(in Vector2<T> left, in Vector2<T> right) => Add(in left, in right);
    
	public T X
    {
		[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
		get;
	}
    
	public T Y
    {
		[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
		get;
	}
    
    public Vector2(T x, T y)
    {
        X = x;
        Y = y;
    }
    
    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public static Vector2<T> Add(in Vector2<T> left, in Vector2<T> right)
    {
        unsafe
        {
            // Total size = 128 bits
            if (sizeof(Vector2<T>) == 16)
            {
                if (Vector128<T>.IsSupported && Vector128.IsHardwareAccelerated)
                    return (left.AsVector128() + right.AsVector128()).AsVector2();
            }
        }

        return new(left.X + right.X, left.Y + right.Y);
    }
    
    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public static Vector2<T> Add2(Vector2<T> left, Vector2<T> right)
    {
        unsafe
        {
            // Total size = 128 bits
            if (sizeof(Vector2<T>) == 16)
            {
                if (Vector128<T>.IsSupported && Vector128.IsHardwareAccelerated)
                    return (left.AsVector128() + right.AsVector128()).AsVector2();
            }
        }

        return new(left.X + right.X, left.Y + right.Y);
    }
    
    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public static Vector2<T> Add3(in Vector2<T> left, in Vector2<T> right)
    {
        return new(left.X + right.X, left.Y + right.Y);
    }
    
    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
    public static Vector2<T> Add4(Vector2<T> left, Vector2<T> right)
    {
        return new(left.X + right.X, left.Y + right.Y);
    }
}

ByRefMethodAdd is worse than ByValMethodAdd for the same reasons as above.

Both examples produce identical codegen between the by-ref/by-val versions under DOTNET_JitStressModeNames=STRESS_NO_OLD_PROMOTION (allowing physical promotion to take over). The work item here is figuring out how to detect the dependent promotion early enough to let physical promotion handle the scenario.

@JulieLeeMSFT
Copy link
Member

Closing as .NET 9 work is complete.

@github-project-automation github-project-automation bot moved this from Team User Stories to Done in .NET Core CodeGen Jul 30, 2024
@github-actions github-actions bot locked and limited conversation to collaborators Aug 30, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI User Story A single user-facing feature. Can be grouped under an epic.
Projects
Status: Done
Development

No branches or pull requests

2 participants