Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Adding by-ref support in stack spilling in System.Linq.Expressions #13126

Merged
merged 11 commits into from
Nov 23, 2016

Conversation

bartdesmet
Copy link
Contributor

This addresses issue #11740 on stack spilling limitations when dealing with ref parameters and value type instances. Dealing with this restriction manually can be quite bothersome; I've run into this personally when building a (de)serialization framework that emits expression trees for efficiency. However, when value types are involved and the generated expression uses Try nodes, it's easy to fall victim to this restriction.

A trivial example of this limitation is shown below:

var dt = Parameter(typeof(DateTimeOffset));

var e =
    Lambda<Func<DateTimeOffset, DateTimeOffset>>(
        Call(
            // a value type instance
            dt,
            typeof(DateTimeOffset).GetMethod(nameof(DateTimeOffset.AddYears)),
            // causes stack spilling
            TryFinally(
                Constant(1),
                Empty()
            )
        ),
        dt
    );

// throws NotSupportedException
var f = e.Compile();

The code throws the following exception:

Unhandled Exception: System.NotSupportedException: TryExpression is not supported as a child expression when accessing a member on type 'System.DateTimeOffset' because it is a value type. Construct the tree so the TryExpression is not nested inside of this expression.
   at System.Linq.Expressions.Compiler.StackSpiller.RequireNotRefInstance(Expression instance)
   at System.Linq.Expressions.Compiler.StackSpiller.RewriteMethodCallExpression(Expression expr, Stack stack)
   at System.Linq.Expressions.Compiler.StackSpiller.RewriteExpression(Expression node, Stack stack)
   at System.Linq.Expressions.Compiler.StackSpiller.RewriteExpressionFreeTemps(Expression expression, Stack stack)
   at System.Linq.Expressions.Compiler.StackSpiller.Rewrite[T](Expression`1 lambda)
   at System.Linq.Expressions.Compiler.LambdaCompiler.Compile(LambdaExpression lambda, DebugInfoGenerator debugInfoGenerator)
   at System.Linq.Expressions.Expression`1.Compile()

Dealing with this in the suggested way requires an overhaul of the generated expression tree. Lifting this restriction seems worth the effort.

The approach used in this PR is as follows:

  • Rather than throwing an exception from RequireNotRefInstance or RequireNoRefArgs, we use all these places to record the child expression's index of the value type instance or ref argument in the ChildRewriter instance.
  • When analysis of child expressions is done and the rewrite action is SpillStack, temporary variables for the spilled child expressions are created. The recorded ref information (by child expression index) is consulted to decide on whether to use a ByRef type instead.
  • In order to store a reference to a value to a ByRef temporary variable, a new internal-only RefExpression is used as the right-hand side of the assignment. This node behaves much like ref locals in C# 7.0.
  • The LambdaCompiler knows about the occurrence of ref locals and emits valid IL when encountering these. For example, rather than emitting ldloca to obtain a reference to a value-typed local, it can emit ldloc in case the local is a ref local introduced by spilling.

Note that the use of an internal-only extension node is similar to the already existing internal-only SpilledExpressionBlock introduced by the stack spiller as well. These node types are only introduced during stack spilling and never leak to the user.

For the example shown above, the rewritten expression tree after stack spilling looks like:

var dt = Parameter(typeof(DateTimeOffset));
var t1 = Parameter(typeof(DateTimeOffset).MakeByRefType(), "$temp$0");
var t2 = Parameter(typeof(int), "$temp$1");

var e =
    Lambda<Func<DateTimeOffset, DateTimeOffset>>(
        Block(
            new[] { t1, t2 },
            // save `ref dt` to a temporary variable
            Assign(
                t1,
                new RefExpression(dt)
            ),
            Assign(
                t2,
                TryFinally(
                    Constant(1),
                    Empty()
                )
            ),
            Call(
                // emit for Call supports ref locals
                t1,
                typeof(DateTimeOffset).GetMethod(nameof(DateTimeOffset.AddYears)),
                t2
            )
        ),
        dt
    )
)

Note that the construction of the rewritten expression tree during stack spilling does not use the Expression factory methods; instead, it uses Make methods and constructors on the node types. This enables nodes such as Call, Member, etc. to be constructed where the target instance or any arguments are ByRef types. The factory methods don't support these (and we don't want to change that now), but the changes to the LambdaCompiler emit functionality support the use of ByRef types in these positions (see EmitInstance and AddressOf changes).

For the running example, the emitted IL code looks more or less like this:

.method valuetype System.DateTimeOffset lambda_method(valuetype System.DateTimeOffset)
{
    .locals init (
        [0] valuetype System.DateTimeOffset&,
        [1] int32,
        [2] int32
    )

    // spilling of instance (cf. EmitAddress)
    ldarga.0
    stloc.0   // $temp$0

    // evaluation stack empty, as required
    .try
    {
        ldc.i4.1
        stloc.2
        leave E
    }
    finally
    {
        endfinally
    }
E:
    ldloc.2
    stloc.1   // $temp$1

    // perform the call in spilled operands
    ldloc.0
    ldloc.1
    call valuetype System.DateTimeOffset System.DateTimeOffset::AddYears(int32)
    ret
}

This PR lifts the "no ref" restriction in pretty much all places, except for:

  • Arguments passed to Method of BinaryExpression and UnaryExpression (when can ByRef occur here?)
  • Arguments passed to the Invoke method of a dynamic call site (to be reviewed)
  • Instance of MemberExpression where Member is a property (what's there to spill in this case?)

Many IL-level tests are included. Issue #13007 was uncovered by doing this work.

CC @VSadov

@karelz
Copy link
Member

karelz commented Nov 6, 2016

@VSadov @OmarTawfik can you please review the code?

@karelz
Copy link
Member

karelz commented Nov 9, 2016

@VSadov @OmarTawfik ping?

@karelz
Copy link
Member

karelz commented Nov 15, 2016

@VSadov @OmarTawfik ping again? This is getting stale :(

IL_0015: ret
}");

var f = e.Compile();
Copy link
Contributor

Choose a reason for hiding this comment

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

The IL test obviously only applies to the compiler, but does this work with the interpreter? Could it?

Copy link
Member

Choose a reason for hiding this comment

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

Also curious - can this work with interpreter or we will have more restricted functionality there?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It turns out the interpreter works just fine; the emitted instructions operate on boxed values and execute late-bound reflection calls to these, allowing the value in the box to get mutated. We also don't spill in the interpreter, so there's no need for ref locals etc. This said, there are existing limitations such as ref parameters where we don't have atomic writes and use write-back behavior (see https://github.com/dotnet/corefx/issues/4411).

I'll add tests to assert the emitted interpreter instructions as well so we can catch any regressions against the current (correct) behavior for the expressions tested here.

EmitExpressionAddress(node, type);
// NB: Stack spilling can generate ref locals.

if (node.Type == type.MakeByRefType())
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to avoid "MakeByRefType" here. Can we check "IsByRef" and then compare underlying type?

/// the stack spiller when spilling value type instances or arguments
/// passed to ref parameters.
/// </summary>
internal sealed class RefExpression : Expression
Copy link
Member

Choose a reason for hiding this comment

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

Another approach, that might avoid creating a new node, could be adding a IsByRef bool flag to Assignment node.

That is - if assignment is ByRef, then an address of RHS is taken and assigned to the LHS that must be a byref local.
If assignment is regular, and LHS is a ref local - just do an indirect assignment. (not sure if the case will arise)

ByRef assignment can itself be used in Emit Expression and EmitAddrerss., if such need arises.
ByRef assignment is essentially a duplicated RHS address, so it can be used as both value and address. - I.E. ref assignment can itself ve a target of a byval assignment.

I think that could be simpler than introducing an extension node.

anpete added a commit to dotnet/efcore that referenced this pull request Nov 21, 2016
Adding bad data error handling logic to materialization introduced the possibility
of hitting a known expression compiler limitation (as described here: dotnet/corefx#13126).

This change works around the limitation by moving the read-value try-catch to a helper method, thus we no
longer insert a try-catch directly into the output ET.
anpete added a commit to dotnet/efcore that referenced this pull request Nov 21, 2016
Adding bad data error handling logic to materialization introduced the possibility
of hitting a known expression compiler limitation (as described here: dotnet/corefx#13126).

This change works around the limitation by moving the read-value try-catch to a helper method, thus we no
longer insert a try-catch directly into the output ET.
@VSadov
Copy link
Member

VSadov commented Nov 23, 2016

LGTM

IP_0006: EnterFinally[0] -> 6
IP_0007: LeaveFinally()
}
IP_0008: StoreField()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will review the instruction printing separately later; this should include the field name for sure. It'd likely be a good idea to review all reflection ToString usage here because many of them don't quite produce enough information to see what's going on (e.g. the string representation of MethodInfo does not include the declaring type).

@VSadov
Copy link
Member

VSadov commented Nov 23, 2016

@bartdesmet - let us know when this is ok to merge

@bartdesmet
Copy link
Contributor Author

OK to merge now.

@VSadov VSadov merged commit 1840aa2 into dotnet:master Nov 23, 2016
@bartdesmet bartdesmet deleted the Issue-11740 branch November 23, 2016 21:26
@karelz karelz modified the milestone: 1.2.0 Dec 3, 2016
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
Adding by-ref support in stack spilling in System.Linq.Expressions

Commit migrated from dotnet/corefx@1840aa2
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.

6 participants