起因
在用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