-
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
Implementing C# null semantics in the new pipeline #15745
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
519 changes: 519 additions & 0 deletions
519
src/EFCore.Relational/Query/PipeLine/NullSemanticsRewritingVisitor.cs
Large diffs are not rendered by default.
Oops, something went wrong.
196 changes: 196 additions & 0 deletions
196
src/EFCore.Relational/Query/PipeLine/SqlExpressionOptimizingVisitor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Generic; | ||
using System.Linq.Expressions; | ||
using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline; | ||
using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; | ||
|
||
namespace Microsoft.EntityFrameworkCore.Query.Pipeline | ||
{ | ||
public class SqlExpressionOptimizingVisitor : ExpressionVisitor | ||
{ | ||
private readonly ISqlExpressionFactory _sqlExpressionFactory; | ||
|
||
private readonly Dictionary<ExpressionType, ExpressionType> _expressionTypesNegationMap | ||
= new Dictionary<ExpressionType, ExpressionType> | ||
{ | ||
{ ExpressionType.AndAlso, ExpressionType.OrElse }, | ||
{ ExpressionType.OrElse, ExpressionType.AndAlso }, | ||
{ ExpressionType.Equal, ExpressionType.NotEqual }, | ||
{ ExpressionType.NotEqual, ExpressionType.Equal }, | ||
{ ExpressionType.GreaterThan, ExpressionType.LessThanOrEqual }, | ||
{ ExpressionType.GreaterThanOrEqual, ExpressionType.LessThan }, | ||
{ ExpressionType.LessThan, ExpressionType.GreaterThanOrEqual }, | ||
{ ExpressionType.LessThanOrEqual, ExpressionType.GreaterThan }, | ||
}; | ||
|
||
public SqlExpressionOptimizingVisitor(ISqlExpressionFactory sqlExpressionFactory) | ||
{ | ||
_sqlExpressionFactory = sqlExpressionFactory; | ||
} | ||
|
||
protected override Expression VisitExtension(Expression extensionExpression) | ||
{ | ||
if (extensionExpression is SqlUnaryExpression sqlUnaryExpression) | ||
{ | ||
return VisitSqlUnaryExpression(sqlUnaryExpression); | ||
} | ||
|
||
if (extensionExpression is SqlBinaryExpression sqlBinaryExpression) | ||
{ | ||
return VisitSqlBinaryExpression(sqlBinaryExpression); | ||
} | ||
|
||
return base.VisitExtension(extensionExpression); | ||
} | ||
|
||
private Expression VisitSqlUnaryExpression(SqlUnaryExpression sqlUnaryExpression) | ||
{ | ||
// !(true) -> false | ||
// !(false) -> true | ||
if (sqlUnaryExpression.OperatorType == ExpressionType.Not | ||
&& sqlUnaryExpression.Operand is SqlConstantExpression innerConstantBool | ||
&& innerConstantBool.Value is bool value) | ||
{ | ||
return value | ||
? _sqlExpressionFactory.Constant(false, sqlUnaryExpression.TypeMapping) | ||
: _sqlExpressionFactory.Constant(true, sqlUnaryExpression.TypeMapping); | ||
} | ||
|
||
// NULL IS NULL -> true | ||
// non_nullablee_constant IS NULL -> false | ||
if (sqlUnaryExpression.OperatorType == ExpressionType.Equal | ||
&& sqlUnaryExpression.Operand is SqlConstantExpression innerConstantNull1) | ||
{ | ||
return _sqlExpressionFactory.Constant(innerConstantNull1.Value == null, sqlUnaryExpression.TypeMapping); | ||
} | ||
|
||
// NULL IS NOT NULL -> false | ||
// non_nullablee_constant IS NOT NULL -> true | ||
if (sqlUnaryExpression.OperatorType == ExpressionType.NotEqual | ||
&& sqlUnaryExpression.Operand is SqlConstantExpression innerConstantNull2) | ||
{ | ||
return _sqlExpressionFactory.Constant(innerConstantNull2.Value != null, sqlUnaryExpression.TypeMapping); | ||
} | ||
|
||
if (sqlUnaryExpression.Operand is SqlUnaryExpression innerUnary) | ||
{ | ||
if (sqlUnaryExpression.OperatorType == ExpressionType.Not) | ||
{ | ||
// !(!a) -> a | ||
if (innerUnary.OperatorType == ExpressionType.Not) | ||
{ | ||
return Visit(innerUnary.Operand); | ||
} | ||
|
||
if (innerUnary.OperatorType == ExpressionType.Equal) | ||
{ | ||
//!(a IS NULL) -> a IS NOT NULL | ||
return Visit(_sqlExpressionFactory.IsNotNull(innerUnary.Operand)); | ||
} | ||
|
||
//!(a IS NOT NULL) -> a IS NULL | ||
if (innerUnary.OperatorType == ExpressionType.NotEqual) | ||
{ | ||
return Visit(_sqlExpressionFactory.IsNull(innerUnary.Operand)); | ||
} | ||
} | ||
|
||
// (!a) IS NULL <==> a IS NULL | ||
if (sqlUnaryExpression.OperatorType == ExpressionType.Equal | ||
&& innerUnary.OperatorType == ExpressionType.Not) | ||
{ | ||
return Visit(_sqlExpressionFactory.IsNull(innerUnary.Operand)); | ||
} | ||
|
||
// (!a) IS NOT NULL <==> a IS NOT NULL | ||
if (sqlUnaryExpression.OperatorType == ExpressionType.NotEqual | ||
&& innerUnary.OperatorType == ExpressionType.Not) | ||
{ | ||
return Visit(_sqlExpressionFactory.IsNotNull(innerUnary.Operand)); | ||
} | ||
} | ||
|
||
if (sqlUnaryExpression.Operand is SqlBinaryExpression innerBinary) | ||
{ | ||
// De Morgan's | ||
if (innerBinary.OperatorType == ExpressionType.AndAlso | ||
|| innerBinary.OperatorType == ExpressionType.OrElse) | ||
{ | ||
var newLeft = (SqlExpression)Visit(_sqlExpressionFactory.Not(innerBinary.Left)); | ||
var newRight = (SqlExpression)Visit(_sqlExpressionFactory.Not(innerBinary.Right)); | ||
|
||
return innerBinary.OperatorType == ExpressionType.AndAlso | ||
? _sqlExpressionFactory.OrElse(newLeft, newRight) | ||
: _sqlExpressionFactory.AndAlso(newLeft, newRight); | ||
} | ||
|
||
// note that those optimizations are only valid in 2-value logic | ||
// they are safe to do here because null semantics removes possibility of nulls in the tree | ||
// however if we decide to do "partial" null semantics (that doesn't distinguish between NULL and FALSE, e.g. for predicates) | ||
// we need to be extra careful here | ||
if (_expressionTypesNegationMap.ContainsKey(innerBinary.OperatorType)) | ||
{ | ||
return Visit( | ||
_sqlExpressionFactory.MakeBinary( | ||
_expressionTypesNegationMap[innerBinary.OperatorType], | ||
innerBinary.Left, | ||
innerBinary.Right, | ||
innerBinary.TypeMapping)); | ||
} | ||
} | ||
|
||
var newOperand = (SqlExpression)Visit(sqlUnaryExpression.Operand); | ||
|
||
return sqlUnaryExpression.Update(newOperand); | ||
} | ||
|
||
private Expression VisitSqlBinaryExpression(SqlBinaryExpression sqlBinaryExpression) | ||
{ | ||
var newLeft = (SqlExpression)Visit(sqlBinaryExpression.Left); | ||
var newRight = (SqlExpression)Visit(sqlBinaryExpression.Right); | ||
|
||
if (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso | ||
|| sqlBinaryExpression.OperatorType == ExpressionType.OrElse) | ||
{ | ||
|
||
var newLeftConstant = newLeft as SqlConstantExpression; | ||
var newRightConstant = newRight as SqlConstantExpression; | ||
|
||
// true && a -> a | ||
// true || a -> true | ||
// false && a -> false | ||
// false || a -> a | ||
if (newLeftConstant != null) | ||
{ | ||
return sqlBinaryExpression.OperatorType == ExpressionType.AndAlso | ||
? (bool)newLeftConstant.Value | ||
? newRight | ||
: newLeftConstant | ||
: (bool)newLeftConstant.Value | ||
? newLeftConstant | ||
: newRight; | ||
} | ||
else if (newRightConstant != null) | ||
{ | ||
// a && true -> a | ||
// a || true -> true | ||
// a && false -> false | ||
// a || false -> a | ||
return sqlBinaryExpression.OperatorType == ExpressionType.AndAlso | ||
? (bool)newRightConstant.Value | ||
? newLeft | ||
: newRightConstant | ||
: (bool)newRightConstant.Value | ||
? newRightConstant | ||
: newLeft; | ||
} | ||
|
||
return sqlBinaryExpression.Update(newLeft, newRight); | ||
} | ||
|
||
return sqlBinaryExpression.Update(newLeft, newRight); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
C# 8 switch expression FTW (more terse, more efficient):
If you need to check whether negation is possible, we can have TryNegate.
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.
Which one would be more performant? Readonly dictionary should give O(1) access. Methodcall may need to check through all cases (rejected ones) before reaching correct one.
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.
A switch over an enum is a switch over its underlying type - so over an int here, which in most cases will do an efficient lookup table under the hood. This is in general much faster than any dictionary - see benchmark below. Also no allocations whatsoever for the dictionary and its internals.
Note that the difference here specifically may be negligible in the grander scheme - especially since this is query compilation only - but a switch expression also seems more terse so why not.
And because it's so easy to do, here's a benchmark comparison:
// * Summary *
BenchmarkDotNet=v0.11.5, OS=ubuntu 19.04
Intel Core i7-6700HQ CPU 2.60GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.0.100-preview4-011223
[Host] : .NET Core 3.0.0-preview5-27626-15 (CoreCLR 4.6.27622.75, CoreFX 4.700.19.22408), 64bit RyuJIT
DefaultJob : .NET Core 3.0.0-preview5-27626-15 (CoreCLR 4.6.27622.75, CoreFX 4.700.19.22408), 64bit RyuJIT
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.
If switch is faster let's use it. Is it specific for Enum case only of any type? I would guess not all pattern matching is as cheap as enums over int.
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.
Here are the results for string keys, switch wins as well:
In general, for small datasets switch should (almost) always be faster than a dictionary - when in doubt we can always do a quick benchmark to find out.