-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
Simplify AND and OR #34133
Simplify AND and OR #34133
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1358,8 +1358,14 @@ or ExpressionType.LessThan | |
} | ||
|
||
return result is SqlBinaryExpression sqlBinaryResult | ||
&& sqlBinaryExpression.OperatorType is ExpressionType.AndAlso or ExpressionType.OrElse | ||
? SimplifyLogicalSqlBinaryExpression(sqlBinaryResult) | ||
&& sqlBinaryResult.OperatorType is ExpressionType.AndAlso or ExpressionType.OrElse | ||
? _sqlExpressionFactory.MakeBinary( // invoke MakeBinary simplifications | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One slight disadvantage here is that this always reconstructs the SqlBinaryExpression (allocation), regardless of whether it's simplified or not. We could refactor a bit and have a SimplifyBinary method on SqlExpressionFactory that simply returns the original expression if it couldn't be optimized (reusing the actual code internally from MakeBinary(). It might be a bit of a micro-optimization, but it might be worth doing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, this is something I am hitting with the branch on top of this (you might guess... it's In the meantime, I might try a middle-ground solution, i.e. creating the new expression and if it is
This might not yet be ideal, but it should be a simple change that makes the code a little closer to the target. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK. We generally don't stress too much about compilation perf, but it would be good to avoid allocating/copying every single binary expression in the tree (which can't be simplified). What I had in mind was something the following on SqlExpressionFactory: SqlExpression MakeBinary(ExpressionType operatorType, SqlExpression left, SqlExpression right, SqlExpression? existingBinary) Coming from MakeBinary, existingBinary would be null so the method constructs a new SqlBinaryExpression if no simplification is possible. If, however, an existingBinary is passed and left/right can't be simplified, existingBinary is simply returned. |
||
sqlBinaryResult.OperatorType, | ||
sqlBinaryResult.Left, | ||
sqlBinaryResult.Right, | ||
sqlBinaryResult.TypeMapping, | ||
sqlBinaryResult | ||
)! | ||
: result; | ||
|
||
SqlExpression AddNullConcatenationProtection(SqlExpression argument, RelationalTypeMapping typeMapping) | ||
|
@@ -1796,26 +1802,16 @@ private SqlExpression RewriteNullSemantics( | |
{ | ||
nullable = leftNullable || rightNullable; | ||
|
||
return SimplifyLogicalSqlBinaryExpression( | ||
_sqlExpressionFactory.OrElse( | ||
body, | ||
SimplifyLogicalSqlBinaryExpression( | ||
_sqlExpressionFactory.AndAlso(leftIsNull, rightIsNull)))); | ||
return _sqlExpressionFactory.OrElse(body, _sqlExpressionFactory.AndAlso(leftIsNull, rightIsNull)); | ||
} | ||
|
||
// doing a full null semantics rewrite - removing all nulls from truth table | ||
nullable = false; | ||
|
||
// (a == b && (a != null && b != null)) || (a == null && b == null) | ||
body = SimplifyLogicalSqlBinaryExpression( | ||
_sqlExpressionFactory.OrElse( | ||
SimplifyLogicalSqlBinaryExpression( | ||
_sqlExpressionFactory.AndAlso( | ||
body, | ||
SimplifyLogicalSqlBinaryExpression( | ||
_sqlExpressionFactory.AndAlso(leftIsNotNull, rightIsNotNull)))), | ||
SimplifyLogicalSqlBinaryExpression( | ||
_sqlExpressionFactory.AndAlso(leftIsNull, rightIsNull)))); | ||
body = _sqlExpressionFactory.OrElse( | ||
_sqlExpressionFactory.AndAlso(body, _sqlExpressionFactory.AndAlso(leftIsNotNull, rightIsNotNull)), | ||
_sqlExpressionFactory.AndAlso(leftIsNull, rightIsNull)); | ||
|
||
if (sqlBinaryExpression.OperatorType == ExpressionType.NotEqual) | ||
{ | ||
|
@@ -1826,65 +1822,6 @@ private SqlExpression RewriteNullSemantics( | |
return body; | ||
} | ||
|
||
private SqlExpression SimplifyLogicalSqlBinaryExpression(SqlExpression expression) | ||
{ | ||
if (expression is not SqlBinaryExpression sqlBinaryExpression) | ||
{ | ||
return expression; | ||
} | ||
|
||
if (sqlBinaryExpression is | ||
{ | ||
Left: SqlUnaryExpression { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual } leftUnary, | ||
Right: SqlUnaryExpression { OperatorType: ExpressionType.Equal or ExpressionType.NotEqual } rightUnary | ||
} | ||
&& leftUnary.Operand.Equals(rightUnary.Operand)) | ||
{ | ||
// a is null || a is null -> a is null | ||
// a is not null || a is not null -> a is not null | ||
// a is null && a is null -> a is null | ||
// a is not null && a is not null -> a is not null | ||
// a is null || a is not null -> true | ||
// a is null && a is not null -> false | ||
Comment on lines
-1847
to
-1848
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
EDIT: these are now implemented There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would also be possible to check for a stronger condition, i.e. |
||
return leftUnary.OperatorType == rightUnary.OperatorType | ||
? leftUnary | ||
: _sqlExpressionFactory.Constant( | ||
sqlBinaryExpression.OperatorType == ExpressionType.OrElse, sqlBinaryExpression.TypeMapping); | ||
} | ||
|
||
// true && a -> a | ||
// true || a -> true | ||
// false && a -> false | ||
// false || a -> a | ||
if (sqlBinaryExpression.Left is SqlConstantExpression { Value: bool leftBoolValue } newLeftConstant) | ||
{ | ||
return sqlBinaryExpression.OperatorType == ExpressionType.AndAlso | ||
? leftBoolValue | ||
? sqlBinaryExpression.Right | ||
: newLeftConstant | ||
: leftBoolValue | ||
? newLeftConstant | ||
: sqlBinaryExpression.Right; | ||
} | ||
|
||
if (sqlBinaryExpression.Right is SqlConstantExpression { Value: bool rightBoolValue } newRightConstant) | ||
{ | ||
// a && true -> a | ||
// a || true -> true | ||
// a && false -> false | ||
// a || false -> a | ||
return sqlBinaryExpression.OperatorType == ExpressionType.AndAlso | ||
? rightBoolValue | ||
? sqlBinaryExpression.Left | ||
: newRightConstant | ||
: rightBoolValue | ||
? newRightConstant | ||
: sqlBinaryExpression.Left; | ||
} | ||
|
||
return sqlBinaryExpression; | ||
} | ||
|
||
/// <summary> | ||
/// Attempts to simplify a unary not operation on a non-nullable operand. | ||
/// </summary> | ||
|
@@ -1938,14 +1875,13 @@ protected virtual SqlExpression OptimizeNonNullableNotExpression(SqlExpression e | |
var left = OptimizeNonNullableNotExpression(_sqlExpressionFactory.Not(sqlBinaryOperand.Left)); | ||
var right = OptimizeNonNullableNotExpression(_sqlExpressionFactory.Not(sqlBinaryOperand.Right)); | ||
|
||
return SimplifyLogicalSqlBinaryExpression( | ||
_sqlExpressionFactory.MakeBinary( | ||
sqlBinaryOperand.OperatorType == ExpressionType.AndAlso | ||
? ExpressionType.OrElse | ||
: ExpressionType.AndAlso, | ||
left, | ||
right, | ||
sqlBinaryOperand.TypeMapping)!); | ||
return _sqlExpressionFactory.MakeBinary( | ||
sqlBinaryOperand.OperatorType == ExpressionType.AndAlso | ||
? ExpressionType.OrElse | ||
: ExpressionType.AndAlso, | ||
left, | ||
right, | ||
sqlBinaryOperand.TypeMapping)!; | ||
} | ||
|
||
// use equality where possible | ||
|
@@ -2266,14 +2202,13 @@ private SqlExpression ProcessNullNotNull(SqlExpression sqlExpression, bool opera | |
sqlUnaryExpression.TypeMapping)!, | ||
operandNullable); | ||
|
||
return SimplifyLogicalSqlBinaryExpression( | ||
_sqlExpressionFactory.MakeBinary( | ||
sqlUnaryExpression.OperatorType == ExpressionType.Equal | ||
? ExpressionType.OrElse | ||
: ExpressionType.AndAlso, | ||
left, | ||
right, | ||
sqlUnaryExpression.TypeMapping)!); | ||
return _sqlExpressionFactory.MakeBinary( | ||
sqlUnaryExpression.OperatorType == ExpressionType.Equal | ||
? ExpressionType.OrElse | ||
: ExpressionType.AndAlso, | ||
left, | ||
right, | ||
sqlUnaryExpression.TypeMapping)!; | ||
} | ||
|
||
case SqlFunctionExpression sqlFunctionExpression: | ||
|
@@ -2295,14 +2230,13 @@ private SqlExpression ProcessNullNotNull(SqlExpression sqlExpression, bool opera | |
sqlUnaryExpression.TypeMapping)!, | ||
operandNullable)) | ||
.Aggregate( | ||
(l, r) => SimplifyLogicalSqlBinaryExpression( | ||
_sqlExpressionFactory.MakeBinary( | ||
sqlUnaryExpression.OperatorType == ExpressionType.Equal | ||
? ExpressionType.AndAlso | ||
: ExpressionType.OrElse, | ||
l, | ||
r, | ||
sqlUnaryExpression.TypeMapping)!)); | ||
(l, r) => _sqlExpressionFactory.MakeBinary( | ||
sqlUnaryExpression.OperatorType == ExpressionType.Equal | ||
? ExpressionType.AndAlso | ||
: ExpressionType.OrElse, | ||
l, | ||
r, | ||
sqlUnaryExpression.TypeMapping)!); | ||
} | ||
|
||
if (!sqlFunctionExpression.IsNullable) | ||
|
@@ -2348,10 +2282,9 @@ private SqlExpression ProcessNullNotNull(SqlExpression sqlExpression, bool opera | |
sqlUnaryExpression.TypeMapping)!, | ||
operandNullable)) | ||
.Aggregate( | ||
(r, e) => SimplifyLogicalSqlBinaryExpression( | ||
sqlUnaryExpression.OperatorType == ExpressionType.Equal | ||
? _sqlExpressionFactory.OrElse(r, e) | ||
: _sqlExpressionFactory.AndAlso(r, e))); | ||
(r, e) => sqlUnaryExpression.OperatorType == ExpressionType.Equal | ||
? _sqlExpressionFactory.OrElse(r, e) | ||
: _sqlExpressionFactory.AndAlso(r, e)); | ||
|
||
return result; | ||
} | ||
|
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.
@ranma42 I thought this a bit more over the night.
It's worth considering that
left.Equals(right)
is a recursive visitation, which is very different from simple pattern matching like checking for false/true constants. In a contrived case where 100 AndAlso expression are being composed over each either, the number of visitations grows very quickly, and this can lead to a compilation performance issue (I say this is contrived, though in some cases where query trees are generated automatically it may happen).I don't think this is necessarily something we need to worry about right now - SQL expression trees are typically not that deep, and IIUC the problem also existed before when the optimization happened as part of SqlNullabilitryProcessor. But it's something to consider; for example, SqlExpression could cache its hash code when it's first calculated, and our recursive equality logic could simply check that first, and return early rather than recursing.
Let me know if you have any thoughts on this - we can open a separate issue to track this work (which again, I don't think is urgent).
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.
You are right, this problem is already present in a few places in the codebase and it is something I would definitely like to improve (and use more 😈 ).
Your suggestion (compute & reuse the hash code) is indeed the most straightforward way to improve it... it would be trivial to implement if the expressions were immutable 😉 #32927.
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.
Posted #34149 to track this
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.
FWIW almost our entire SQL tree is already immutable, with the big exception being SelectExpression (and that's also supposed to be immutable once translation is complete, i.e. in post-processing - though I wouldn't commit to it 100%...). So we should be able to implement the has code optimization today for almost all expression types, and specifically skip SelectExpression.
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.
SelectExpression
can be a subtree of most expressions (it is used inInExpression
) :(Maybe this is something we want to do on an IR and possibly skip/ignore on C# and SQL expressions.
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.
Good point, you're right. I don't think we need to wait on an IR here though - #32927 (full immutability) is something I want us to do for many other reasons, regardless/before an IR.