字符串解析与内存优化

在处理大量数据时,经常需要将字符串数据解析为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个字节)以反映数组需要的元素数量。

如何指向SAFEARRAY结构

在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)

这些函数有两个问题:

  • 它们对VBA用户不可用。
  • 它们比之前描述的通过数组直接访问要慢,因为在调用DLL导入函数时有一些开销。

示例

在附带的示例中,创建了一个20MB的字符串,然后开始计算其中数字字符的数量,使用了三种方法:

  • 使用Mid$()函数(字符串访问)
  • 创建一个整数缓冲数组并将字符串数据复制到其中(数组访问)
  • 使用GetMem2()函数(内存访问)
  • 创建一个指向字符串数据的整数数组(直接访问)
  • 使用C++ 2012编写的DLL函数(C++ API访问),这只在EXE文件中有效,不在IDE中,除非更改了其声明。

与字符串访问相比,性能提升非常巨大(EXE中快30倍),而数组和直接访问之间的速度提升非常小……但实际上,由于没有使用数据副本来解析字符串,所以节省了大约40MB的内存。另一方面,内存访问为提供了良好的性能,没有内存过载,代码也更简单,大约比最后两种方法慢2-3倍。当然,性能最高的赢家是C++ API访问,它是最快的,但并不比直接访问方法快多少。

警告:

这种方法有一个奇怪的行为……当在IDE中运行应用程序时,它会完美地工作,但当编译它时,它会引发一个“下标越界(错误9)”,除非在编译项目时取消数组边界检查。可以通过按选项按钮,然后转到编译选项卡,按高级优化按钮,然后选中“移除数组边界检查”来移除它。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485