Pull to refresh

Скорость работы скомпилированного Linq Expression Tree

Reading time5 min
Views5.5K
Вот в этой статье в комментариях произошёл не то, чтобы спор, но некоторое «не схождение» в сравнении скорости IL Emit и скомпилированного Linq Expression Tree.

Данная мини статья — код теста скорости + результаты прогона этого теста.

Для теста был выбран код, который пересекается с содержимым изначальной статьи — сериализация в поток длинны строки, а затем — всех байтов строки в кодировке 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)
    }
}

Tags:
Hubs:
+5
Comments8

Articles