2
\$\begingroup\$

I have some part of my code that uses reflection and there is some slowdown issue that I have to fix.

After some research I found that expression tree seem to be a good candidate to fix it also it MUST work under .NET 3.5.

I have less than a week of experience with expression tree and the code below was found on some blogs/articles/questions/answers and I modified it a little.

This is the code that I want to be reviewed:

Public Module PropertyInfoExtensions
    <Extension>
    Public Function GetValueGetter(propertyInfo As PropertyInfo) As Func(Of Object, Object)
        Dim instance As ParameterExpression = Expression.Parameter(GetType(Object), "instance")

        Dim instanceCast As UnaryExpression = If(Not propertyInfo.DeclaringType.IsValueType, Expression.TypeAs(instance, propertyInfo.DeclaringType), Expression.Convert(instance, propertyInfo.DeclaringType))

        Dim getterCall As MethodCallExpression = Expression.[Call](instanceCast, propertyInfo.GetGetMethod())

        Dim convert As UnaryExpression = If((Not propertyInfo.PropertyType.IsValueType), Expression.TypeAs(getterCall, GetType(Object)), Expression.Convert(getterCall, GetType(Object))) 'Expression.Convert(getterCall, GetType(Object))

        Dim lambda As Expression(Of Func(Of Object, Object)) = Expression.Lambda(Of Func(Of Object, Object))(convert, instance)

        Return lambda.Compile
    End Function

    <Extension>
    Public Function GetValueSetter(propertyInfo As PropertyInfo) As Action(Of Object, Object)
        Dim instance As ParameterExpression = Expression.Parameter(GetType(Object), "instance")

        Dim value As ParameterExpression = Expression.Parameter(GetType(Object), "value")

        Dim IsNull As BinaryExpression = Expression.Equal(value, Expression.Constant(Nothing, GetType(Object)))

        Dim nullValue As UnaryExpression = Expression.Convert(propertyInfo.PropertyType.GetDefaultExpression, GetType(Object))

        Dim nullCheckedValue = Expression.Condition(IsNull, nullValue, value)

        Dim instanceCast As UnaryExpression = If((Not propertyInfo.DeclaringType.IsValueType), Expression.TypeAs(instance, propertyInfo.DeclaringType), Expression.Convert(instance, propertyInfo.DeclaringType))

        Dim valueCast As UnaryExpression = If((Not propertyInfo.PropertyType.IsValueType), Expression.TypeAs(value, propertyInfo.PropertyType), Expression.Convert(nullCheckedValue, propertyInfo.PropertyType))

        Dim setterCall As MethodCallExpression = Expression.[Call](instanceCast, propertyInfo.GetSetMethod(), valueCast)

        Dim lambda As Expression(Of Action(Of Object, Object)) = Expression.Lambda(Of Action(Of Object, Object))(setterCall, instance, value)

        Return lambda.Compile()
    End Function

    <Extension>
    Public Function GetDefaultExpression(type As Type) As Expression
        If type.IsValueType Then
            Return Expression.Constant(Activator.CreateInstance(type), GetType(Object))
        End If
        Return Expression.Constant(Nothing, GetType(Object))
    End Function

End Module

This is the code that I'm using to test it:

Option Strict On
Option Explicit On

Imports System.ComponentModel
Imports System.Linq.Expressions
Imports System.Reflection
Imports System.Runtime.CompilerServices

Module Module1
    Const loopRun As Integer = 10000
    Const benchRun As Integer = 5

    Private myObj As New Obj With {.Prop1 = "hello", .Prop2 = "world", .Prop3 = "!",
                                   .Prop4 = 123, .Prop5 = 456, .Prop6 = 42,
                                   .Prop7 = Now.AddDays(-1), .Prop8 = Now, .Prop9 = Now.AddDays(1),
                                   .Prop10 = Obj.test.value1, .Prop11 = Obj.test.value2, .Prop12 = Obj.test.value3,
                                   .Prop13 = New ObjTest With {.Abc = "inst1", .Cde = 1},
                                   .Prop14 = New ObjTest With {.Abc = "inst2", .Cde = 2},
                                   .Prop15 = New ObjTest With {.Abc = "inst3", .Cde = 3}}

    Private myProp() As String = New String() {"Prop1", "Prop2", "Prop3", "Prop4", "Prop5", "Prop6", "Prop7", "Prop8", "Prop9", "Prop10", "Prop11", "Prop12", "Prop13", "Prop14", "Prop15"}

    Sub Main()
        DisplayValue()
        WarmUp()
        Benchmark()

        Console.Read()
    End Sub

    Private Sub DisplayValue()
        Dim value As Object

        For Each i In Cache.reflectionDict
            value = i.Value.GetValue(myObj, Nothing)
            Console.WriteLine("Original reflectionDict.{0}={1}", i.Key, i.Value.GetValue(myObj, Nothing))
            i.Value.SetValue(myObj, Nothing, Nothing)
            Console.WriteLine("Cleared reflectionDict.{0}={1}", i.Key, i.Value.GetValue(myObj, Nothing))
            i.Value.SetValue(myObj, value, Nothing)
            Console.WriteLine("Old reflectionDict.{0}={1}", i.Key, i.Value.GetValue(myObj, Nothing))
            Console.WriteLine()
        Next

        For Each i In Cache.descriptorDict
            value = i.Value.GetValue(myObj)
            Console.WriteLine("Original reflectionDict.{0}={1}", i.Key, i.Value.GetValue(myObj))
            i.Value.SetValue(myObj, Nothing)
            Console.WriteLine("Cleared descriptorDict.{0}={1}", i.Key, i.Value.GetValue(myObj))
            i.Value.SetValue(myObj, value)
            Console.WriteLine("Old descriptorDict.{0}={1}", i.Key, i.Value.GetValue(myObj))
            Console.WriteLine()
        Next

        For Each i In Cache.expressionGetDict
            value = i.Value(myObj)
            Console.WriteLine("Original expressionGetDict.{0}={1}", i.Key, i.Value(myObj))
            Cache.expressionSetDict(i.Key)(myObj, Nothing)
            Console.WriteLine("Cleared expressionGetDict.{0}={1}", i.Key, i.Value(myObj))
            Cache.expressionSetDict(i.Key)(myObj, value)
            Console.WriteLine("Old expressionGetDict.{0}={1}", i.Key, i.Value(myObj))
            Console.WriteLine()
        Next
    End Sub

    Private Sub WarmUp()
        For j = 0 To 100
            For Each i In Cache.reflectionDict
                i.Value.SetValue(myObj, i.Value.GetValue(myObj, Nothing), Nothing)
            Next

            For Each i In Cache.descriptorDict
                i.Value.SetValue(myObj, i.Value.GetValue(myObj))
            Next

            For Each i In Cache.expressionSetDict
                i.Value.Invoke(myObj, Cache.expressionGetDict(i.Key)(myObj))
            Next
        Next
    End Sub

    Private Sub Benchmark()
        Dim sw As Stopwatch

        For z = 1 To benchRun
            sw = Stopwatch.StartNew
            For i = 0 To loopRun
                For Each s In myProp
                    Cache.descriptorDict(s).SetValue(myObj, Cache.descriptorDict(s).GetValue(myObj))
                Next
            Next
            sw.Stop()
            Console.WriteLine("{0} : {1}ms", "descriptorDict", sw.Elapsed.TotalMilliseconds)
        Next

        For z = 1 To benchRun
            sw = Stopwatch.StartNew
            For i = 0 To loopRun
                For Each s In myProp
                    Cache.reflectionDict(s).SetValue(myObj, Cache.reflectionDict(s).GetValue(myObj, Nothing), Nothing)
                Next
            Next
            sw.Stop()
            Console.WriteLine("{0} : {1}ms", "reflectionDict", sw.Elapsed.TotalMilliseconds)
        Next

        For z = 1 To benchRun
            sw = Stopwatch.StartNew
            For i = 0 To loopRun
                For Each s In myProp
                    Cache.expressionSetDict(s)(myObj, Cache.expressionGetDict(s)(myObj))
                Next
            Next
            sw.Stop()
            Console.WriteLine("{0} {1} : {2}ms", "expressionSetDict", "expressionGetDict", sw.Elapsed.TotalMilliseconds)
        Next
    End Sub

End Module

Public Class ObjTest
    Public Property Abc As String
    Public Property Cde As Integer

    Public Overrides Function ToString() As String
        Return String.Format("ObjTest {0} : {1}", Abc, Cde)
    End Function
End Class

Public Class Obj

    Public Enum test As Byte
        value1 = 10
        value2 = 50
        value3 = 250
    End Enum

    Public Property Prop1 As String
    Public Property Prop2 As String
    Public Property Prop3 As String
    Public Property Prop4 As Integer
    Public Property Prop5 As Integer
    Public Property Prop6 As Integer
    Public Property Prop7 As DateTime
    Public Property Prop8 As DateTime
    Public Property Prop9 As DateTime
    Public Property Prop10 As test
    Public Property Prop11 As test
    Public Property Prop12 As test
    Public Property Prop13 As ObjTest
    Public Property Prop14 As ObjTest
    Public Property Prop15 As ObjTest
End Class


Public Module Cache
    Public ReadOnly reflectionDict As New Dictionary(Of String, PropertyInfo)

    Public ReadOnly descriptorDict As New Dictionary(Of String, PropertyDescriptor)

    Public ReadOnly expressionGetDict As New Dictionary(Of String, Func(Of Object, Object))
    Public ReadOnly expressionSetDict As New Dictionary(Of String, Action(Of Object, Object))

    Sub New()
        For Each p In GetType(Obj).GetProperties(BindingFlags.Instance Or BindingFlags.[Public])
            reflectionDict.Add(p.Name, p)
        Next

        For Each p As PropertyDescriptor In TypeDescriptor.GetProperties(GetType(Obj))
            descriptorDict.Add(p.Name, p)
        Next

        For Each p In GetType(Obj).GetProperties(BindingFlags.Instance Or BindingFlags.[Public])
            expressionGetDict.Add(p.Name, p.GetValueGetter)
            expressionSetDict.Add(p.Name, p.GetValueSetter)
        Next
    End Sub

End Module

Public Module PropertyInfoExtensions
    <Extension>
    Public Function GetValueGetter(propertyInfo As PropertyInfo) As Func(Of Object, Object)
        Dim instance As ParameterExpression = Expression.Parameter(GetType(Object), "instance")

        Dim instanceCast As UnaryExpression = If(Not propertyInfo.DeclaringType.IsValueType, Expression.TypeAs(instance, propertyInfo.DeclaringType), Expression.Convert(instance, propertyInfo.DeclaringType))

        Dim getterCall As MethodCallExpression = Expression.[Call](instanceCast, propertyInfo.GetGetMethod())

        Dim convert As UnaryExpression = If((Not propertyInfo.PropertyType.IsValueType), Expression.TypeAs(getterCall, GetType(Object)), Expression.Convert(getterCall, GetType(Object))) 'Expression.Convert(getterCall, GetType(Object))

        Dim lambda As Expression(Of Func(Of Object, Object)) = Expression.Lambda(Of Func(Of Object, Object))(convert, instance)

        Return lambda.Compile
    End Function

    <Extension>
    Public Function GetValueSetter(propertyInfo As PropertyInfo) As Action(Of Object, Object)
        Dim instance As ParameterExpression = Expression.Parameter(GetType(Object), "instance")

        Dim value As ParameterExpression = Expression.Parameter(GetType(Object), "value")

        Dim IsNull As BinaryExpression = Expression.Equal(value, Expression.Constant(Nothing, GetType(Object)))

        Dim nullValue As UnaryExpression = Expression.Convert(propertyInfo.PropertyType.GetDefaultExpression, GetType(Object))

        Dim nullCheckedValue = Expression.Condition(IsNull, nullValue, value)

        Dim instanceCast As UnaryExpression = If((Not propertyInfo.DeclaringType.IsValueType), Expression.TypeAs(instance, propertyInfo.DeclaringType), Expression.Convert(instance, propertyInfo.DeclaringType))

        Dim valueCast As UnaryExpression = If((Not propertyInfo.PropertyType.IsValueType), Expression.TypeAs(value, propertyInfo.PropertyType), Expression.Convert(nullCheckedValue, propertyInfo.PropertyType))

        Dim setterCall As MethodCallExpression = Expression.[Call](instanceCast, propertyInfo.GetSetMethod(), valueCast)

        Dim lambda As Expression(Of Action(Of Object, Object)) = Expression.Lambda(Of Action(Of Object, Object))(setterCall, instance, value)

        Return lambda.Compile()
    End Function

    <Extension>
    Public Function GetDefaultExpression(type As Type) As Expression
        If type.IsValueType Then
            Return Expression.Constant(Activator.CreateInstance(type), GetType(Object))
        End If
        Return Expression.Constant(Nothing, GetType(Object))
    End Function

End Module
\$\endgroup\$
2
  • \$\begingroup\$ Which time are you looking to optimize? \$\endgroup\$
    – Shlomo
    Nov 10, 2015 at 16:42
  • \$\begingroup\$ @Shlomo, expression (get/set) \$\endgroup\$
    – Fredou
    Nov 10, 2015 at 16:58

1 Answer 1

3
\$\begingroup\$

I tried a couple of things, and couldn't improve the performance. I did do the below for readability, though that's fairly subjective. I tried skipping expressions and just using reflection, but that was markedly slower, to my surprise.

I think all the if checking for value/reference type then switching between Expression.TypeAs and Expression.Convert is unnecessary. Expression.Convert will work for both reference and value types, and I don't think there's any serious performance benefits either way.

The main difference between the two is that TypeAs will return null if the subject doesn't match the type, whereas Convert will throw an exception.

<Extension>
Public Function GetValueGetter(propertyInfo As PropertyInfo) As Func(Of Object, Object)
    Dim instance As ParameterExpression = Expression.Parameter(GetType(Object), "instance")

    Dim lambda2 As Expression(Of Func(Of Object, Object)) = Expression.Lambda(Of Func(Of Object, Object))(
        Expression.Convert(
            Expression.[Call](
                Expression.Convert(instance, propertyInfo.DeclaringType),
                propertyInfo.GetGetMethod()),
            GetType(Object)
        ),
        instance)

    Return lambda2.Compile

End Function

<Extension>
Public Function GetValueSetter(propertyInfo As PropertyInfo) As Action(Of Object, Object)
    Dim instance As ParameterExpression = Expression.Parameter(GetType(Object), "instance")

    Dim value As ParameterExpression = Expression.Parameter(GetType(Object), "value")

    Dim lambda2 As Expression(Of Action(Of Object, Object)) = Expression.Lambda(Of Action(Of Object, Object))(
        Expression.[Call](
            Expression.Convert(instance, propertyInfo.DeclaringType),
            propertyInfo.GetSetMethod(),
            Expression.Convert(
                Expression.Condition(
                    Expression.Equal(
                        value,
                        Expression.Constant(Nothing, GetType(Object))
                    ),
                    Expression.Convert(propertyInfo.PropertyType.GetDefaultExpression, GetType(Object)),
                    value
                ),
                propertyInfo.PropertyType
            )
        ),
        instance, value)

    Return lambda2.Compile()

End Function
\$\endgroup\$
1
  • 1
    \$\begingroup\$ after looking at the code for a while, i like the way it's structured now. switch to convert only make sense too, i will wait a few hours to see if there is someone else that want to jump in and then i will mark the best answer \$\endgroup\$
    – Fredou
    Nov 10, 2015 at 18:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.