Вот в этой статье в комментариях произошёл не то, чтобы спор, но некоторое «не схождение» в сравнении скорости 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) } }