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):
Listagemusing 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):
Teste | Tempo (intervalo de tempo) | Tempo (ticks) | % tempo |
---|
Nativo | 00: 00: 01.5760651 | 15760651 | 101,935% |
Dlgt nativo | 00: 00: 01.5461478 | 15461478 | 100% |
Expressões | 00: 00: 01.5835454 | 15835454 | 102,4188% |
A compilação no Release, com otimização, inicia sem depurador (Ctrl + F5):
Teste | Tempo (intervalo de tempo) | Tempo (ticks) | % tempo |
---|
Nativo | 00: 00: 01.3182291
| 13182291
| 100% |
Dlgt nativo | 00: 00: 01.3300925
| 13300925
| 100,8999% |
Expressões | 00: 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)
}
}