Начнем с того, что это не то же самое, что Почему Func <> создается из Expression> медленнее, чем Func <>, объявленный напрямую? и, на удивление, просто противоположен этому. Кроме того, все ссылки и вопросы, которые я обнаружил при исследовании этой проблемы, возникли в период 2010-2012 гг., Поэтому я решил открыть новый вопрос здесь, чтобы узнать, следует ли обсуждать текущее состояние делегата. поведение в экосистеме .NET.
Тем не менее, я использую .NET Core 2.0 и .NET 4.7.1 и вижу некоторые любопытные метрики производительности в отношении делегатов, которые создаются из скомпилированного выражения, по сравнению с делегатами, которые описаны и объявлены как объект CLR.
Для некоторого контекста того, как я наткнулся на эту проблему, я проводил тест, включающий выборку данных в массивах из 1000 и 10000 объектов, и заметил, что, если я использую скомпилированное выражение, оно получит более быстрые результаты по всем направлениям. Мне удалось свести это к очень простому проекту, который воспроизводит эту проблему, которую вы можете найти здесь:
https://github.com/Mike-EEE/StackOverflow.Performance.Delegates
Для тестирования у меня есть два набора эталонных тестов, которые показывают скомпилированный делегат в паре с объявленным делегатом, в результате чего получается всего четыре базовых эталонных теста.
Первый набор делегатов состоит из пустого делегата, который возвращает пустую строку. Второй набор - это делегат с простым выражением. Я хотел продемонстрировать, что эта проблема возникает с простыми делегатами, а также с делегатами с определенным телом внутри.
Затем эти тесты выполняются в среде CLR и среде .NET Core с помощью превосходного продукта производительности Benchmark.NET , в результате чего получается восемь тестов. Кроме того, я также использую столь же превосходный диагностический анализатор разборки Benchmark.NET , чтобы выдавать разборку, возникшую во время JIT эталонных измерений. Я делюсь результатами этого ниже.
Вот код, который запускает тесты. Вы можете видеть, что это очень просто:
[CoreJob, ClrJob, DisassemblyDiagnoser(true, printSource: true)]
public class Delegates
{
readonly DelegatePair<string, string> _empty;
readonly DelegatePair<string, int> _expression;
readonly string _message;
public Delegates() : this(new DelegatePair<string, string>(_ => default, _ => default),
new DelegatePair<string, int>(x => x.Length, x => x.Length)) {}
public Delegates(DelegatePair<string, string> empty, DelegatePair<string, int> expression,
string message = "Hello World!")
{
_empty = empty;
_expression = expression;
_message = message;
EmptyDeclared();
EmptyCompiled();
ExpressionDeclared();
ExpressionCompiled();
}
[Benchmark]
public void EmptyDeclared() => _empty.Declared(default);
[Benchmark]
public void EmptyCompiled() => _empty.Compiled(default);
[Benchmark]
public void ExpressionDeclared() => _expression.Declared(_message);
[Benchmark]
public void ExpressionCompiled() => _expression.Compiled(_message);
}
Это результаты, которые я вижу в Benchmark.NET:
.
BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.371 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i7-4820K CPU 3.70GHz (Haswell), 1 CPU, 8 logical and 8 physical cores
.NET Core SDK=2.1.300-preview2-008533
[Host] : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
Clr : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2633.0
Core : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
Method | Job | Runtime | Mean | Error | StdDev |
------------------- |----- |-------- |----------:|----------:|----------:|
EmptyDeclared | Clr | Clr | 1.3691 ns | 0.0302 ns | 0.0282 ns |
EmptyCompiled | Clr | Clr | 1.1851 ns | 0.0381 ns | 0.0357 ns |
ExpressionDeclared | Clr | Clr | 1.3805 ns | 0.0314 ns | 0.0294 ns |
ExpressionCompiled | Clr | Clr | 1.1431 ns | 0.0396 ns | 0.0371 ns |
EmptyDeclared | Core | Core | 1.5733 ns | 0.0329 ns | 0.0308 ns |
EmptyCompiled | Core | Core | 0.9326 ns | 0.0275 ns | 0.0244 ns |
ExpressionDeclared | Core | Core | 1.6040 ns | 0.0394 ns | 0.0368 ns |
ExpressionCompiled | Core | Core | 0.9380 ns | 0.0485 ns | 0.0631 ns |
Обратите внимание, что тесты, использующие скомпилированный делегат, всегда быстрее.
Наконец, вот результаты разборки для каждого теста:
<style type="text/css">
table { border-collapse: collapse; display: block; width: 100%; overflow: auto; }
td, th { padding: 6px 13px; border: 1px solid #ddd; }
tr { background-color: #fff; border-top: 1px solid #ccc; }
tr:nth-child(even) { background: #f8f8f8; }
</style>
</head>
<body>
<table>
<thead>
<tr><th colspan="2">Delegates.EmptyDeclared</th></tr>
<tr>
<th>.NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2633.0</th>
<th>.NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT</th>
</tr>
</thead>
<tbody>
<tr>
<td style="vertical-align:top;"><pre><code>
00007ffd`4f8f0ea0 StackOverflow.Performance.Delegates.Delegates.EmptyDeclared()
public void EmptyDeclared() => _empty.Declared(default);
^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`4f8f0ea4 4883c110 add rcx,10h
00007ffd`4f8f0ea8 488b01 mov rax,qword ptr [rcx]
00007ffd`4f8f0eab 488b4808 mov rcx,qword ptr [rax+8]
00007ffd`4f8f0eaf 33d2 xor edx,edx
00007ffd`4f8f0eb1 ff5018 call qword ptr [rax+18h]
00007ffd`4f8f0eb4 90 nop
00007ffd`39c8d8b0 StackOverflow.Performance.Delegates.Delegates.EmptyDeclared()
public void EmptyDeclared() => _empty.Declared(default);
^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`39c8d8b4 4883c110 add rcx,10h
00007ffd`39c8d8b8 488b01 mov rax,qword ptr [rcx]
00007ffd`39c8d8bb 488b4808 mov rcx,qword ptr [rax+8]
00007ffd`39c8d8bf 33d2 xor edx,edx
00007ffd`39c8d8c1 ff5018 call qword ptr [rax+18h]
00007ffd`39c8d8c4 90 nop
Delegates.EmptyCompiled |
.NET Framework 4.7.1 (CLR 4.0.30319.42000), 64-разрядная версия RyuJIT-v4.7.2633.0 |
.NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64-разрядная версия RyuJIT |
00007ffd`4f8e0ef0 StackOverflow.Performance.Delegates.Delegates.EmptyCompiled()
public void EmptyCompiled() => _empty.Compiled(default);
^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`4f8e0ef4 4883c110 add rcx,10h
00007ffd`4f8e0ef8 488b4108 mov rax,qword ptr [rcx+8]
00007ffd`4f8e0efc 488b4808 mov rcx,qword ptr [rax+8]
00007ffd`4f8e0f00 33d2 xor edx,edx
00007ffd`4f8e0f02 ff5018 call qword ptr [rax+18h]
00007ffd`4f8e0f05 90 nop
|
00007ffd`39c8d900 StackOverflow.Performance.Delegates.Delegates.EmptyCompiled()
public void EmptyCompiled() => _empty.Compiled(default);
^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`39c8d904 4883c110 add rcx,10h
00007ffd`39c8d908 488b4108 mov rax,qword ptr [rcx+8]
00007ffd`39c8d90c 488b4808 mov rcx,qword ptr [rax+8]
00007ffd`39c8d910 33d2 xor edx,edx
00007ffd`39c8d912 ff5018 call qword ptr [rax+18h]
00007ffd`39c8d915 90 nop
|
Delegates.ExpressionDeclared |
.NET Framework 4.7.1 (CLR 4.0.30319.42000), 64-разрядная версия RyuJIT-v4.7.2633.0 |
.NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64-битная версия RyuJIT |
00007ffd`4f8e0f20 StackOverflow.Performance.Delegates.Delegates.ExpressionDeclared()
public void ExpressionDeclared() => _expression.Declared(_message);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`4f8e0f24 488d5120 lea rdx,[rcx+20h]
00007ffd`4f8e0f28 488b02 mov rax,qword ptr [rdx]
00007ffd`4f8e0f2b 488b5108 mov rdx,qword ptr [rcx+8]
00007ffd`4f8e0f2f 488b4808 mov rcx,qword ptr [rax+8]
00007ffd`4f8e0f33 ff5018 call qword ptr [rax+18h]
00007ffd`4f8e0f36 90 nop
|
00007ffd`39c9d930 StackOverflow.Performance.Delegates.Delegates.ExpressionDeclared()
public void ExpressionDeclared() => _expression.Declared(_message);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`39c9d934 488d5120 lea rdx,[rcx+20h]
00007ffd`39c9d938 488b02 mov rax,qword ptr [rdx]
00007ffd`39c9d93b 488b5108 mov rdx,qword ptr [rcx+8]
00007ffd`39c9d93f 488b4808 mov rcx,qword ptr [rax+8]
00007ffd`39c9d943 ff5018 call qword ptr [rax+18h]
00007ffd`39c9d946 90 nop
|
Delegates.ExpressionCompiled |
.NET Framework 4.7.1 (CLR 4.0.30319.42000), 64-разрядная версия RyuJIT-v4.7.2633.0 |
.NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64-разрядная версия RyuJIT |
00007ffd`4f8f0f70 StackOverflow.Performance.Delegates.Delegates.ExpressionCompiled()
public void ExpressionCompiled() => _expression.Compiled(_message);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`4f8f0f74 488d5120 lea rdx,[rcx+20h]
00007ffd`4f8f0f78 488b4208 mov rax,qword ptr [rdx+8]
00007ffd`4f8f0f7c 488b5108 mov rdx,qword ptr [rcx+8]
00007ffd`4f8f0f80 488b4808 mov rcx,qword ptr [rax+8]
00007ffd`4f8f0f84 ff5018 call qword ptr [rax+18h]
00007ffd`4f8f0f87 90 nop
|
00007ffd`39c9d980 StackOverflow.Performance.Delegates.Delegates.ExpressionCompiled()
public void ExpressionCompiled() => _expression.Compiled(_message);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
00007ffd`39c9d984 488d5120 lea rdx,[rcx+20h]
00007ffd`39c9d988 488b4208 mov rax,qword ptr [rdx+8]
00007ffd`39c9d98c 488b5108 mov rdx,qword ptr [rcx+8]
00007ffd`39c9d990 488b4808 mov rcx,qword ptr [rax+8]
00007ffd`39c9d994 ff5018 call qword ptr [rax+18h]
00007ffd`39c9d997 90 nop
|
Казалось бы, единственная разница между объявленной и скомпилированной разборкой делегата - это rcx
для объявленной и rcx+8
для скомпилированной, используемой в их соответствующих первых mov
операциях. Я еще не очень хорошо разбираюсь в разборке, поэтому очень важно получить представление об этом. На первый взгляд может показаться, что это не вызовет различий / улучшений, и если это так, то делегат, объявленный в нативе, должен также включить его (иными словами, ошибка).
С учетом всего сказанного, очевидные вопросы для меня:
- Это известная проблема и / или ошибка?
- Я делаю что-то совершенно не по месту? (Угадай, это должен быть первый вопрос.:))
- Является ли тогда руководство использовать скомпилированные делегаты всегда, где это возможно? Как я упоминал ранее, может показаться, что магия, которая случается с скомпилированными делегатами, уже выпечена в объявленных делегатах, поэтому это немного сбивает с толку.
Для полноты вот весь код, использованный в примере здесь:
sealed class Program
{
static void Main()
{
BenchmarkRunner.Run<Delegates>();
}
}
[CoreJob, ClrJob, DisassemblyDiagnoser(true, printSource: true)]
public class Delegates
{
readonly DelegatePair<string, string> _empty;
readonly DelegatePair<string, int> _expression;
readonly string _message;
public Delegates() : this(new DelegatePair<string, string>(_ => default, _ => default),
new DelegatePair<string, int>(x => x.Length, x => x.Length)) {}
public Delegates(DelegatePair<string, string> empty, DelegatePair<string, int> expression,
string message = "Hello World!")
{
_empty = empty;
_expression = expression;
_message = message;
EmptyDeclared();
EmptyCompiled();
ExpressionDeclared();
ExpressionCompiled();
}
[Benchmark]
public void EmptyDeclared() => _empty.Declared(default);
[Benchmark]
public void EmptyCompiled() => _empty.Compiled(default);
[Benchmark]
public void ExpressionDeclared() => _expression.Declared(_message);
[Benchmark]
public void ExpressionCompiled() => _expression.Compiled(_message);
}
public struct DelegatePair<TFrom, TTo>
{
DelegatePair(Func<TFrom, TTo> declared, Func<TFrom, TTo> compiled)
{
Declared = declared;
Compiled = compiled;
}
public DelegatePair(Func<TFrom, TTo> declared, Expression<Func<TFrom, TTo>> expression) :
this(declared, expression.Compile()) {}
public Func<TFrom, TTo> Declared { get; }
public Func<TFrom, TTo> Compiled { get; }
}
Заранее благодарим за любую помощь, которую вы можете оказать!