在处理大量数据时,经常需要将字符串数据解析为JSON对象或其他数据结构。Sam提出了一种方法,即将字符串字符复制到整数数组中,这样可以更快地读取和解析数据。尽管复制数据会消耗一些时间,但性能的提升仍然令人惊讶。然而,当处理非常大的字符串时,需要缓冲,这就需要创建一个相同大小的数组,并在其中复制字符串数据,这会消耗大量的内存和时间。
建议是创建一个与字符串共享数据的数组。换句话说,数组的第一个整数正是字符串的第一个字符,改变它们中的任何一个都会改变另一个。在这种情况下,只需要创建一个小数组(一个或两个元素),然后改变其底层长度和数据,使其指向字符串的长度和字符。
Visual Basic 6和Visual Basic for Applications都是建立在COM结构之上的,其所有数据类型都是自动化数据类型。因此,大多数变量都是指向COM结构的指针。在这里,对两种数据类型感兴趣:字符串和数组。
1. 字符串
字符串以Unicode字符的形式保存在BSTR结构中。不需要知道BSTR的详细信息,只需知道可以通过著名的未记录函数‘StrPtr()’获得字符串字符序列的指针。
2. 数组
这里的情况更加复杂,因为需要改变数组的底层结构(长度和数据指针)。数组保存在SAFEARRAY结构中,定义如下:
typedef struct tagSAFEARRAY {
USHORT cDims; // 维度数量
USHORT Features; // 标志
ULONG cbElements; // 单个元素的大小
ULONG cLocks; // 锁定数量
PVOID pvData; // 数据
SAFEARRAYBOUND rgsabound[1]; // 每个维度一个界限
} SAFEARRAY, *LPSAFEARRAY;
因此,当获得SAFEARRAY指针时,需要改变数组的数据指针(pvData),它位于SAFEARRAY结构的前12个字节之后。还需要改变数组大小,这在‘rgsabound’成员中保存,以反映字符串的大小。
typedef struct tagSAFEARRAYBOUND {
ULONG cElements; // 维度中的元素数量
ULONG lLbound; // 维度的下限
} SAFEARRAYBOUND, *LPSAFEARRAYBOUND;
因此,需要改变cElements(位于SAFEARRAY结构开始后的16个字节)以反映数组需要的元素数量。
在VB6或VBA中,很容易通过直接使用VarPtr()函数获得任何对象的指针。但这对数组不适用!如果尝试将数组传递给该函数,将得到一个错误。为了解决这个问题,需要为VarPtr()函数创建一个新的声明,该声明接受数组作为参数。这种方式的问题是,如果改变了VB版本,需要改变声明……以下代码描述了如何做到这一点:
' 对于VB6用户:
Private Declare Function VarPtrArray Lib "msvbvm60.dll" Alias "VarPtr" (var() As Any) As Long
' 对于VB5用户:
Private Declare Function VarPtrArray Lib "msvbvm50.dll" Alias "VarPtr" (var() As Any) As Long
' 对于Office 2010+的VBA用户:
Private Declare Function VarPtrArray Lib "VBE7" Alias "VarPtr" (var() As Any) As Long
' 对于Office 2010之前的VBA用户:
Private Declare Function VarPtrArray Lib "VBE6" Alias "VarPtr" (var() As Any) As Long
1. 获取字符串
Dim S as string, Count as long
S = "Some large string"
Count = Len(s)
2. 创建缓冲数组
Dim Buffer(1) as Integer ' 两个元素数组
3. 获取SAFEARRAY指针
Dim pArray as Long, pSafeArray as Long
pArray = VarPtrArray(Buffer())
CopyMemoryToAny pSafeArray, pArray, 4
4. 备份SAFEARRAY原始数据
Dim pOldData as Long
CopyMemoryToAny pOldData, pSafeArray + 12, 4
5. 将SAFEARRAY数据更改为字符串数据
CopyAnyToMemory pSafeArray + 12, StrPtr(S), 4
CopyAnyToMemory pSafeArray + 16, Count, 4
6. 执行工作
现在可以开始操作、解析甚至更改字符串数据,而不需要更改其大小。请注意不要删除字符串变量或数组数据。这可能会导致意外的行为。
7. 恢复原始SAFEARRAY数据
CopyAnyToMemory pSafeArray + 12, pOldData, 4
CopyAnyToMemory pSafeArray + 16, 2, 4
也可以通过使用未记录的函数GetMem2直接访问字符串数据,可以声明如下:
' 对于VB6用户
Private Declare Sub GetInteger Lib "MSVBVM60.dll" Alias "GetMem2" (ByRef Src As Any, ByRef Dst As Integer)
' 对于VB5用户
Private Declare Sub GetInteger Lib "MSVBVM50.dll" Alias "GetMem2" (ByRef Src As Any, ByRef Dst As Integer)
这些函数有两个问题:
在附带的示例中,创建了一个20MB的字符串,然后开始计算其中数字字符的数量,使用了三种方法:
与字符串访问相比,性能提升非常巨大(EXE中快30倍),而数组和直接访问之间的速度提升非常小……但实际上,由于没有使用数据副本来解析字符串,所以节省了大约40MB的内存。另一方面,内存访问为提供了良好的性能,没有内存过载,代码也更简单,大约比最后两种方法慢2-3倍。当然,性能最高的赢家是C++ API访问,它是最快的,但并不比直接访问方法快多少。
警告:
这种方法有一个奇怪的行为……当在IDE中运行应用程序时,它会完美地工作,但当编译它时,它会引发一个“下标越界(错误9)”,除非在编译项目时取消数组边界检查。可以通过按选项按钮,然后转到编译选项卡,按高级优化按钮,然后选中“移除数组边界检查”来移除它。