Вот в этой статье в комментариях произошёл не то, чтобы спор, но некоторое «не схождение» в сравнении скорости IL Emit и скомпилированного Linq Expression Tree.
Данная мини статья — код теста скорости + результаты прогона этого теста.
Для теста был выбран код, который пересекается с содержимым изначальной статьи — сериализация в поток длинны строки, а затем — всех байтов строки в кодировке UTF-16 (Encoding.Unicode).
Сам код сериализации, возможно, не самый оптимальный, но близок к тому, если не пользоваться unsafe конструкциями.
Код в обеих реализациях получается одинаковый, в чём можно убедиться, разобрав построение Lambda выражения.
Я не стал заморачиваться с генерацией IL через Emit — код, который должен быть «оптимальным» я просто написал на C# в статическом методе (на самом деле, все методы тестовой программы — статические, т.к. это очень простое консольное приложение) — этот метод далее назван Native.
Второй метод для сравнения — сгенерированное Lambda выражение, скомилированное вызовом метода Compile (возможно, прирост скорости может дать использование CompileToMethod, но это — не точно) — этот метод далее назван Expresisons.
БОНУС! По некоторому раздумью, был добавлен дополнительный тест — косвенный вызов метода, использованного в тесте Native — через присвоение метода в статическое поле-делегат — этот метод назван Native Dlgt.
В самом начале приложения происходит вывод результата работы обоих методов, чтобы можно было убедиться, что код генерирует абсолютно одинаковые данные.
Итак, вот код приложения (весь сразу):
А вот результаты теста на следующей конфигурации:
Target .Net Framework 4.7
OS Windows 10 Pro x64 1803 build 17134.48
Итак, обещанные результаты:
Компиляция в Debug, без оптимизации, запуск без отладчика (Ctrl+F5):
Компиляция в Release, с опимизацией, запуск без отладчика (Ctrl+F5):
Можно подвести некоторые итоги:
Бонус №2 под спойлером — Debug представление Lambda выражения до компиляции:
Данная мини статья — код теста скорости + результаты прогона этого теста.
Для теста был выбран код, который пересекается с содержимым изначальной статьи — сериализация в поток длинны строки, а затем — всех байтов строки в кодировке UTF-16 (Encoding.Unicode).
Сам код сериализации, возможно, не самый оптимальный, но близок к тому, если не пользоваться unsafe конструкциями.
Код в обеих реализациях получается одинаковый, в чём можно убедиться, разобрав построение Lambda выражения.
Я не стал заморачиваться с генерацией IL через Emit — код, который должен быть «оптимальным» я просто написал на C# в статическом методе (на самом деле, все методы тестовой программы — статические, т.к. это очень простое консольное приложение) — этот метод далее назван Native.
Второй метод для сравнения — сгенерированное Lambda выражение, скомилированное вызовом метода Compile (возможно, прирост скорости может дать использование CompileToMethod, но это — не точно) — этот метод далее назван Expresisons.
БОНУС! По некоторому раздумью, был добавлен дополнительный тест — косвенный вызов метода, использованного в тесте Native — через присвоение метода в статическое поле-делегат — этот метод назван Native Dlgt.
В самом начале приложения происходит вывод результата работы обоих методов, чтобы можно было убедиться, что код генерирует абсолютно одинаковые данные.
Итак, вот код приложения (весь сразу):
Листинг
using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; namespace ExpressionSpeedTest { class Program { static void Main(string[] args) { InitExpression(); InitDelegateNative(); var inst = new TestClass { StringProp = "abcdefabcdef" }; byte[] buff1, buff2; using (var ms1 = new MemoryStream()) { SaveString(ms1, inst); buff1 = ms1.ToArray(); } using (var ms2 = new MemoryStream()) { DynamicMethod(ms2, inst); buff2 = ms2.ToArray(); } Console.WriteLine($"Native string: {string.Join("", buff1.Select(b => Encoding.Default.GetString(new[] { b })))}"); Console.WriteLine($"Expressions string: {string.Join("", buff2.Select(b => Encoding.Default.GetString(new[] { b })))}"); GC.Collect(GC.MaxGeneration); GC.WaitForPendingFinalizers(); GC.Collect(GC.MaxGeneration); TestNative(); GC.Collect(GC.MaxGeneration); GC.WaitForPendingFinalizers(); GC.Collect(GC.MaxGeneration); TestDelegateNative(); GC.Collect(GC.MaxGeneration); GC.WaitForPendingFinalizers(); GC.Collect(GC.MaxGeneration); TestExpressions(); GC.Collect(GC.MaxGeneration); GC.WaitForPendingFinalizers(); GC.Collect(GC.MaxGeneration); Console.ReadLine(); } private static void TestDelegateNative() { var inst = new TestClass { StringProp = "abcdefabcdef" }; using (var ms = new MemoryStream()) { var sw = new Stopwatch(); sw.Start(); for (var idx = 0; idx < loopLength; idx++) { SaveString(ms, inst); } sw.Stop(); Console.WriteLine($"Native Dlgt test: {sw.Elapsed}, {sw.ElapsedTicks} ticks"); } } private static void InitDelegateNative() { NativeDelegate = SaveString; } private static void InitExpression() { var intGetBytes = typeof(BitConverter).GetMethods(BindingFlags.Static | BindingFlags.Public) .Single(x => x.Name == nameof(BitConverter.GetBytes) && x.GetParameters()[0].ParameterType == typeof(int)); var stringGetBytes = typeof(Encoding).GetMethods(BindingFlags.Instance | BindingFlags.Public) .Single(x => x.Name == nameof(Encoding.GetBytes) && x.GetParameters().Length == 1 && x.GetParameters()[0].ParameterType == typeof(string)); var unicodeProp = typeof(Encoding).GetProperty(nameof(Encoding.Unicode), BindingFlags.Static | BindingFlags.Public); var streamWrite = typeof(Stream).GetMethod(nameof(Stream.Write)); var streamPar = Expression.Parameter(typeof(Stream), "stream"); var instPar = Expression.Parameter(typeof(TestClass), "inst"); var intBuffVar = Expression.Variable(typeof(byte[]), "intBuff"); var strBuffVar = Expression.Variable(typeof(byte[]), "strBuff"); var expressionBody = Expression.Block( new[] { intBuffVar, strBuffVar }, Expression.Assign(intBuffVar, Expression.Call(null, intGetBytes, Expression.Property( Expression.Property( instPar, nameof(TestClass.StringProp)), nameof(string.Length)))), Expression.Assign(strBuffVar, Expression.Call(Expression.Property(null, unicodeProp), stringGetBytes, Expression.Property( instPar, nameof(TestClass.StringProp) ))), Expression.Call(streamPar, streamWrite, intBuffVar, Expression.Constant(0), Expression.Property(intBuffVar, nameof(Array.Length))), Expression.Call(streamPar, streamWrite, strBuffVar, Expression.Constant(0), Expression.Property(strBuffVar, nameof(Array.Length))) ); DynamicMethod = Expression.Lambda<Action<Stream, TestClass>>(expressionBody, streamPar, instPar).Compile(); } private const int loopLength = 10000000; private static Action<Stream, TestClass> DynamicMethod; private static Action<Stream, TestClass> NativeDelegate; private static void TestExpressions() { var inst = new TestClass { StringProp = "abcdefabcdef" }; using (var ms = new MemoryStream()) { var sw = new Stopwatch(); sw.Start(); for (var idx = 0; idx < loopLength; idx++) { DynamicMethod(ms, inst); } sw.Stop(); Console.WriteLine($"Expressions test: {sw.Elapsed}, {sw.ElapsedTicks} ticks"); } } private static void TestNative() { var inst = new TestClass { StringProp = "abcdefabcdef" }; using (var ms = new MemoryStream()) { var sw = new Stopwatch(); sw.Start(); for (var idx = 0; idx < loopLength; idx++) { SaveString(ms, inst); } sw.Stop(); Console.WriteLine($"Native test: {sw.Elapsed}, {sw.ElapsedTicks} ticks"); } } public static void SaveString(Stream stream, TestClass instance) { var intBuff = BitConverter.GetBytes(instance.StringProp.Length); var strBuff = Encoding.Unicode.GetBytes(instance.StringProp); stream.Write(intBuff, 0, intBuff.Length); stream.Write(strBuff, 0, strBuff.Length); } } class TestClass { public string StringProp { get; set; } } }
А вот результаты теста на следующей конфигурации:
CPU
Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz Base speed: 3,90 GHz Sockets: 1 Cores: 4 Logical processors: 8 Virtualization: Enabled L1 cache: 256 KB L2 cache: 1,0 MB L3 cache: 8,0 MB Utilization 8% Speed 4,05 GHz Up time 5:00:43:01 Processes 239 Threads 4092 Handles 168774
Memory
32,0 GB DDR3 Speed: 1600 MHz Slots used: 4 of 4 Form factor: DIMM Hardware reserved: 42,5 MB Available 20,7 GB Cached 20,1 GB Committed 13,4/36,7 GB Paged pool 855 MB Non-paged pool 442 MB In use (Compressed) 11,2 GB (48,6 MB)
Target .Net Framework 4.7
OS Windows 10 Pro x64 1803 build 17134.48
Итак, обещанные результаты:
Компиляция в Debug, без оптимизации, запуск без отладчика (Ctrl+F5):
| Test | Time (timespan) | Time (ticks) | %time |
|---|---|---|---|
| Native | 00:00:01.5760651 | 15760651 | 101.935% |
| Native Dlgt | 00:00:01.5461478 | 15461478 | 100% |
| Expressions | 00:00:01.5835454 | 15835454 | 102.4188% |
Компиляция в Release, с опимизацией, запуск без отладчика (Ctrl+F5):
| Test | Time (timespan) | Time (ticks) | %time |
|---|---|---|---|
| Native | 00:00:01.3182291 |
13182291 |
100% |
| Native Dlgt | 00:00:01.3300925 |
13300925 |
100.8999% |
| Expressions | 00:00:01.4871786 |
14871786 |
112.8164% |
Можно подвести некоторые итоги:
- Скомпилированные Expression Tree работают на 1-2 % медленнее в Debug и на 10-12 в Release, что очень даже хорошо, с учётом, что генерировать код через Expression Tree в runtime в разы проще.
- В Debug режиме, почему-то, косвенный вызов метода через делегат работает быстрее, чем прямой вызов.
Бонус №2 под спойлером — Debug представление Lambda выражения до компиляции:
Lambda
.Lambda #Lambda1<System.Action`2[System.IO.Stream,ExpressionSpeedTest.TestClass]>(
System.IO.Stream $stream,
ExpressionSpeedTest.TestClass $inst) {
.Block(
System.Byte[] $intBuff,
System.Byte[] $strBuff) {
$intBuff = .Call System.BitConverter.GetBytes(($inst.StringProp).Length);
$strBuff = .Call (System.Text.Encoding.Unicode).GetBytes($inst.StringProp);
.Call $stream.Write(
$intBuff,
0,
$intBuff.Length);
.Call $stream.Write(
$strBuff,
0,
$strBuff.Length)
}
}
