Como ya hablamos en artículos anteriores, una de las nuevas características más importantes de EFCore 6 es la mejora de rendimiento en varios aspectos.
En la .NET Conf 2021, Microsoft presento estas mejoras de EFCore 6, y aunque no hicieron hincapié en el trasfondo para conseguir las mismas, si nos dieron alguna información cuantitativa de esta mejora mejor, por ejemplo:
- EFCore 6 es un 70% mas rápido ejecutando consultas que EFCore 5 según el benchmark de
TechEmpower Fortunes. Hay que tener en cuenta que este resultado hace referencia al stack completo de net6 (.NET6 + ASP.NET Core 6 + EFCore 6).
- EFCore 6 por si solo es un 31% más rápido.
En ciertas ocasiones y aspectos, Dapper es solo un 2% mas rápido que EFCore 6, por tanto se podría considerar dejar de usar Dapper en determinados casos.
Comprobemos la realidad
En este artículo, comprobaremos la realidad de las afirmaciones que hizo Microsoft, veremos cuál es la diferencia entre EFCore 5 y EFCore 6 en distintos casos. Para ello, en un mismo proyecto, nos apoyaremos de la librería de Benchmark -> BenchmarkDotNet.
Realizaremos distintas operaciones sobre la siguiente entidad simple:
[scss]public class SimpleEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
[/scss]
Vamos a ver el comportamiento frente a distintas operaciones del stack casi al completo, realizaremos pruebas con net6 + EFCore 6, frente a net5 + EFCore 5. Para comprobar los tiempos de respuesta de los siguientes métodos:
- Create()
- Read()
- ReadById()
- ReadByName()
- ReadAll()
- Update()
- Delete()
Cabe destacar que estamos utilizando dos contextos distintos, y hacemos uso del DbContext pooling que se introdujo ya en EFCore 5, ya que de otra forma, las diferencias entre 5 y 6 son prácticamente inapreciables. En la propia documentación de Microsoft, podemos encontrar las siguientes diferencias con el uso o no del ContextPooling:
Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Gen Allocated |
WithoutContextPooling | 701.6 μs | 26.62 μs | 78.48 μs | 11.7188 | – | – | 50.38 KB |
WithContextPooling 350.1 μs 6.80 μs 14.64 μs 0.9766 – – 4.63 KB
En nuestro caso, realizando una prueba con 10.000 iteraciones y dos hilos separados, uno para net5 y otro para net6, tenemos los siguientes resultados:
Podemos observar que, en las operaciones de escritura de datos, apenas notamos una diferencia, incluso se aprecia en alguna que net6 es más lento, ya que hay que tener en cuenta que no nos encontramos en el contexto más idóneo para realizar este tipo de pruebas. Deberíamos haber aislado nuestro entorno, para que nada pudiera afectar a las medidas realizadas. No obstante, como era de esperar la gran diferencia se observa en las operaciones de lectura, siendo mayor en estas en las que no hacemos usos de índices.
Cuantitativamente, tenemos los siguientes datos:
Method | Job | Runtime | Mean | Error | StdDev | Median | Allocated | ||||
Create | net5.0 | .NET 5.0 | 1,207.8 μs | 5.35 μs | 159.98 μs | 1,185.7 μs | 23 KB | ||||
Read | net5.0 | .NET 5.0 | 451.7 μs | 2.45 μs | 73.33 μs | 445.1 μs | 8 KB | ||||
ReadById | net5.0 | .NET 5.0 | 456.9 μs | 3.67 μs | 110.68 μs | 439.1 μs | 12 KB | ||||
Method | Job | Runtime | Mean | Error | StdDev | Median | Allocated | ||||
ReadByName | net5.0 | .NET 5.0 | 2,692.4 μs | 34.92 μs | 1,058.74 μs | 2,622.4 μs | 18 KB | ||||
ReadAll | net5.0 | .NET 5.0 | 14,709.0 μs | 62.35 μs | 1,880.91 μs | 14,672.5 μs | 5,026 KB | ||||
Update | net5.0 | .NET 5.0 | 361.7 μs | 2.90 μs | 87.00 μs | 345.0 μs | 8 KB | ||||
Delete | net5.0 | .NET 5.0 | 1,466.2 μs | 8.69 μs | 261.61 μs | 1,422.1 μs | 25 KB | ||||
Create | net6.0 | .NET 6.0 | 1,028.3 μs | 5.52 μs | 164.73 μs | 978.2 μs | 22 KB | ||||
Read | net6.0 | .NET 6.0 | 364.9 μs | 3.02 μs | 90.77 μs | 343.1 μs | 7 KB | ||||
ReadById | net6.0 | .NET 6.0 | 463.8 μs | 2.61 μs | 78.06 μs | 451.0 μs | 13 KB | ||||
ReadByName | net6.0 | .NET 6.0 | 824.4 μs | 3.52 μs | 104.64 μs | 802.9 μs | 18 KB | ||||
ReadAll | net6.0 | .NET 6.0 | 9,833.0 μs | 20.92 μs | 613.27 μs | 9,794.8 μs | 2,835 KB | ||||
Update | net6.0 | .NET 6.0 | 418.6 μs | 2.60 μs | 77.60 μs | 407.8 μs | 7 KB | ||||
Delete net6.0 .NET 6.0 1,569.7 μs 7.30 μs 219.26 μs 1,546.1 μs 24 KB
Podemos ver más en detalle cada una de las operaciones, para comprender a que se refiere cada una y cuál es la diferencia.
Read
En este método, simplemente cogemos el set de datos, y recuperamos el primer resultado a través de LINQ.
[scss]public void Read()
{
using var context = _poolingFactory.CreateDbContext();
var entity = context.SimpleEntities.FirstOrDefault();
}
[/scss]
Y según los datos anteriores, tenemos una diferencia del 19,2% con el stack de net6, respecto al stack de net5.
ReadById
En este método, recuperamos la entidad por Id, el cual lo recogemos del contador para cada una de las iteraciones.
[scss]public void ReadById()
{
using var context = _poolingFactory.CreateDbContext();
_runCountReadId++;
var entity = context.SimpleEntities.Where(e => e.Id ==
_runCountReadId5).FirstOrDefault();
}
[/scss]
Según los datos que hemos visto anteriormente, tenemos una diferencia del 1,5% con el stack de net6,
respecto al stack de net5.
ReadByName
En este método, recuperamos la entidad por Name, este campo no tiene índice.
[scss]public void ReadByName()
{
using var context = _poolingFactory.CreateDbContext();
_runCountReadName++;
var entity = context.SimpleEntities.Where(e => e.Name == $»Name-
{_runCountReadName}»).FirstOrDefault();
}
[/scss]
En este caso ya apreciamos sustanciales diferencias, ya que nos reporta que el stack de net6 es un 69,4% mas rápido que net5.
ReadAll
Aquí vamos a recuperar el listado entero de entidades.
[scss]public void ReadAll()
{
using var context = _poolingFactory.CreateDbContext();
var list = context.SimpleEntities.ToList();
}
[/scss]
Por último, seguimos comprobando que las diferencias son notables, ya que net6 nos reporta una mejoría del 33,1% con respecto a net5.
En conclusión, podemos afirmar que EFCore6 es más rápido que EFCore5. Hemos comprobado que las diferencias anunciadas por Microsoft están en los ordenes de magnitud que hemos observado, aunque nuestras pruebas no hayan sido en un entorno de lo más propicio para la realización de las mismas. Cabe destacar que con las condiciones idóneas se llegará a las diferencias anunciadas por Microsoft sin problema.
Nos quedaría comprobar las diferencias que existen con Dapper, lo haremos en artículos venideros.
Si quieres ver o descargar el código correspondiente a este artículo, está disponible en el siguiente enlace benchmark-EFCore.
Siente libre de hacer cualquier aportación al mismo.