在尝试构建可序列化的数据结构的过程中,我发现自己正在构建大型字符串,这很慢,因为VBA每次执行连接时都会复制一个字符串。和Java的StringBuilder接口,我将一个unicode clsStringBuilder类拼凑在一起。考虑到了VBA可能在“背后”执行的意外复制行为,我可以避免这种行为,或者对编码样式进行了纠正(或没有纠正)。

Option Compare Database
Option Explicit

'******
'* v2 *
'******


Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal dst As Long, ByVal src As Long, ByVal Length As Long)

Private Const DEFAULT_CAPACITY As Long = &H10
Private m_currLen As Long
Private m_stringBuffer() As Byte

Private Sub Class_Initialize()
    ReDim m_stringBuffer(0 To (DEFAULT_CAPACITY * 2) - 1) 'Each unicode character is 2 bytes
End Sub

Public Function Append(strString As String) As clsStringBuilder
On Error GoTo derp


    If m_currLen + LenB(strString) < UBound(m_stringBuffer) Then
        CopyMemory VarPtr(m_stringBuffer(m_currLen)), StrPtr(strString), LenB(strString)
    Else
        If m_currLen + LenB(strString) < UBound(m_stringBuffer) * 2 Then
            Expand
        Else
            Expand m_currLen + LenB(strString)
        End If
        CopyMemory VarPtr(m_stringBuffer(m_currLen)), StrPtr(strString), LenB(strString)
    End If
    m_currLen = m_currLen + LenB(strString)
    Set Append = Me
    Exit Function

derp:
    Stop
    Resume
End Function

Public Property Get Length() As Long
    Length = m_currLen / 2
End Property

Public Property Get Capacity() As Long
    Capacity = UBound(m_stringBuffer)
End Property

Private Sub Expand(Optional newSize As Long = 0)
    If newSize <> 0 Then
        ReDim Preserve m_stringBuffer(0 To newSize - 1)
    Else
        ReDim Preserve m_stringBuffer(0 To (UBound(m_stringBuffer) * 2) + 1)
    End If
End Sub

Public Function toString() As String
    toString = Mid(m_stringBuffer, 1, m_currLen / 2)
End Function


这里是一个测试:

Public Sub Main()
    Dim sb As clsStringBuilder
    Set sb = New clsStringBuilder
    Dim strString As String
    Dim i As Long
    Dim StartTime As Double

    'VBA String
    StartTime = MicroTimer()
    For i = 0 To 100000
        strString = strString + "Hello World;"
    Next
    Debug.Print "The VBA String took: " & Round(MicroTimer - StartTime, 3) & " seconds"

    'StringBuilder
    StartTime = MicroTimer()
    For i = 0 To 100000
        sb.Append "Hello World;"
    Next
    Debug.Print "The Stringbuilder took: " & Round(MicroTimer - StartTime, 3) & " seconds"

    'Are the strings the same?
    Debug.Print StrComp(strString, sb.toString, vbBinaryCompare)
End Sub


这是Microsoft的MicroTimer函数,可以在这里找到:

Private Declare Function getFrequency Lib "kernel32" _
Alias "QueryPerformanceFrequency" (cyFrequency As Currency) As Long
Private Declare Function getTickCount Lib "kernel32" _
Alias "QueryPerformanceCounter" (cyTickCount As Currency) As Long
Function MicroTimer() As Double
'

' Returns seconds.
    Dim cyTicks1 As Currency
    Static cyFrequency As Currency
    '
    MicroTimer = 0

' Get frequency.
    If cyFrequency = 0 Then getFrequency cyFrequency

' Get ticks.
    getTickCount cyTicks1

' Seconds
    If cyFrequency Then MicroTimer = cyTicks1 / cyFrequency
End Function


评论

嘿黑鹰(Blackhawk)-我已经对代码进行了一些调整,并将其上传到GitHub上,我不知道您是否看到了蒂姆·霍尔(Tim Hall)的VBA-Blocks,但我认为通过类似nuget的StringBuilder类来使用真棒VBA的软件包管理器(Rubberduck最终可能会为其设计UI,因此可以在VBE中添加/更新/维护项目的软件包)。如果您可以联系Tim同意我对您的代码所做的事情(或者如果您愿意,我可以删除该存储库),那就太好了!
@MathieuGuindon太棒了!如果您需要我戳别人,请让我知道,否则请将此评论视为批准的印章:)

@TonyDallimore添加了更多说明,标准ReDim Preserve最昂贵的部分是副本。当您有一块内存并且想要仅将其“扩展”一个字符时,实际发生的情况是操作系统必须找到一个全新的内存块(即新请求的大小),然后必须从将旧块转换为新块。随着数据的增长,这花费的时间越来越长。想象一下,建立一个2GB的阵列,一次扩展1个字节-您最终将制作约20亿个副本,总计约20亿GB!

@Blackhawk感谢您的评论。我了解字符串连接和ReDim Preserve如何在后台工作的原理。我看到ReDim Preserve Arr(1到UBound(Arr)+ 1)常常使我没有足够仔细地研究您的代码。一次,MathieuGuindon指出,每个ReDim都会使数组的大小增加一倍,而不仅仅是为下一个追加添加足够的数组,因此代码如此之快的原因显而易见。

@Mathieu:您部分正确;使用上面的测试:VBA字符串花费了68107秒; MatsMugStringbuilder花费了:0,883秒; BlackhawkStringbuilder用了0,396秒;并且两个StringBuilder版本都避免了字符串空间不足...在10K串联下没什么大不了的。

#1 楼

我很喜欢这个棒极了。像.NET String对象一样,VBA字符串是不可变的,这意味着在.NET中,当"the quick brown fox"附加"jumps over"然后"the lazy dog"时,已经生成了4个字符串,因此第一个被复制了3次。因此,绝对欢迎任何VBA工具包都欢迎使用VBA StringBuilder类!

这是您在这里获得的一些严肃的代码。让我们看一下。

因此,您已将其称为clsStringBuilder类。我知道您来自哪里,但是没有“ cls”匈牙利前缀的真正原因-我将其删除,并将其称为StringBuilder类。 >

不必理会。我知道版本控制本来是用VBA几乎不可能的,但是仍然不需要在注释中“版本化”代码。您实际上维护版本号吗?何必呢?只需将其删除,它就变得毫无用处。


十六进制16对吗?我认为使用十进制表示法会更清楚。实际上,这种容量可能会造成混淆,尤其是考虑到十六进制表示法时。 10的字节数是多少?是字符吗?


'******
'* v2 *
'******



啊,那么字符。调用常数16怎么样?不,太久了..我个人不喜欢YELLCASE,我只称它为&H10,但是我看到其他人使用ALL CAPS作为常量-只要您一致,它就可以工作:) br />
顺便说一句,您在这里的评论不错,但我不会麻烦指定“ unicode”字符;当IDE本身仅支持ANSI字符串时,它会造成混乱! br />实际上,由于DEFAULT_CHARACTER_CAPACITY是以字节为单位,因此我将其称为InitialCharacterCapacity,以避免出现问号:


Private Const DEFAULT_CAPACITY As Long = &H10





ReDim m_stringBuffer(0 To (DEFAULT_CAPACITY * 2) - 1) 'Each unicode character is 2 bytes



m_currLen,真的吗?我没有比这更多的匈牙利语了!您还应该知道默认情况下会传递参数currentLength-我将签名更改为:

Public Property Get Length() As Long
    Length = m_currLen / 2
End Property


错误处理不是最佳的-如果发生异常,您将为最终用户提供IDE,以帮助他们调试和调试代码!那不是生产准备就绪的: br />
Public Function Append(strString As String) As clsStringBuilder



您可能还想确保m_stringBuffer不会实际缩小缓冲区。我认为。 ;)

最后,我不确定我为什么理解stringBufffer不遵循约定并像所有公共方法一样在buffer中进行命名-currentLength看起来更好。 >辛苦了!

评论


\ $ \ begingroup \ $
感谢您的反馈-现在修复命名约定。对于成员变量,我经常遇到需要将它们设为私有并通过Get / Set / Let属性提供访问权限的情况。在这种情况下,我使用“ m_”阻止VBA大喊“检测到歧义名称:membername”。在这些情况下的最佳做法是什么?
\ $ \ endgroup \ $
–黑鹰
2014-10-22 20:36



\ $ \ begingroup \ $
我喜欢将我所有的私有字段填充为私有类型-这里是私有类型TStringBuilder,其中包含所有成员。然后该类只有1个字段,我将其称为,例如,将其私有化为TStringBuilder-这样就消除了名称冲突,并且我喜欢看到this.MemberName =公共属性中的值Let MemberName(ByVal value As Whatever)块;)
\ $ \ endgroup \ $
– Mathieu Guindon♦
14-10-22在20:39

\ $ \ begingroup \ $
有趣的...我喜欢这个:D我必须仔细考虑一下含义...
\ $ \ endgroup \ $
–黑鹰
14-10-22在20:54

\ $ \ begingroup \ $
也许只是我,但我喜欢YELLCASE常量...
\ $ \ endgroup \ $
–RubberDuck
2014年10月23日,1:13

#2 楼

您的StringBuilder非常令人印象深刻:) ++符合Mat的建议,除了我在RubberDuck一边的YELLCASE;)

我想我已经确定了潜在的内存溢出(out of memory)。可能几乎没有任何人会发生,但是,嘿...如果用另一个循环包装循环,则VBA运行时似乎无法赶上计数和释放引用的速度。

例如:

For j = 0 To 1000
    Dim csb As New clsStringBuilder

    StartTime = MicroTimer()
    For i = 0 To 100000
        csb.Append "Hello World;"
    Next
Next


它将在某个时候停止在StringBuilder中,并导致derp ... AFAIC,您无能为力。 ..除了不允许像我这样的人测试您的代码; P jk!

我还有其他一些小事情: />
□多次计算得出相同的数字效率低下。如果需要在一个if-else / select情况下获得5次out of memory的值,请考虑将此数字存储在变量中。

Select Case(在If-Else中)应该比Ubound(arr)快一点。 (您的Mid$()

通过我的改进,总体速度似乎只快了一点-太微妙了吗? ;)

每个测试1000次



好吧,我刚刚将名称更改为ToString(),这就是我所做的: />
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" _
    (ByVal dst As Long, ByVal src As Long, ByVal Length As Long)

Private Const DEFAULT_CAPACITY As Long = 16
Private m_currLen As Long
Private m_stringBuffer() As Byte

Private Sub Class_Initialize()
    ReDim m_stringBuffer(0 To (DEFAULT_CAPACITY + DEFAULT_CAPACITY) - 1) 'Each unicode character is 2 bytes
End Sub

Public Function Append(strString As String) As StringBuilder
On Error GoTo derp

    Dim uBuffer As Long
    uBuffer = UBound(m_stringBuffer)

    Dim lengthB As Long
    lengthB = LenB(strString)

    Dim sPtr As Long
    sPtr = StrPtr(strString)

    Dim currLen As Long
    currLen = m_currLen + lengthB

    Select Case currLen
        Case Is < uBuffer
            CopyMemory VarPtr(m_stringBuffer(m_currLen)), sPtr, lengthB
        Case Is < (uBuffer + uBuffer)
            Expand
            CopyMemory VarPtr(m_stringBuffer(m_currLen)), sPtr, lengthB
        Case Else
            Expand currLen
            CopyMemory VarPtr(m_stringBuffer(m_currLen)), sPtr, lengthB
    End Select

    m_currLen = currLen
    Set Append = Me
    Exit Function

derp:
    Stop
    Resume
End Function

Public Property Get Length() As Long
    Length = m_currLen * 0.5
End Property

Public Property Get Capacity() As Long
    Capacity = UBound(m_stringBuffer)
End Property

Private Sub Expand(Optional newSize As Long = 0)
    Select Case newSize
        Case Is = 0
            ReDim Preserve m_stringBuffer(0 To (UBound(m_stringBuffer) + UBound(m_stringBuffer)) + 1)
        Case Else
            ReDim Preserve m_stringBuffer(0 To newSize - 1)
    End Select
End Sub

Public Function ToString() As String
    ToString = Mid$(m_stringBuffer, 1, m_currLen * 0.5)
End Function


您可以使用Mid()玩更多一些,但我把它留在对...感到满意的状态。

&应该是+,因此无需存储在变量IMO

评论


\ $ \ begingroup \ $
谢谢!现在解决这个问题...您使用什么来生成花式图表?
\ $ \ endgroup \ $
–黑鹰
14-10-23在17:43

\ $ \ begingroup \ $
你不能认真对待你的问题吗? :P Excel 2010 ...
\ $ \ endgroup \ $
–user28366
14-10-23在17:59

\ $ \ begingroup \ $
当然,是因为这些颜色不是“办公室粉彩”罐头而引起的
\ $ \ endgroup \ $
–黑鹰
14-10-23在18:01

\ $ \ begingroup \ $
@Blackhawk不用担心;)我敢猜想自己,我敢打赌我们都有我们最喜欢的RGB;)
\ $ \ endgroup \ $
–user28366
14-10-23在22:14

\ $ \ begingroup \ $
@ user28366您已经成为VBA的As New语法的受害者。在循环中使用Dim csb作为New StringBuilder不会在每次迭代中创建新实例,因此,您只能使用第一个实例,并且可以有效地添加“ Hello World;”。仅100m次。这是一个超过2GB的字符串(2,402,424,024字节),或者在内存用尽之前出现了长时间溢出。溢出问题与StringBuilder类无关,而与您的过度使用有关。开源VBE外接程序Rubberduck VBA(我为之贡献)会发现这种疏忽。
\ $ \ endgroup \ $
– ThunderFrame
17 Mar 29 '17 at 22:22

#3 楼

实际上不需要使用CopyMemory,您可以使用数组简单地实现相同的目的。代码不仅更短,而且更快。

Dim MyBuffer() As String
Dim MyCurrentIndex As Long
Dim MyMaxIndex As Long

Private Sub Class_Initialize()

    MyCurrentIndex = 0
    MyMaxIndex = 16
    ReDim MyBuffer(1 To MyMaxIndex)

End Sub

'Appends the given Text to this StringBuilder
Public Sub Append(Text As String)

    MyCurrentIndex = MyCurrentIndex + 1

    If MyCurrentIndex > MyMaxIndex Then
        MyMaxIndex = 2 * MyMaxIndex
        ReDim Preserve MyBuffer(1 To MyMaxIndex)
    End If
    MyBuffer(MyCurrentIndex) = Text

End Sub

'Returns the text in this StringBuilder
'Optional Parameter: Separator (default vbNullString) used in joining components
Public Function ToString(Optional Separator As String = vbNullString) As String

    If MyCurrentIndex > 0 Then
        ReDim Preserve MyBuffer(1 To MyCurrentIndex)
        MyMaxIndex = MyCurrentIndex
        ToString = Join(MyBuffer, Separator)
    End If

End Function


评论


\ $ \ begingroup \ $
您可能想添加一些解释,以及为什么和为什么这比OP发布的要好。还要注意,这个问题已经很老了(这里我们不在乎规范的答案,而是代码的实际审查),因此OP可能已经发展了。
\ $ \ endgroup \ $
–地狱
17年2月8日在10:32

\ $ \ begingroup \ $
@ Martin.Roller仅看代码,似乎MyCurrentIndex需要增加。是这样吗?? myCurrentIndex = myCurrentIndex + Len(文本)-1
\ $ \ endgroup \ $
– donPablo
17年5月2日在4:26



\ $ \ begingroup \ $
@donPablo:再看一遍或尝试单步执行代码。文本的长度无关紧要。
\ $ \ endgroup \ $
– Martin.Roller
17年5月15日在10:23

\ $ \ begingroup \ $
@Blackhawk:请注意,此处的速度仅取决于附加的数量,而不取决于附加文本的大小。
\ $ \ endgroup \ $
– Martin.Roller
17年5月15日在10:28

\ $ \ begingroup \ $
这显然是要使用的版本,只有一个没有32/64位可移植性问题。
\ $ \ endgroup \ $
–Patrick Honorez
18-11-15在13:53

#4 楼

我知道这是一个古老的问题,但是发现它的人可能会发现这个有价值的答案。我已经使用Excel 2013测试了StringBuilder的4个版本,最快的版本似乎是Blackhawk代码的优化版本(下面列出了优化代码)。下面的列表显示了一个示例,说明每个版本进行Blackhawk的测试所需的时间为1000次(总共100,000,000次附加)。新的优化版本标记为“新”,耗时略长于34秒。 Martin.Roller的基于数组的代码被标记为“ Ary”,耗时超过36秒,并且几乎与Blackhawk的代码相关。我已经运行了几次测试,Blackhawk的代码确实领先于Martin.Roller的代码。 Blackhawk的代码被标记为“旧”,耗时超过36秒。 “ Mid”版本是最后一个版本,它是我使用VBA的MID语句创建的一个版本,用于替换字符串的内容。在下面的代码中删除“ On Error”应该可以进一步提高速度,但是请确保调用代码所生成的字符串永远不会超过VBA可以处理的长度。


新的StringBuilder需要: 34.396秒
Ary StringBuilder花费了:36.467秒
老StringBuilder花费了:36.605秒
Mid StringBuilder花费了:40.141秒

Blackhawk的StringBuilder的新优化版本:

Option Compare Text
Option Explicit

Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal dst As Long, ByVal src As Long, ByVal Length As Long)

Private Const InitialCharCount As Long = 16
' Define members
Private mUBound As Long
Private mString() As Byte

Private Sub Class_Initialize()
    Clear
End Sub

Public Sub Clear()
    mUBound = -1
    ReDim mString(0 To InitialCharCount * 2 - 1) 'Each unicode character is 2 bytes
End Sub

Public Function Append(value As String) As StringBuilder
Dim NewUBound As Long
Dim CapacityUBound As Long
On Error GoTo Failed
    NewUBound = mUBound + LenB(value)

    If NewUBound > UBound(mString) Then
        CapacityUBound = UBound(mString) * 2 + 1
        If NewUBound > CapacityUBound Then CapacityUBound = NewUBound * 2 + 1
        ReDim Preserve mString(0 To CapacityUBound)
    End If
    CopyMemory VarPtr(mString(mUBound + 1)), StrPtr(value), LenB(value)

    mUBound = NewUBound
    Set Append = Me
    Exit Function

Failed:
    Stop
    Resume
End Function

Public Property Get Length() As Long
    Length = (mUBound + 1) / 2
End Property

Public Function ToString() As String
    ToString = Mid(mString, 1, Length)
End Function


评论


\ $ \ begingroup \ $
感谢您这样做!本着问题的精神,您可以添加对所做优化的描述吗?这将帮助任何关注此答案的人了解什么变化导致加速。我不得不说,我最喜欢VBA的一件事是缺少标准库-您将学习如何将自己的所有东西都投放出去:
\ $ \ endgroup \ $
–黑鹰
18年8月20日在14:41