-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Adding by-ref support in stack spilling in System.Linq.Expressions #13126
Conversation
@VSadov @OmarTawfik can you please review the code? |
@VSadov @OmarTawfik ping? |
@VSadov @OmarTawfik ping again? This is getting stale :( |
IL_0015: ret | ||
}"); | ||
|
||
var f = e.Compile(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
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.
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.
LGTM |
IP_0006: EnterFinally[0] -> 6 | ||
IP_0007: LeaveFinally() | ||
} | ||
IP_0008: StoreField() |
There was a problem hiding this comment.
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).
@bartdesmet - let us know when this is ok to merge |
OK to merge now. |
Adding by-ref support in stack spilling in System.Linq.Expressions Commit migrated from dotnet/corefx@1840aa2
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 usesTry
nodes, it's easy to fall victim to this restriction.A trivial example of this limitation is shown below:
The code throws the following exception:
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:
RequireNotRefInstance
orRequireNoRefArgs
, we use all these places to record the child expression's index of the value type instance orref
argument in theChildRewriter
instance.SpillStack
, temporary variables for the spilled child expressions are created. The recordedref
information (by child expression index) is consulted to decide on whether to use aByRef
type instead.ByRef
temporary variable, a new internal-onlyRefExpression
is used as the right-hand side of the assignment. This node behaves much likeref
locals in C# 7.0.LambdaCompiler
knows about the occurrence ofref
locals and emits valid IL when encountering these. For example, rather than emittingldloca
to obtain a reference to a value-typed local, it can emitldloc
in case the local is aref
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:
Note that the construction of the rewritten expression tree during stack spilling does not use the
Expression
factory methods; instead, it usesMake
methods and constructors on the node types. This enables nodes such asCall
,Member
, etc. to be constructed where the target instance or any arguments areByRef
types. The factory methods don't support these (and we don't want to change that now), but the changes to theLambdaCompiler
emit functionality support the use ofByRef
types in these positions (seeEmitInstance
andAddressOf
changes).For the running example, the emitted IL code looks more or less like this:
This PR lifts the "no ref" restriction in pretty much all places, except for:
Method
ofBinaryExpression
andUnaryExpression
(when canByRef
occur here?)Invoke
method of a dynamic call site (to be reviewed)MemberExpression
whereMember
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