VBA类对象的GC效率问题

起因

在用VBA处理数据时发现,如果向Microsoft Scripting Runtime类库的字典对象中添加约10万个的类(Class)对象,在脚本跑完时会卡住几秒才恢复;当类对象数量达到约16万个时,卡住的时间会达到十几秒。

问题原因

经过排查和搜索相关讨论,发现问题很可能是由于VBA的垃圾回收(garbage collection)造成的。VBA的GC机制是,当对象的引用数量变为零时,则清除该对象。但由于未知的原因,该GC机制的时间复杂度似乎是O(n²),一旦对象数量大幅增加,GC的时间就会显著增长。

时间与对象数量的关系
时间与对象数量的关系

为了验证该问题,写了一小段代码向Collection对象塞入指定数量的myClass对象,代码见文末,结果如上图。从图中可以明显发现,创建对象所需时间(蓝线)基本上是线性增长,但清除对象所需时间(红线)却是二次函数增长,当对象数量在30万时,创建只花费0.8秒,但清除却花了近12秒。

其他发现

经过进一步测试还有些其他发现:

  • 只有内容为类对象才存在该问题,对于类型(例如integer, string, UDT等)不存在该问题,因为这些并不是对象,不存在引用数量归零时清除垃圾问题。
  • 当容器为对象(例如Dictionary, Collection等)时,该问题可以稳定复现;当容器为数组(Array)时,该问题的复现不稳定。
  • 只有当类实例化后才存在该问题,只声明类变量并没有该问题。这很容易理解:既然对象没有实例化,自然无需GC。

解决方法

由于该问题过于基础,并未找到很好的解决方法。缓解方法包括:

  • 避免使用类(Class),而是使用自定义类型(UDT, user-defined type)。但有太多需求是UDT无法满足的。
  • 只在有必要的时候才实例化对象(比如相关性高的维度避免组合成笛卡尔积)。
  • 改用其他语言(比如Power Query M)。

附录

测试用代码如下。

Option Explicit
 
Sub TestScrrunClassSpeed()
    Dim c As New Collection     'Scripting.Collection
    Dim rgIO As Range           'Range for input/output
    Dim arrIO As Variant        'Temporary array for input/output
    Dim i&                      'Iterator
    Dim testnr%, qty&           'The # and object quantity of each test
    Dim t!(0 To 2)              'The start time, create time and destroy time
     
    'Read parameter from sheet: column A defines object quantity of each test
    Set rgIO = Sheets("Result").Range("A2:C31")
    arrIO = rgIO.Value
     
    'Do each test
    For testnr = LBound(arrIO) To UBound(arrIO)
        'Read object quantity and prepare
        qty = CLng(arrIO(testnr, 1))
        t(0) = Timer
         
        'Create class objects
        For i = 1 To qty
            c.Add New myClass
        Next i
        t(1) = Timer
         
        'Clear class objects
        Set c = Nothing
        t(2) = Timer
         
        'Calculate the time
        arrIO(testnr, 2) = (t(1) - t(0)) * 1000
        arrIO(testnr, 3) = (t(2) - t(1)) * 1000
    Next testnr
     
    'Write result to sheet
    rgIO.Value = arrIO
End Sub

发表评论

您的电子邮箱地址不会被公开。