Open maumar opened 3 days ago
smoking gun benchmark:
internal class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run(args, typeof(Program).Assembly);
}
}
public static class BenchmarkRunner
{
public static void Run(string[] args, Assembly assembly, IConfig config = null)
{
config ??= DefaultConfig.Instance;
config = config
.AddDiagnoser(MemoryDiagnoser.Default)
.AddColumn(StatisticColumn.OperationsPerSecond);
BenchmarkSwitcher.FromAssembly(assembly).Run(args, config);
}
}
public class InterpretationBenchmark
{
private Expression<Func<object, Func<object?, object?, bool>[]>> _expression;
private Func<object, object, bool>[] _compiled;
private Func<object, object, bool>[] _compiledInterpreted;
[GlobalSetup]
public virtual void Initialize()
{
var left = Expression.Parameter(typeof(object), "left");
var right = Expression.Parameter(typeof(object), "right");
var prm = Expression.Parameter(typeof(object));
_expression = Expression.Lambda<Func<object, Func<object?, object?, bool>[]>>(
Expression.NewArrayInit(
typeof(Func<object, object, bool>),
Expression.Lambda<Func<object?, object?, bool>>(
Expression.Condition(
Expression.Equal(left, Expression.Constant(null)),
Expression.Equal(right, Expression.Constant(null)),
Expression.AndAlso(
Expression.NotEqual(right, Expression.Constant(null)),
Expression.Equal(
Expression.Convert(left, typeof(int)),
Expression.Convert(left, typeof(int))))),
left,
right)), prm);
_compiled = _expression.Compile()("_");
_compiledInterpreted = _expression.Compile(preferInterpretation: true)("_");
}
[Benchmark]
public virtual void Compiled()
{
var equal = 0;
for (var i = 0; i < 100; i++)
{
for (var j = 0; j < 100; j++)
{
if (_compiled[0](i, j))
{
equal++;
}
}
}
}
[Benchmark]
public virtual void CompiledInterpolated()
{
var equal = 0;
for (var i = 0; i < 100; i++)
{
for (var j = 0; j < 100; j++)
{
if (_compiledInterpreted[0](i, j))
{
equal++;
}
}
}
}
}
results:
Method | Mean | Error | StdDev | Op/s | Gen0 | Allocated |
---|---|---|---|---|---|---|
Compiled | 65.69 us | 0.706 us | 0.660 us | 15,223.4 | 76.4160 | 468.75 KB |
CompiledInterpolated | 930.20 us | 7.046 us | 6.591 us | 1,075.0 | 433.5938 | 2656.25 KB |
reopening for potential patch
This is part of a bigger perf regression between EF8 and EF9: https://github.com/dotnet/efcore/issues/35053
When processing LiftableConstantExpressions in the regular (non-AOT) mode we compile the resolver lambda and then evaluate it to get back the actual constant we plan to use. We do the compilation in the interpretation mode and that causes problems if the resulting object is itself (or contains) a delegate. Compiling into a delegate using interpreted mode, has impact on allocations as well as execution speed.
before the fix (this already includes the invoke fix https://github.com/dotnet/efcore/issues/35206)
after the fix