Какое влияние имеет DirectCast на производительность и позднюю/раннюю привязку?

Я всегда думал, что DirectCast() был довольно недорогим, проницательным и запоминающимся, и видел его в основном как способ помочь мне с IntelliSense, например. в обработчиках событий:

Public Sub myObject_EventHandler(sender As Object, e As System.EventArgs)
    'Explicit casting with DirectCast
    Dim myObject As myClass = DirectCast(sender, myClass)

    myObject.MyProperty = "myValue"
End Sub

Я понял, что это было лучше, очевидно, для меня как разработчик, но также и для скомпилированного кода и итоговой производительности, потому что он включал "раннее связывание", в отличие от...

Public Sub myObject_EventHandler(sender As Object, e As System.EventArgs)
    'No casting at all (late binding)
    myObject.MyProperty = "myValue"
End Sub

... который также компилируется и запускается, но использует "позднюю привязку", если я правильно понял термины. т.е. предполагая, что sender фактически является объектом myClass.

Что касается производительности, позднего/раннего связывания или чего-либо еще, каковы различия между самым первым фрагментом выше и следующим:

Public Sub myObject_EventHandler(sender As Object, e As System.EventArgs)
    'Implicit casting via variable declaration
    Dim myObject As myClass = sender

    myObject.MyProperty = "myValue"
End Sub

Является ли явный вызов DirectCast() полезным/вредным или не имеет значения после того, как компилятор оптимизировал код?

Ответ 1

TL; версия DR: Используйте DirectCast() вместо позднего связывания или отражения для лучшей производительности во время выполнения.

Этот вопрос был очень интригующим для меня. Я использую DirectCast() на регулярной основе почти для каждого приложения, которое я написал. Я всегда использовал его, потому что он заставляет IntelliSense работать, и если я не с Option Strict On, тогда я буду получать ошибки при компиляции. Время от времени я использую Option Strict Off, если я спешу, и я тестирую концепцию дизайна, или если я спешу за быстрое и грязное решение одноразовой проблемы. Я никогда не задумывался над эффектами производительности при использовании этого, потому что мне никогда не приходилось беспокоиться о производительности во время работы с тем, что я пишу.

Размышление об этом вопросе сделало меня немного любопытным, поэтому я решил проверить его и увидеть различия для себя. Я создал новое консольное приложение в Visual Studio и пошел работать. Ниже приведен весь исходный код приложения. Если вы хотите сами увидеть результаты, вы можете просто скопировать/вставить напрямую:

Option Strict Off
Option Explicit On
Imports System.Diagnostics
Imports System.Reflection
Imports System.IO
Imports System.Text


Module Module1
    Const loopCntr As Int32 = 1000000
    Const iterationCntr As Int32 = 5
    Const resultPath As String = "C:\StackOverflow\DirectCastResults.txt"

    Sub Main()
        Dim objDirectCast As New MyObject("objDirectCast")
        Dim objImplicitCasting As New MyObject("objImplicitCasting")
        Dim objLateBound As New MyObject("objLateBound")
        Dim objReflection As New MyObject("objReflection")
        Dim objInvokeMember As New MyObject("objInvokeMember")
        Dim sbElapsed As New StringBuilder : sbElapsed.Append("Running ").Append(iterationCntr).Append(" iterations for ").Append(loopCntr).AppendLine(" loops.")
        Dim sbAverage As New StringBuilder : sbAverage.AppendLine()

        AddHandler objDirectCast.ValueSet, AddressOf SetObjectDirectCast
        AddHandler objImplicitCasting.ValueSet, AddressOf SetObjectImplictCasting
        AddHandler objLateBound.ValueSet, AddressOf SetObjectLateBound
        AddHandler objReflection.ValueSet, AddressOf SetObjectReflection
        AddHandler objInvokeMember.ValueSet, AddressOf SetObjectInvokeMember

        For Each myObj As MyObject In {objDirectCast, objImplicitCasting, objLateBound, objReflection, objInvokeMember}
            Dim resultlist As New List(Of TimeSpan)
            sbElapsed.AppendLine().Append("Time (seconds) elapsed for ").Append(myObj.Name).Append(" is: ")
            For i = 1 To iterationCntr
                Dim stpWatch As New Stopwatch
                stpWatch.Start()
                For _i = 0 To loopCntr
                    myObj.SetValue(_i)
                Next
                stpWatch.Stop()
                sbElapsed.Append(stpWatch.Elapsed.TotalSeconds.ToString()).Append(", ")
                resultlist.Add(stpWatch.Elapsed)
                Console.WriteLine(myObj.Name & " is done.")
            Next

            Dim totalTicks As Long = 0L
            resultlist.ForEach(Sub(x As TimeSpan) totalTicks += x.Ticks)
            Dim averageTimeSpan As New TimeSpan(totalTicks / CLng(resultlist.Count))
            sbAverage.Append("Average elapsed time for ").Append(myObj.Name).Append(" is: ").AppendLine(averageTimeSpan.ToString)
        Next

        Using strWriter As New StreamWriter(File.Open(resultPath, FileMode.Create, FileAccess.Write))
            strWriter.WriteLine(sbElapsed.ToString)
            strWriter.WriteLine(sbAverage.ToString)
        End Using
    End Sub

    Sub SetObjectDirectCast(sender As Object, newValue As Int32)
        Dim myObj As MyObject = DirectCast(sender, MyObject)
        myObj.MyProperty = newValue
    End Sub

    Sub SetObjectImplictCasting(sender As Object, newValue As Int32)
        Dim myObj As MyObject = sender
        myObj.MyProperty = newValue
    End Sub

    Sub SetObjectLateBound(sender As Object, newValue As Int32)
        sender.MyProperty = newValue
    End Sub

    Sub SetObjectReflection(sender As Object, newValue As Int32)
        Dim pi As PropertyInfo = sender.GetType().GetProperty("MyProperty", BindingFlags.Public + BindingFlags.Instance)
        pi.SetValue(sender, newValue, Nothing)
    End Sub

    Sub SetObjectInvokeMember(sender As Object, newValue As Int32)
        sender.GetType().InvokeMember("MyProperty", BindingFlags.Instance + BindingFlags.Public + BindingFlags.SetProperty, Type.DefaultBinder, sender, {newValue})
    End Sub
End Module

Public Class MyObject
    Private _MyProperty As Int32 = 0
    Public Event ValueSet(sender As Object, newValue As Int32)
    Public Property Name As String

    Public Property MyProperty As Int32
        Get
            Return _MyProperty
        End Get
        Set(value As Int32)
            _MyProperty = value
        End Set
    End Property

    Public Sub New(objName As String)
        Me.Name = objName
    End Sub

    Public Sub SetValue(newvalue As Int32)
        RaiseEvent ValueSet(Me, newvalue)
    End Sub

End Class

Я попробовал запустить приложение в конфигурации Release, а также запустить конфигурацию выпуска без прикрепленного отладчика Visual Studio. Ниже приведены результаты:

Отпустите с помощью Visual Studio Debugger:

Running 5 iterations for 1000000 loops.

Time (seconds) elapsed for objDirectCast is: 0.0214367, 0.0155618, 0.015561, 0.015544, 0.015542, 
Time (seconds) elapsed for objImplicitCasting is: 0.014661, 0.0148947, 0.015051, 0.0149164, 0.0152732, 
Time (seconds) elapsed for objLateBound is: 4.2572548, 4.2073932, 4.3517058, 4.480232, 4.4216707, 
Time (seconds) elapsed for objReflection is: 0.3900658, 0.3833916, 0.3938861, 0.3875427, 0.4558457, 
Time (seconds) elapsed for objInvokeMember is: 1.523336, 1.1675438, 1.1519875, 1.1698862, 1.2878384, 

Average elapsed time for objDirectCast is: 00:00:00.0167291
Average elapsed time for objImplicitCasting is: 00:00:00.0149593
Average elapsed time for objLateBound is: 00:00:04.3436513
Average elapsed time for objReflection is: 00:00:00.4021464
Average elapsed time for objInvokeMember is: 00:00:01.2601184

Выпуск без Visual Studio Debugger:

Running 5 iterations for 1000000 loops.

Time (seconds) elapsed for objDirectCast is: 0.0073776, 0.0055385, 0.0058196, 0.0059637, 0.0057557, 
Time (seconds) elapsed for objImplicitCasting is: 0.0060359, 0.0056653, 0.0065522, 0.0063639, 0.0057324, 
Time (seconds) elapsed for objLateBound is: 4.4858827, 4.1643164, 4.2380467, 4.1217441, 4.1270739, 
Time (seconds) elapsed for objReflection is: 0.3828591, 0.3790779, 0.3849563, 0.3852133, 0.3847144, 
Time (seconds) elapsed for objInvokeMember is: 1.0869766, 1.0808392, 1.0881596, 1.1139259, 1.0811786, 

Average elapsed time for objDirectCast is: 00:00:00.0060910
Average elapsed time for objImplicitCasting is: 00:00:00.0060699
Average elapsed time for objLateBound is: 00:00:04.2274128
Average elapsed time for objReflection is: 00:00:00.3833642
Average elapsed time for objInvokeMember is: 00:00:01.0902160

Глядя на эти результаты, было бы похоже, что использование DirectCast() почти не влияет на производительность по сравнению с добавлением компилятора в литье. Однако, если полагаться на объект, который должен быть связан с последним, есть удар производительности HUGE, и выполнение значительно замедляется. При использовании System.Reflection наблюдается небольшое замедление при прямом литье объекта. Я думал, что необычно, что получение PropertyInfo было намного быстрее, чем использование метода .InvokeMember().

Вывод: По возможности используйте DirectCast() или непосредственно передайте объект. Использование отражения следует резервировать только тогда, когда вам это нужно. Используйте только последние связанные предметы в качестве последнего средства. Правда, если вы только несколько раз модифицируете объект здесь или там, это вряд ли будет иметь значение в великой схеме вещей.

Вместо этого вам следует больше беспокоиться о том, как эти методы могут выйти из строя, и о том, как их избежать. Например, в методе SetObjectInvokeMember(), если объект sender не имеет свойства MyProperty, то этот бит кода вызовет исключение. В методе SetObjectReflection() возвращаемая информация о свойствах могла бы быть nothing, которая привела бы к NullReferenceException.

Ответ 2

Я бы предложил запустить позднюю привязку и прямой листинг в цикле for около 100 000 раз и посмотреть, есть ли разница во времени между ними.

Создайте часы для обеих циклов и распечатайте результаты. Сообщите нам, есть ли разница. 100 000 раз могут быть слишком низкими, и вы можете на самом деле работать дольше.