Velocidade da árvore de expressão Linq compilada

Aqui neste artigo, nos comentários, não houve uma disputa, mas alguma "não convergência" na comparação da velocidade do IL Emit e da Linq Expression Tree compilada.

Este mini-artigo é o código do teste de velocidade + os resultados da execução deste teste.

Para o teste, foi selecionado um código que cruza com o conteúdo do artigo original - serialização em um fluxo de comprimento de uma sequência e, em seguida, de todos os bytes de uma sequência codificada em UTF-16 (Encoding.Unicode).

O código de serialização em si pode não ser o mais ideal, mas próximo disso se você não usar construções inseguras.

O código nas duas implementações é o mesmo, como você pode ver examinando a construção da expressão Lambda.

Eu não me incomodei com a geração de IL por meio do Emit - o código que deveria ser “ótimo”. Acabei de escrever em C # no método estático (na verdade, todos os métodos do programa de teste são estáticos, porque esse é um aplicativo de console muito simples) - isso o método também é chamado de nativo .

O segundo método de comparação é a expressão gerada pelo Lambda, compilada chamando o método Compile (talvez seja possível obter um ganho de velocidade usando o CompileToMethod, mas isso não é preciso) - esse método também é chamado de Expresisons .

BÔNUS! Por alguma razão, um teste adicional foi adicionado - uma chamada indireta ao método usado no teste nativo - atribuindo o método a um campo delegado estático - esse método é chamado de Dlgt nativo .

No início do aplicativo, a saída dos dois métodos é emitida, para que você possa garantir que o código gere exatamente os mesmos dados.

Então, aqui está o código do aplicativo (de uma só vez):

Listagem
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; } } } 


E aqui estão os resultados do teste na seguinte configuração:

CPU
	 CPU Intel (R) Core (TM) i7-3770 a 3.40GHz

	 Velocidade base: 3,90 GHz
	 Soquetes: 1
	 Cores: 4
	 Processadores lógicos: 8
	 Virtualização: ativada
	 Cache L1: 256 KB
	 Cache L2: 1,0 MB
	 Cache L3: 8,0 MB

	 Utilização 8%
	 Velocidade 4,05 GHz
	 Tempo de atividade 5: 00: 43: 01
	 Processos 239
	 Tópicos 4092
	 Alças 168774


Memória
	 32,0 GB DDR3

	 Velocidade: 1600 MHz
	 Slots usados: 4 de 4
	 Fator de forma: DIMM
	 Hardware reservado: 42,5 MB

	 Disponível 20,7 GB
	 Em cache de 20,1 GB
	 Confirmado 13.4 / 36.7 GB
	 Pool paginado 855 MB
	 Pool não paginado 442 MB
	 Em uso (compactado) 11,2 GB (48,6 MB)

Target .Net Framework 4.7
Sistema operacional Windows 10 Pro x64 1803 build 17134.48

Então, os resultados prometidos:

Compilação em Debug, sem otimização, iniciando sem um depurador (Ctrl + F5):
TesteTempo (intervalo de tempo)Tempo (ticks)% tempo
Nativo00: 00: 01.576065115760651101,935%
Dlgt nativo00: 00: 01.546147815461478100%
Expressões00: 00: 01.583545415835454102,4188%

A compilação no Release, com otimização, inicia sem depurador (Ctrl + F5):
TesteTempo (intervalo de tempo)Tempo (ticks)% tempo
Nativo00: 00: 01.3182291
13182291
100%
Dlgt nativo00: 00: 01.3300925
13300925
100,8999%
Expressões00: 00: 01.4871786
14871786
112,8164%

Alguns resultados podem ser resumidos:

  • A Árvore de Expressão Compilada é executada 1-2% mais lenta no Debug e 10-12 no Release, o que é muito bom, considerando que gerar código através da Árvore de Expressão no tempo de execução é muito mais fácil.
  • No modo de depuração, por algum motivo, uma chamada de método indireta por meio de um delegado é mais rápida que uma chamada direta.

Número de bônus 2 sob o spoiler - Representação de depuração de expressões Lambda antes da compilação:

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 = .Chamada System.BitConverter.GetBytes (($ inst.StringProp) .Length);
         $ strBuff = .Call (System.Text.Encoding.Unicode) .GetBytes ($ inst.StringProp);
         .Call $ stream.Write (
             $ intBuff,
             0 0
             $ intBuff.Length);
         .Call $ stream.Write (
             $ strBuff,
             0 0
             $ strBuff.Length)
     }
 }

Source: https://habr.com/ru/post/pt413561/


All Articles