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.