Foto de Pixabay
La reflexión en .NET es muy importante conocerla, permite inspeccionar y manipular tipos, métodos, propiedades y otros miembros de objetos en tiempo de ejecución. Sin embargo, esta flexibilidad y dinamismo vienen con una serie de aspectos que debes tener en cuenta:
Acceso Dinámico
La reflexión implica el uso de metadatos para acceder a la información de tipos y miembros en tiempo de ejecución, lo que es inherentemente más lento que el acceso estático y directo proporcionado por el compilador en tiempo de compilación, ejemplo que os he puesto antes.
using System.Reflection;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
public class TargetClass
{
public void Compute()
{
Console.WriteLine("Compute method is called");
}
}
[SimpleJob(RuntimeMoniker.Net80, baseline: true)]
[SimpleJob(RuntimeMoniker.Net90)]
public class ComplexBenchmark
{
private readonly Type _type;
private readonly object _instance;
private readonly MethodInfo _computeMethod;
public ComplexBenchmark()
{
_type = typeof(TargetClass);
_instance = Activator.CreateInstance(_type)!;
_computeMethod = _type.GetMethod("Compute")!;
}
[Benchmark]
public void InvokeMethodWithReflection()
{
for (int i = 0; i < 10000; i++)
{
_computeMethod.Invoke(_instance, new object[] { });
}
}
[Benchmark]
public void InvokeMethodWithoutReflection()
{
var instance = new TargetClass();
for (int i = 0; i < 10000; i++)
{
instance.Compute();
}
}
}
Resultado:
Method |
Runtime |
Mean |
InvokeMethodWithReflection |
.NET 8.0 |
3.216 s |
InvokeMethodWithReflection |
.NET 9.0 |
3.144 s |
InvokeMethodWithoutReflection |
.NET 8.0 |
3.181 s |
InvokeMethodWithoutReflection |
.NET 9.0 |
3.184 s |
En Vista de los Resultados, ¿Debería Preocuparme?
La diferencia en rendimiento entre el uso de reflexión y el acceso directo es mínima en este caso (y como podeis observar en .NET9 mejora); sin embargo, en otros escenarios podría ser más significativa. La clave para tomar una decisión informada es realizar pruebas de rendimiento específicas que permitan ser objetivos.
Por ejemplo, si estás desarrollando un sistema que requiere cientos de miles de llamadas por segundo y maneja un alto número de usuarios concurrentes, es esencial evaluar el impacto del uso de reflexión. Sin embargo, para aplicaciones con una carga menor, como 100 usuarios concurrentes y un 1000 llamadas por segundo a tu API, el impacto podría ser menos significativo.
Optimización Limitada
El uso de reflexión limita las optimizaciones que el compilador y el tiempo de ejecución pueden aplicar, como la inlining de métodos o la eliminación de código muerto, ya que las llamadas dinámicas no pueden ser analizadas en profundidad por el compilador.
Si estas haciendo Native AOT deployment, pues te estas complicando la vida, ya que AOT y reflexión tienen características que pueden entrar en conflicto.
Costos de Búsqueda
Al utilizar reflexión, el sistema debe realizar búsquedas en los metadatos para encontrar la información solicitada, lo que añade sobrecarga en términos de tiempo de ejecución.
using System;
using System.Reflection;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Jobs;
public class PerformanceTargetClass
{
public int Compute(int a, int b)
{
return a * b + a - b;
}
}
[SimpleJob(RuntimeMoniker.Net80, baseline: true)]
[SimpleJob(RuntimeMoniker.Net90)]
public class ReflectionPerformanceBenchmark
{
private readonly Type _type;
private readonly object _instance;
private readonly MethodInfo _computeMethod;
public ReflectionPerformanceBenchmark()
{
_type = typeof(PerformanceTargetClass);
_instance = Activator.CreateInstance(_type)!;
_computeMethod = _type.GetMethod("Compute")!;
}
[Benchmark]
public void InvokeMethodWithReflection()
{
for (int i = 0; i < 10000; i++)
{
_computeMethod.Invoke(_instance, new object[] { i, i + 1 });
}
}
[Benchmark]
public void InvokeMethodWithoutReflection()
{
var instance = new PerformanceTargetClass();
for (int i = 0; i < 10000; i++)
{
instance.Compute(i, i + 1);
}
}
}
class Program
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<ReflectionPerformanceBenchmark>();
}
}
Method |
Runtime |
Mean |
InvokeMethodWithReflection |
.NET 8.0 |
416.036 μs |
InvokeMethodWithoutReflection |
.NET 8.0 |
2.984 μs |
InvokeMethodWithReflection |
.NET 9.0 |
414.669 μs |
InvokeMethodWithoutReflection |
.NET 9.0 |
2.991 μs |
¿Y ahora?
A medida que las cosas se complican, más penalización tienes.
Lógicamente nadie hace las instanciaciones con reflexión, entiende que esto es para demostrar la penalización.
Seguridad y Comprobaciones
La reflexión requiere comprobaciones adicionales de seguridad y acceso en tiempo de ejecución, lo que también contribuye a una mayor carga de procesamiento. Por ejemplo, la inyección de dependencias (DI) puede dar muchas sorpresas cuando en un escenario donde tienes un contenedor de inyección de dependencias que carga automáticamente todas las implementaciones de una interfaz desde un ensamblado podría cargar código no deseado o inseguro.
using System;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
namespace DependencyInjectionReflectionExample
{
public interface IService
{
void Execute();
}
public class ServiceA : IService
{
public void Execute()
{
Console.WriteLine("Executing ServiceA");
}
}
// Supongamos que alguien introduce un ensamblado malicioso con esta implementación
public class MaliciousService : IService
{
public void Execute()
{
Console.WriteLine("Executing MaliciousService - Doing something harmful!");
}
}
class Program
{
static void Main(string[] args)
{
var serviceCollection = new ServiceCollection();
// Cargar todos los tipos que implementan IService desde un ensamblado
var assembly = Assembly.GetExecutingAssembly();
var serviceTypes = assembly.GetTypes()
.Where(t => typeof(IService).IsAssignableFrom(t) && t.IsClass);
foreach (var type in serviceTypes)
{
serviceCollection.AddTransient(typeof(IService), type);
}
var serviceProvider = serviceCollection.BuildServiceProvider();
// Ejecutar todos los servicios
var services = serviceProvider.GetServices<IService>();
foreach (var service in services)
{
service.Execute();
}
}
}
}
Conclusión
Es importante ser cauteloso y consciente de las posibles implicaciones de seguridad y rendimiento al utilizar reflexión.
Este artículo es una nota mental que voy a usar para compartirla cuando se inicia una discusión o realizo una revisión de una arquitectura y así evitar discusiones recurrentes.