【问题标题】:Are VBA Class module properties slow compared to accessing fields of User Defined Types?与访问用户定义类型的字段相比,VBA 类模块属性是否慢?
【发布时间】:2014-05-30 00:25:26
【问题描述】:

我一直在尝试使用类模块开发宏,但与 UDT 相比,get/let 似乎需要很长时间。我真的很感兴趣这是为什么,有人可以解释一下吗?我只发现讨论函数/子执行的讨论,这似乎同样快。

当前的问题是设置一个属性,该类大约需要 3000 毫秒(对于 200 万个 let),而使用 UDT 执行相同操作需要 120 毫秒。

我正在尝试决定是否应该建议宏开发人员在需要获取或设置大量属性时避免使用类模块。我应该只使用这个作为数据,但也许你有不同的见解。

我想了解为什么速度这么慢。也许我只是做错了什么。

示例代码:

Public Type Participant
    Name As String
    Gender As Integer
End Type

Public Declare Function GetTickCount Lib "kernel32.dll" () As Long

Sub TimeUDT()
   Dim i As Long
   Dim startMs As Long
   startMs = GetTickCount
   Dim participants(1 To 1000000) As Participant
   For i = 1 To 1000000
      participants(i).Name = "TestName"
      participants(i).Gender = 1
   Next
   Debug.Print GetTickCount - startMs
End Sub

Sub TimeCls()
   Dim i As Long
   Dim startMs As Long
   Dim participants(1 To 1000000) As New clsParticipant
   startMs = GetTickCount
   For i = 1 To 1000000
      participants(i).Name = "TestName"
      participants(i).Gender = 1
   Next
   Debug.Print GetTickCount - startMs
End Sub

以及类模块(名为 clsParticipant):

Private iGender As Integer
Private sName As String

Public Property Let Gender(value As Integer)
   iGender = value
End Property
Public Property Get Gender() As Integer
   Gender = iGender
End Property

Public Property Get Name() As String
   Name = sName
End Property
Public Property Let Name(value As String)
   sName = value
End Property

【问题讨论】:

  • 我的猜测是你正在寻找一个可扩展性问题并且不一定执行缓慢。如果您对少量迭代进行测试,我猜它们每次执行的时间仅为 300 毫秒的一小部分。如果您的设计确实需要 RAM 中的类的 200 万个实例,我认为您最好看看不同的设计。
  • 是的,您遇到了与几年前我必须处理的类对象相同的问题。我被“起草”来分析一个已在全球范围内投入生产的大型访问数据库,它像狗一样运行。我在每个函数和子例程中都放置了计时器,发现对类的引用很疯狂。我切换到一些全局变量和宾果游戏!我从不费心去理解为什么那些特定的慢,但它与初始化有关(不,它们是非常简单的类)。
  • @rheitzman : 不幸的是,由于这是一个正在改造的大型项目,目前无法更改设计。确实,例如为 10.000 个对象运行它需要 0 毫秒,但真正的问题是需要设置很多属性和少量(打开的工作簿的数量)实例。它用于保存在计算每个打开的工作簿时使用的数据。因此,由于数据集太大且类模块太慢,UDT 不是一个选项。
  • @WayneG.Dunn :如果我理解正确,您是在类模块之外使用全局变量吗?我们需要将这些作为实例,否则我们必须在另一个模块中保留一个单独的设置数组。就初始化而言:我在初始化类实例数组后启动计时器,所以我想在这种情况下这不会是问题。
  • 真的不理解关于这个问题的 3 个“缺乏足够信息”的近距离投票。它写得很清楚,并包含一个 SSCCE 来证明问题。它的质量比 excel、vba 标签的标准票价高得多。

标签: vba excel


【解决方案1】:

首先,我强烈建议您使用高分辨率计时器,这样您就不必测试尽可能多的迭代。使用QueryPerformanceCounter查看CTimer

这是我机器上的基线,10K 次迭代,高精度计时器

Sub TimeUDT()
   Dim i As Long
   Dim timer As New CTimer
   timer.StartCounter
   Dim participants(1 To 10000) As Participant
   For i = 1 To 10000
      participants(i).Name = "TestName"
      participants(i).Gender = 1
   Next
   Debug.Print "Elapsed time: " & timer.TimeElapsed & " ms"
End Sub

Elapsed time: 1.14359022404999 ms

不管你信不信,你实际上正在在你的循环中接受对象创建的打击。在启动计时器之前在循环中显式创建它们并查看差异:

之前

Sub TimeCls()
   Dim i As Long
   Dim timer As New CTimer
   Dim participants(1 To 10000) As New clsParticipant

   timer.StartCounter
   For i = 1 To 10000
      participants(i).Name = "TestName"
      participants(i).Gender = 1
   Next
   Debug.Print "Elapsed time: " & timer.TimeElapsed & " ms"
End Sub

Elapsed time: 24.9600996727434 ms

之后

Sub TimeCls()
   Dim i As Long
   Dim timer As New CTimer
   'Dim participants(1 To 10000) As New clsParticipant
   Dim participants(1 To 10000) As clsParticipant
   For i = 1 To 10000
       Set participants(i) = New clsParticipant
   Next i

   timer.StartCounter
   For i = 1 To 10000
      participants(i).Name = "TestName"
      participants(i).Gender = 1
   Next
   Debug.Print "Elapsed time: " & timer.TimeElapsed & " ms"
End Sub

Elapsed time: 4.66722880515984 ms

这仅比基线慢 4 倍(在对象创建命中现在从测量中排除之后)。如果你进一步声明你的iGendersName public 并直接改变它们,那么性能会更接近基线,所以其余的大部分性能影响来自Let 间接。

Sub TimeCls()
   Dim i As Long
   Dim timer As New CTimer
   Dim participants(1 To 10000) As clsParticipant
   For i = 1 To 10000
       Set participants(i) = New clsParticipant
   Next i

   timer.StartCounter
   For i = 1 To 10000
      'participants(i).Name = "TestName"
      'participants(i).Gender = 1
      participants(i).sName = "TestName"
      participants(i).iGender = 1
   Next
   Debug.Print "Elapsed time: " & timer.TimeElapsed & " ms"
End Sub

Elapsed time: 1.71887815565976 ms

【讨论】:

  • 非常好的故障!! +1
  • 很好的回应!非常感谢,我意识到我可能看到了由于实例的异步创建而导致的初始化,但是直到现在我还没有时间测试它,而且我看到你已经为我做了这个。感谢您花时间解释这一点。我认为类字段在 VBA 中是私有的。显然我还有一些东西要学。感谢您抽出宝贵时间进行测试并向我解释!
【解决方案2】:

韦伯在他的回答中正确地强调了这样一个事实,即在您的测试中,您没有考虑创建对象所需的时间,但忘记了添加对象的销毁。

下面代码中的时间同时考虑了创建和销毁时间,并表明没有什么是免费的,也就是说不管你怎么做,最后总时间差不多。

我还添加了最后 3 个函数,它们以三种不同的方式显式销毁对象,而不是将其留给垃圾收集器,时间保持不变。

我添加了这三个测试,因为我期待看到巨大的变化。我记得我很久以前做过的一个测试,其中垃圾收集器需要 10 或 100 倍的时间,只是改变了破坏的顺序。我无法在这里重现问题,可能是因为对象不多或者对象更简单。但我添加代码只是为了展示另一个测试,如果您发现销毁所需的时间突然增加。

这是我的代码:

Sub Test()
  Dim T0 As Single

  T0 = timer
  TimeCls_a
  Debug.Print , timer - T0

  T0 = timer
  TimeCls_b
  Debug.Print , timer - T0

  T0 = timer
  TimeCls_c
  Debug.Print , timer - T0

  T0 = timer
  TimeCls_c_up
  Debug.Print , timer - T0

  T0 = timer
  TimeCls_c_dn
  Debug.Print , timer - T0

  T0 = timer
  TimeCls_c_all
  Debug.Print , timer - T0
End Sub

Sub TimeCls_a()
   Dim i As Long
   Dim T0 As Single
   Dim participants(1 To NCYCLES) As New clsParticipant

   T0 = timer
   For i = 1 To NCYCLES
      participants(i).Name = "TestName"
      participants(i).Gender = 1
   Next
   Debug.Print "TimeCls_a:", timer - T0
End Sub

Sub TimeCls_b()
   Dim i As Long
   Dim T0 As Single
   Dim participants(1 To NCYCLES) As clsParticipant
   For i = 1 To NCYCLES
       Set participants(i) = New clsParticipant
   Next i

   T0 = timer
   For i = 1 To NCYCLES
      participants(i).Name = "TestName"
      participants(i).Gender = 1
   Next
   Debug.Print "TimeCls_b:", timer - T0
End Sub

Sub TimeCls_c()
   Dim i As Long
   Dim T0 As Single
   Dim participants(1 To NCYCLES) As clsParticipant
   For i = 1 To NCYCLES
       Set participants(i) = New clsParticipant
   Next i

   T0 = timer
   For i = 1 To NCYCLES
      participants(i).Name = "TestName"
      participants(i).Gender = 1
   Next
   Debug.Print "TimeCls_c:", timer - T0
End Sub

Sub TimeCls_c_up()
   Dim i As Long
   Dim T0 As Single
   Dim participants(1 To NCYCLES) As clsParticipant
   For i = 1 To NCYCLES
       Set participants(i) = New clsParticipant
   Next i

   T0 = timer
   For i = 1 To NCYCLES
      participants(i).Name = "TestName"
      participants(i).Gender = 1
   Next

   For i = 1 To NCYCLES
       Set participants(i) = Nothing
   Next i
   Debug.Print "TimeCls_c_up:", timer - T0
End Sub

Sub TimeCls_c_dn()
   Dim i As Long
   Dim T0 As Single
   Dim participants(1 To NCYCLES) As clsParticipant
   For i = 1 To NCYCLES
       Set participants(i) = New clsParticipant
   Next i

   T0 = timer
   For i = 1 To NCYCLES
      participants(i).Name = "TestName"
      participants(i).Gender = 1
   Next

   For i = NCYCLES To 1 Step -1
       Set participants(i) = Nothing
   Next i
   Debug.Print "TimeCls_c_dn:", timer - T0
End Sub

Sub TimeCls_c_al()
   Dim i As Long
   Dim T0 As Single
   Dim participants() As clsParticipant
   ReDim participants(1 To NCYCLES)
   For i = 1 To NCYCLES
       Set participants(i) = New clsParticipant
   Next i

   T0 = timer
   For i = 1 To NCYCLES
      participants(i).Name = "TestName"
      participants(i).Gender = 1
   Next

   ReDim participants(1 To NCYCLES)
   Debug.Print "TimeCls_c_al:", timer - T0
End Sub

【讨论】:

  • 我们试图隔离字段访问(设置)的成本,因此我们有目的地排除了对象创建和销毁时间。但是,如果对象的寿命很短,这些都是有效的考虑因素。对于您声称破坏顺序会影响性能,我并不感到非常惊讶。 VBA 使用引用计数,因此可能必须根据关系和顺序传播更多的引用计数减量。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-09
  • 2018-09-10
  • 2023-03-24
相关资源
最近更新 更多