리플렉션 대신 생성된 메서드 사용 | RicardoPeres

리플렉션을 사용하지 않고 컴파일된 람다를 동적으로 생성해 특정 속성 값을 설정하는 방법과 성능비교를 하는 게시물입니다.


2개의 좋아요

좋은 내용 감사합니다.
그런데 코드가 잘 눈에 들어오지 않아 간단히 정리해봤습니다.

  • 코드의 내용은 같지만 일부 코드를 보기 쉽도록 형식을 바꿨으니 참고 부탁드립니다.

이 글은 아래 예제를 기준으로 3가지 방법과 그 성능에 대해 안내하고 있습니다
(혹시 몰라 결론은 가져오지 않았으니 성능비교 결과는 원문에서 확인 부탁드립니다).

  • 리플렉션만 이용한 방법
  • 리플렉션과 캐싱을 함께 이용한 방법
  • 컴파일된 람다식을 이용한 방법
public abstract class Setter
{
    public abstract void Set(object obj, string propertyName, object value);
}

리플렉션만 이용한 방법

public sealed class ReflectionSetter : Setter
{
    public override void Set(object obj, string propertyName, object value)
    {
        ArgumentNullException.ThrowIfNull(obj);
        ArgumentNullException.ThrowIfNull(propertyName);

        var property = obj.GetType().GetProperty(
            propertyName,
            BindingFlags.Instance
            | BindingFlags.Public
            | BindingFlags.GetProperty
            | BindingFlags.SetProperty);

        if (property != null)
        {
            property.SetValue(obj, value, null);
        }
        else
        {
            throw new InvalidOperationException("Property not found.");
        }
    }
}

리플렉션과 캐싱을 함께 이용한 방법

public sealed class CachedReflectionSetter : Setter
{
    private readonly Dictionary<Type, Dictionary<string, PropertyInfo>> _properties = new();

    public void Initialize(Type type)
    {
        ArgumentNullException.ThrowIfNull(type);

        _properties[type] = new Dictionary<string, PropertyInfo>();

        var properties= type.GetProperties(
            BindingFlags.Instance
            | BindingFlags.Public
            | BindingFlags.GetProperty
            | BindingFlags.SetProperty);

        foreach (var prop in properties)
        {
            _properties[type][prop.Name] = prop;
        }
    }

    public override void Set(object obj, string propertyName, object value)
    {
        ArgumentNullException.ThrowIfNull(obj);
        ArgumentNullException.ThrowIfNull(propertyName);

        var property = GetPropertyFor(obj.GetType(), propertyName);

        if (property != null)
        {
            property.SetValue(obj, value, null);
        }
        else
        {
            throw new InvalidOperationException("Property not found.");
        }
    }

    private PropertyInfo? GetPropertyFor(Type type, string propertyName)
    {
        if (_properties.TryGetValue(type, out var properties))
        {
            if (properties.TryGetValue(propertyName, out var prop))
            {
                return prop;
            }
        }

        return null;
    }
}

컴파일된 람다식을 이용한 방법

public sealed class CompiledSetter : Setter
{
    private readonly Dictionary<Type, Dictionary<string, Delegate>> _properties = new();

    public void Initialize(Type type)
    {
        ArgumentNullException.ThrowIfNull(type);

        _properties[type] = new Dictionary<string, Delegate>();

        var properties = type.GetProperties(
            BindingFlags.Instance
            | BindingFlags.Public
            | BindingFlags.GetProperty
            | BindingFlags.SetProperty);

        foreach (var prop in properties)
        {
           GenerateSetterFor(type, prop);
        }
    }

    public override void Set(object obj, string propertyName, object value)
    {
        ArgumentNullException.ThrowIfNull(obj);
        ArgumentNullException.ThrowIfNull(propertyName);

        var action = GetActionFor(obj.GetType(), propertyName);

        if (action is Action<object, object> act)
        {
            act(obj, value);
        }
        else
        {
            throw new InvalidOperationException("Property not found.");
        }
    }

    private void GenerateSetterFor(Type type, PropertyInfo property)
    {
        var propertyName = property.Name;
        var propertyType = property.PropertyType;
        var parmExpression = Expression.Parameter(typeof(object), "it");
        var castExpression = Expression.Convert(parmExpression, type);
        var propertyExpression = Expression.Property(castExpression, propertyName);
        var valueExpression = Expression.Parameter(typeof(object), propertyName);
        var operationExpression = Expression.Assign(propertyExpression, Expression.Convert(valueExpression, propertyType));
        var lambdaExpression = Expression.Lambda(typeof(Action<,>).MakeGenericType(typeof(object), typeof(object)), operationExpression, parmExpression, valueExpression);
        var action = lambdaExpression.Compile();

        _properties[type][propertyName] = action;
    }

    private Delegate? GetActionFor(Type type, string propertyName)
    {
        if (_properties.TryGetValue(type, out var properties))
        {
            if (properties.TryGetValue(propertyName, out var action))
            {
                return action;
            }
        }

        return null;
    }
}
2개의 좋아요

그런데 댓글도 꽤 재밌네요,

어떤 분은 자기가 여러가지 샘플을 준비했다고 링크를 달아주셨고,

또 어떤 분은 이 방법보다 FastMember가 더 빠르다고 하네요

1개의 좋아요

제 컴퓨터에서는 성능 측정이 이렇게 나왔습니다.

캐시된 리플렉션 속도가 의외인데요, 반대로 이야기 하자면 .NET 6의 리플릭션 속도가 많이 개선되었다고 봐도 될 듯 하군요.

image

1개의 좋아요

흠 신기하네요. 제네릭 버젼으로 하면 좀 더 빨라질까 해서 테스트를 해봤는데 되려 조금 더 느려집니다.

public override void Set<T, TValue>(T obj, string propertyName, TValue value)
private void GenerateSetterFor(PropertyInfo property, Type type)
{
        var propertyName = property.Name;
        var propertyType = property.PropertyType;
        var parmExpression = Expression.Parameter(type, "it");
        var propertyExpression = Expression.Property(parmExpression, propertyName);
        var valueExpression = Expression.Parameter(propertyType, propertyName);
        var operationExpression = Expression.Assign(propertyExpression, valueExpression);
        var lambdaExpression = Expression.Lambda(typeof(Action<,>).MakeGenericType(type, propertyType), operationExpression, parmExpression, valueExpression);

        var action = lambdaExpression.Compile();

        this._properties[type][propertyName] = action;
}

대략 4us 정도 증가합니다. TestCompiled뿐만 아니라 TestReflection 및 TestCachedReflection 모두 다같이 증가하는것으로 보아 제네릭 Set 구조로 인한것 같네요.