图 1:标准连接与加括号连接在内存使用模式方面的比较
在特定情况下,使用括号可以对性能和可缩放性产生十分显著的影响,后文将对此进行进一步的说明。
StringBuilder
我们已经找到了解决字符串连接问题的快捷方法,在多数情况下,此方法可以达到性能和投入的最佳平衡。但是,如果要进一步提高构建大型字符串的性能,需要采用第二种方法,即减少字符串分配操作的数目。为此,需要使用 StringBuilder。StringBuilder 是一个类,用于维护可配置的字符串缓冲区,管理插入到此缓冲区的新文本片断,并仅在文本长度超出字符串缓冲区长度时对字符串进行重新分配。Microsoft .NET 框架免费提供了这样一个类 (System.Text.StringBuilder),并建议在该环境下进行的所有字符串连接操作中使用它。在 ASP 和传统的 Visual Basic 环境中,我们无法访问此类,因此需要自行创建。下面是使用 Visual Basic 6.0 创建的 StringBuilder 类示例(为简洁起见,省略了错误处理代码)。
Option Explicit
' 默认的缓冲区初始大小和增长系数
Private Const DEF_INITIALSIZE As Long = 1000
Private Const DEF_GROWTH As Long = 1000
' 缓冲区大小和增长
Private m_nInitialSize As Long
Private m_nGrowth As Long
' 缓冲区和缓冲区计数器
Private m_sText As String
Private m_nSize As Long
Private m_nPos As Long
Private Sub Class_Initialize()
' 设置大小和增长的默认值
m_nInitialSize = DEF_INITIALSIZE
m_nGrowth = DEF_GROWTH
' 初始化缓冲区
InitBuffer
End Sub
' 设置初始大小和增长数量
Public Sub Init(ByVal InitialSize As Long, ByVal Growth As Long)
If InitialSize >; 0 Then m_nInitialSize = InitialSize
If Growth >; 0 Then m_nGrowth = Growth
End Sub
' 初始化缓冲区
Private Sub InitBuffer()
m_nSize = -1
m_nPos = 1
End Sub
' 增大缓冲区
Private Sub Grow(Optional MinimimGrowth As Long)
' 初始化缓冲区(如有必要)
If m_nSize = -1 Then
m_nSize = m_nInitialSize
m_sText = Space$(m_nInitialSize)
Else
' 只是增长
Dim nGrowth As Long
nGrowth = IIf(m_nGrowth >; MinimimGrowth,
m_nGrowth, MinimimGrowth)
m_nSize = m_nSize + nGrowth
m_sText = m_sText &; Space$(nGrowth)
End If
End Sub
' 将缓冲区大小调整到当前使用的大小
Private Sub Shrink()
If m_nSize >; m_nPos Then
m_nSize = m_nPos - 1
m_sText = RTrim$(m_sText)
End If
End Sub
' 添加单个文本字符串
Private Sub AppendInternal(ByVal Text As String)
If (m_nPos + Len(Text)) >; m_nSize Then Grow Len(Text)
Mid$(m_sText, m_nPos, Len(Text)) = Text
m_nPos = m_nPos + Len(Text)
End Sub
' 添加一些文本字符串
Public Sub Append(ParamArray Text())
Dim nArg As Long
For nArg = 0 To UBound(Text)
AppendInternal CStr(Text(nArg))
Next nArg
End Sub
' 返回当前字符串数据并调整缓冲区大小
Public Function ToString() As String
If m_nPos >; 0 Then
Shrink
ToString = m_sText
Else
ToString = ""
End If
End Function
' 清除缓冲区并重新初始化
Public Sub Clear()
InitBuffer
End Sub
此类中使用的基本原则是,在类级别将变量 (m_sText) 用作字符串缓冲区,并使用 Space$ 函数以空格字符填充此缓冲区以将其设置为特定的大小。如果要将更多文本与现有文本连接在一起,则在检查缓冲区的大小足以存放新文本后,使用 Mid$ 函数在正确位置插入文本。ToString 函数将返回当前存储在缓冲区中的文本,并将缓冲区的大小调整为能够容纳此文本的正确长度。使用 StringBuilder 的 ASP 代码如下所示:
Function WriteHTML( Data )
Dim oSB
Dim nRep
Set oSB = Server.CreateObject( "StringBuilderVB.StringBuilder" )
' 用大小和增长系数初始化缓冲区
oSB.Init 15000, 7500
For nRep = 0 to 99
oSB.Append "<;TR>;<;TD>;", (nRep + 1), "<;/TD>;<;TD>;", _
Data( 0, nRep ), "<;/TD>;<;TD>;", _
Data( 1, nRep ), "<;/TD>;<;TD>;", _
Data( 2, nRep ), "<;/TD>;<;TD>;", _
Data( 3, nRep ), "<;/TD>;<;TD>;", _
Data( 4, nRep ), "<;/TD>;<;TD>;", _
Data( 5, nRep ), "<;/TD>;<;/TR>;"
Next
WriteHTML = oSB.ToString()
Set oSB = Nothing
End Function
使用 StringBuilder 需要一定的开销,因为每次使用此类时都必须创建它的实例,并且在创建第一个类实例时必须加载包含此类的 DLL。对 StringBuilder 实例进行额外方法调用时也需要开销。使用加括号的“&;”方法时,StringBuilder 如何执行取决于多个因素,包括连接的数目、要构建的字符串的大小以及选择的 StringBuilder 字符串缓冲区的初始化参数的性能。请注意,在多数情况下,将缓冲区中所需的空间量估计得略高一些要远远好于让其不断增长。
内置方法
ASP 包含一种非常快捷的创建 HTML 代码的方法,只需多次调用 Response.Write。Write 函数使用隐式优化的字符串缓冲区,此缓冲区能够提供非常优秀的性能特性。修改后的 WriteHTML 代码如下所示:
Function WriteHTML( Data )
Dim nRep
For nRep = 0 to 99
Response.Write "<;TR>;<;TD>;"
Response.Write (nRep + 1)
Response.Write "<;/TD>;<;TD>;"
Response.Write Data( 0, nRep )
Response.Write "<;/TD>;<;TD>;"
Response.Write Data( 1, nRep )
Response.Write "<;/TD>;<;TD>;"
Response.Write Data( 2, nRep )
Response.Write "<;/TD>;<;TD>;"
Response.Write Data( 3, nRep )
Response.Write "<;/TD>;<;TD>;"
Response.Write Data( 4, nRep )
Response.Write "<;/TD>;<;TD>;"
Response.Write Data( 5, nRep )
Response.Write "<;/TD>;<;/TR>;"
Next
End Function
虽然这段代码很可能为我们提供最佳的性能和可缩放性,但在某种程度上已经破坏了封装,因为现在会将函数内部的代码直接写入 Response 流,所以调用代码丧失了一定程度的控制权。另外,移动此代码(例如,移入 COM 组件)将变得更加困难,因为此函数与 Response 流存在依赖关系。
测试
上面提到的四种方法分别通过一个简单的 ASP 页面(包含一个由虚拟字符串数组提供数据的单个表格)进行了测试。我们使用 Application Center Test?; (ACT) 从单个客户端(Windows?; XP Professional,PIII-850MHz,512MB RAM)针对 100Mb/sec 网络中的单个服务器(Windows 2000 Advanced Server,双 PIII-1000MHz,256MB RAM)执行了测试。ACT 配置为使用 5 个线程,以模拟 5 个用户连接至网站时的负载。每个测试都包括 20 秒预热时间和随后的 100 秒负载时间,在负载期间创建了尽可能多的请求。
通过更改主表格循环中的重复次数,针对不同数目的连接操作重复运行测试,如 WriteHTML 函数中的代码片断所示。运行的每个测试都使用上文提到的四种不同的方法执行。
结果
下面的一系列图表显示了各种方法对整个应用程序吞吐量的影响,以及 ASP 页面的响应时间。通过这些图表,我们可以了解应用程序支持的请求数目,以及用户等待页面下载至浏览器所需的时间。
此主题相关图片如下:
表 1:使用的连接方法缩写的说明
方法缩写 说明
RESP 内置 Response.Write 方法
CAT 标准连接(“&;”)方法
PCAT 加括号的连接(“&;”)方法
BLDR StringBuilder 方法
在模拟典型 ASP 应用程序工作负荷方面,此测试与实际情况相差甚远,从表 2 中可以明显看到,即使重复 420 次,此页面仍不是特别大。现在很多复杂的 ASP 页面在这些数字上都是比较高的,设置有可能超出此测试范围的限制。
表 2:测试示例的页面大小和连接数目
重复次数 连接数目 页面大小(以字节为单位)
15 240 2,667
30 480 4,917
45 720 7,167
60 960 9,417
75 1,200 11,667
120 1,920 18,539
180 2,880 27,899
240 3,840 37,259
300 4,800 46,619
360 5,760 55,979
420 6,720 62,219
此主题相关图片如下:
图 2:吞吐量结果图
从图 2 的图表中可以看到,正如我们所预期的,多重 Response.Write 方法 (RESP) 在测试的整个重复测试范围中为我们提供了最佳的吞吐量。但令人惊讶的是,标准字符串连接方法 (CAT) 的下降如此巨大,而加括号的方法 (PCAT) 在重复执行 300 多次时性能依旧要好很多。在大约重复 220 次之处,字符串缓存带来的性能提高超过了 StringBuilder 方法 (BLDR) 固有的开销,在这一点以上,在此 ASP 页面中使用 StringBuilder 所需的额外开销是值得的。
此主题相关图片如下:
图 3:响应时间结果图
图 4:省略 CAT 的响应时间结果图
图 3 和图 4 中的图表显示了按“到第一字节的时间”测量的响应时间(以毫秒为单位)。因为标准字符串连接方法 (CAT) 的响应时间增加过快,所以又提供了未包括此方法的图表(图 4),以便分析其他方法之间的差异。有一点值得注意,多重 Response.Write 方法 (RESP) 和 StringBuilder 方法 (BLDR) 随重复次数的增加呈现一种近似线性的增长,而标准连接方法 (CAT) 和加括号的方法 (PCAT) 则在超过一定的阈值之后开始迅速增加。
小结
本文着重讲述了如何在 ASP 环境中应用不同的字符串构建技术,这些内容同样适用于所有使用 Visual Basic 代码创建大型字符串的方案,例如手动创建 XML 文档。以下原则可以帮助您确定哪种方法最适合您的需要。
首先尝试加括号的“&;”方法,尤其是在处理现有代码时。这种方法对代码结构的影响微乎其微,但您会发现应用程序的性能将显著增强,甚至会超出预定目标。
在不破坏所需的封装级别的情况下使用 Response.Write。使用此方法,可以避免不必要的内存内字符串处理,从而提供最佳的性能。
使用 StringBuilder 构建真正大型或连接数目较多的字符串。
尽管您可能未看到本文所示的这种性能增长,但我已在真实的 ASP Web 应用程序中使用了这些技巧,只需要很少的额外投入就可以在性能和可缩放性方面获得很大的提高。