大多數(shù) Active Server Pages (ASP) 應(yīng)用程序都要通過字符串連接來創(chuàng)建呈現(xiàn)給用戶的 HTML 格式的數(shù)據(jù)。本文對(duì)幾種創(chuàng)建此 HTML 數(shù)據(jù)流的方法進(jìn)行了比較,在特定情況下,某些方法在性能方面要優(yōu)于其他方法。本文假定您已經(jīng)具備一定的 ASP 和 Visual Basic 編程方面的知識(shí)。
目錄
- 簡介
- ASP 設(shè)計(jì)
- 字符串連接
- 快捷的解決方案
- StringBuilder
- 內(nèi)置方法
- 測試
- 結(jié)果
- 小結(jié)
簡介
編寫 ASP 頁面時(shí),開發(fā)人員實(shí)際上是創(chuàng)建一個(gè)格式化的文本流,通過 ASP 提供的 Response 對(duì)象寫入 Web 客戶端。創(chuàng)建此文本流的方法有多種,而您選擇的方法將對(duì) Web 應(yīng)用程序的性能和可縮放性產(chǎn)生很大影響。很多次,在我?guī)椭蛻魞?yōu)化其 Web 應(yīng)用程序的性能時(shí),發(fā)現(xiàn)其中一個(gè)比較有效的方法是更改 HTML 流的創(chuàng)建方式。本文將介紹幾種常用技術(shù),并測試它們對(duì)一個(gè)簡單的 ASP 頁面的性能所產(chǎn)生的影響。
ASP 設(shè)計(jì)
許多 ASP 開發(fā)人員都遵循良好的軟件工程原則,盡可能地將其代碼模塊化。這種設(shè)計(jì)通常使用一些包含文件,這些文件中包含對(duì)頁面的特定不連續(xù)部分進(jìn)行格式化生成的函數(shù)。這些函數(shù)的字符串輸出(通常是 HTML 表格代碼)可以通過各種組合創(chuàng)建一個(gè)完整的頁面。某些開發(fā)人員對(duì)此方法進(jìn)行了改進(jìn),將這些 HTML 函數(shù)移到 Visual Basic COM 組件中,希望充分利用已編譯的代碼提供的額外性能。
盡管這種設(shè)計(jì)方法很不錯(cuò),但創(chuàng)建組成這些不連續(xù) HTML 代碼組件的字符串所使用的方法將對(duì) Web 站點(diǎn)的性能和可縮放性產(chǎn)生很大的影響,無論實(shí)際的操作是在 ASP 包含文件中執(zhí)行還是在 Visual Basic COM 組件中執(zhí)行。
字符串連接
請(qǐng)看以下 WriteHTML 函數(shù)的代碼片斷。名為 Data 的參數(shù)只是一個(gè)字符串?dāng)?shù)組,其中包含一些要格式化為表格結(jié)構(gòu)的數(shù)據(jù)(例如,從數(shù)據(jù)庫返回的數(shù)據(jù))。
Function WriteHTML( Data )
Dim nRep
For nRep = 0 to 99
sHTML = sHTML vbcrlf _
"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 = sHTML
End Function
這是很多 ASP 和 Visual Basic 開發(fā)人員創(chuàng)建 HTML 代碼時(shí)常用的方法。sHTML 變量中包含的文本返回到調(diào)用代碼,然后使用 Response.Write 寫入客戶端。當(dāng)然,這還可以表示為直接嵌入不包含 WriteHTML 函數(shù)的頁面的類似代碼。此代碼的問題是,ASP 和 Visual Basic 使用的字符串?dāng)?shù)據(jù)類型(BSTR 或 Basic 字符串)實(shí)際上無法更改長度。這意味著每當(dāng)字符串長度更改時(shí),內(nèi)存中字符串的原始表示形式都將遭到破壞,而且將創(chuàng)建一個(gè)包含新字符串?dāng)?shù)據(jù)的新的表示形式:這將增加分配內(nèi)存和解除分配內(nèi)存的操作。當(dāng)然,ASP 和 Visual Basic 已為您解決了這一問題,因此實(shí)際開銷不會(huì)立即顯現(xiàn)出來。分配內(nèi)存和解除分配內(nèi)存要求基本運(yùn)行時(shí)代碼解除各個(gè)專用鎖定,因此需要大量開銷。當(dāng)字符串變得很大并且有大塊內(nèi)存要被快速連續(xù)地分配和解除分配時(shí),此問題變得尤為明顯,就像在大型字符串連接期間出現(xiàn)的情況一樣。盡管這一問題對(duì)單用戶環(huán)境的影響不大,但在服務(wù)器環(huán)境(例如,在 Web 服務(wù)器上運(yùn)行的 ASP 應(yīng)用程序)中,它將導(dǎo)致嚴(yán)重的性能和可縮放性問題。
下面,我們回到上述代碼片段:此代碼中要執(zhí)行多少個(gè)字符串分配操作?答案是 16 個(gè)。在這種情況下,“
”運(yùn)算符的每次應(yīng)用都將導(dǎo)致變量 sHTML
所指的字符串被破壞和重新創(chuàng)建。前面已經(jīng)提到,字符串分配的開銷很大,并且隨著字符串的增大而增加,因此,我們可以對(duì)上述代碼進(jìn)行改進(jìn)。
快捷的解決方案
有兩種方法可以緩解字符串連接的影響,第一種方法是嘗試減小要處理的字符串的大小,第二種方法是嘗試減少執(zhí)行字符串分配操作的數(shù)目。請(qǐng)參見下面所示的 WriteHTML 代碼的修訂版本。
Function WriteHTML( Data )
Dim nRep
For nRep = 0 to 99
sHTML = sHTML ( vbcrlf _
"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 = sHTML
End Function
乍一看,可能很難發(fā)現(xiàn)這段代碼與上一個(gè)代碼示例的差別。其實(shí),此代碼只是在 sHTML = sHTML
后的內(nèi)容外面加上了括號(hào)。這實(shí)際上是通過更改優(yōu)先順序,來減小大多數(shù)字符串連接操作中處理的字符串大小。在最初的代碼示例中,ASP 編譯器將查看等號(hào)右邊的表達(dá)式,并從左到右進(jìn)行計(jì)算。結(jié)果,每次重復(fù)都要進(jìn)行 16 個(gè)連接操作,這些操作針對(duì)不斷增長的 sHTML
進(jìn)行。在新版本中,我們提示編譯器更改操作順序?,F(xiàn)在,它將按從左到右、從括號(hào)內(nèi)到括號(hào)外的順序計(jì)算表達(dá)式。此技術(shù)使得每次重復(fù)包括 15 個(gè)連接操作,這些操作針對(duì)的是不會(huì)增長的較小字符串,只有一個(gè)是針對(duì)不斷增長的大的 sHTML
。圖 1 顯示了這種優(yōu)化方法與標(biāo)準(zhǔn)連接方法在內(nèi)存使用模式方面的比較。
圖 1:標(biāo)準(zhǔn)連接與加括號(hào)連接在內(nèi)存使用模式方面的比較
在特定情況下,使用括號(hào)可以對(duì)性能和可縮放性產(chǎn)生十分顯著的影響,后文將對(duì)此進(jìn)行進(jìn)一步的說明。
StringBuilder
我們已經(jīng)找到了解決字符串連接問題的快捷方法,在多數(shù)情況下,此方法可以達(dá)到性能和投入的最佳平衡。但是,如果要進(jìn)一步提高構(gòu)建大型字符串的性能,需要采用第二種方法,即減少字符串分配操作的數(shù)目。為此,需要使用 StringBuilder。StringBuilder 是一個(gè)類,用于維護(hù)可配置的字符串緩沖區(qū),管理插入到此緩沖區(qū)的新文本片斷,并僅在文本長度超出字符串緩沖區(qū)長度時(shí)對(duì)字符串進(jìn)行重新分配。Microsoft .NET 框架免費(fèi)提供了這樣一個(gè)類 (System.Text.StringBuilder),并建議在該環(huán)境下進(jìn)行的所有字符串連接操作中使用它。在 ASP 和傳統(tǒng)的 Visual Basic 環(huán)境中,我們無法訪問此類,因此需要自行創(chuàng)建。下面是使用 Visual Basic 6.0 創(chuàng)建的 StringBuilder 類示例(為簡潔起見,省略了錯(cuò)誤處理代碼)。
Option Explicit
' 默認(rèn)的緩沖區(qū)初始大小和增長系數(shù)
Private Const DEF_INITIALSIZE As Long = 1000
Private Const DEF_GROWTH As Long = 1000
' 緩沖區(qū)大小和增長
Private m_nInitialSize As Long
Private m_nGrowth As Long
' 緩沖區(qū)和緩沖區(qū)計(jì)數(shù)器
Private m_sText As String
Private m_nSize As Long
Private m_nPos As Long
Private Sub Class_Initialize()
' 設(shè)置大小和增長的默認(rèn)值
m_nInitialSize = DEF_INITIALSIZE
m_nGrowth = DEF_GROWTH
' 初始化緩沖區(qū)
InitBuffer
End Sub
' 設(shè)置初始大小和增長數(shù)量
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
' 初始化緩沖區(qū)
Private Sub InitBuffer()
m_nSize = -1
m_nPos = 1
End Sub
' 增大緩沖區(qū)
Private Sub Grow(Optional MinimimGrowth As Long)
' 初始化緩沖區(qū)(如有必要)
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
' 將緩沖區(qū)大小調(diào)整到當(dāng)前使用的大小
Private Sub Shrink()
If m_nSize > m_nPos Then
m_nSize = m_nPos - 1
m_sText = RTrim$(m_sText)
End If
End Sub
' 添加單個(gè)文本字符串
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
' 返回當(dāng)前字符串?dāng)?shù)據(jù)并調(diào)整緩沖區(qū)大小
Public Function ToString() As String
If m_nPos > 0 Then
Shrink
ToString = m_sText
Else
ToString = ""
End If
End Function
' 清除緩沖區(qū)并重新初始化
Public Sub Clear()
InitBuffer
End Sub
此類中使用的基本原則是,在類級(jí)別將變量 (m_sText
) 用作字符串緩沖區(qū),并使用 Space$ 函數(shù)以空格字符填充此緩沖區(qū)以將其設(shè)置為特定的大小。如果要將更多文本與現(xiàn)有文本連接在一起,則在檢查緩沖區(qū)的大小足以存放新文本后,使用 Mid$ 函數(shù)在正確位置插入文本。ToString 函數(shù)將返回當(dāng)前存儲(chǔ)在緩沖區(qū)中的文本,并將緩沖區(qū)的大小調(diào)整為能夠容納此文本的正確長度。使用 StringBuilder 的 ASP 代碼如下所示:
Function WriteHTML( Data )
Dim oSB
Dim nRep
Set oSB = Server.CreateObject( "StringBuilderVB.StringBuilder" )
' 用大小和增長系數(shù)初始化緩沖區(qū)
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 需要一定的開銷,因?yàn)槊看问褂么祟悤r(shí)都必須創(chuàng)建它的實(shí)例,并且在創(chuàng)建第一個(gè)類實(shí)例時(shí)必須加載包含此類的 DLL。對(duì) StringBuilder 實(shí)例進(jìn)行額外方法調(diào)用時(shí)也需要開銷。使用加括號(hào)的“
”方法時(shí),StringBuilder 如何執(zhí)行取決于多個(gè)因素,包括連接的數(shù)目、要構(gòu)建的字符串的大小以及選擇的 StringBuilder 字符串緩沖區(qū)的初始化參數(shù)的性能。請(qǐng)注意,在多數(shù)情況下,將緩沖區(qū)中所需的空間量估計(jì)得略高一些要遠(yuǎn)遠(yuǎn)好于讓其不斷增長。
內(nèi)置方法
ASP 包含一種非??旖莸膭?chuàng)建 HTML 代碼的方法,只需多次調(diào)用 Response.Write。Write 函數(shù)使用隱式優(yōu)化的字符串緩沖區(qū),此緩沖區(qū)能夠提供非常優(yōu)秀的性能特性。修改后的 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
雖然這段代碼很可能為我們提供最佳的性能和可縮放性,但在某種程度上已經(jīng)破壞了封裝,因?yàn)楝F(xiàn)在會(huì)將函數(shù)內(nèi)部的代碼直接寫入 Response 流,所以調(diào)用代碼喪失了一定程度的控制權(quán)。另外,移動(dòng)此代碼(例如,移入 COM 組件)將變得更加困難,因?yàn)榇撕瘮?shù)與 Response 流存在依賴關(guān)系。
測試
上面提到的四種方法分別通過一個(gè)簡單的 ASP 頁面(包含一個(gè)由虛擬字符串?dāng)?shù)組提供數(shù)據(jù)的單個(gè)表格)進(jìn)行了測試。我們使用 Application Center Test® (ACT) 從單個(gè)客戶端(Windows® XP Professional,PIII-850MHz,512MB RAM)針對(duì) 100Mb/sec 網(wǎng)絡(luò)中的單個(gè)服務(wù)器(Windows 2000 Advanced Server,雙 PIII-1000MHz,256MB RAM)執(zhí)行了測試。ACT 配置為使用 5 個(gè)線程,以模擬 5 個(gè)用戶連接至網(wǎng)站時(shí)的負(fù)載。每個(gè)測試都包括 20 秒預(yù)熱時(shí)間和隨后的 100 秒負(fù)載時(shí)間,在負(fù)載期間創(chuàng)建了盡可能多的請(qǐng)求。
通過更改主表格循環(huán)中的重復(fù)次數(shù),針對(duì)不同數(shù)目的連接操作重復(fù)運(yùn)行測試,如 WriteHTML 函數(shù)中的代碼片斷所示。運(yùn)行的每個(gè)測試都使用上文提到的四種不同的方法執(zhí)行。
結(jié)果
下面的一系列圖表顯示了各種方法對(duì)整個(gè)應(yīng)用程序吞吐量的影響,以及 ASP 頁面的響應(yīng)時(shí)間。通過這些圖表,我們可以了解應(yīng)用程序支持的請(qǐng)求數(shù)目,以及用戶等待頁面下載至瀏覽器所需的時(shí)間。
表 1:使用的連接方法縮寫的說明
方法縮寫 |
說明 |
RESP |
內(nèi)置 Response.Write 方法 |
CAT |
標(biāo)準(zhǔn)連接(“ ”)方法 |
PCAT |
加括號(hào)的連接(“ ”)方法 |
BLDR |
StringBuilder 方法 |
在模擬典型 ASP 應(yīng)用程序工作負(fù)荷方面,此測試與實(shí)際情況相差甚遠(yuǎn),從表 2 中可以明顯看到,即使重復(fù) 420 次,此頁面仍不是特別大?,F(xiàn)在很多復(fù)雜的 ASP 頁面在這些數(shù)字上都是比較高的,設(shè)置有可能超出此測試范圍的限制。
表 2:測試示例的頁面大小和連接數(shù)目
重復(fù)次數(shù) |
連接數(shù)目 |
頁面大?。ㄒ宰止?jié)為單位) |
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:吞吐量結(jié)果圖
從圖 2 的圖表中可以看到,正如我們所預(yù)期的,多重 Response.Write 方法 (RESP) 在測試的整個(gè)重復(fù)測試范圍中為我們提供了最佳的吞吐量。但令人驚訝的是,標(biāo)準(zhǔn)字符串連接方法 (CAT) 的下降如此巨大,而加括號(hào)的方法 (PCAT) 在重復(fù)執(zhí)行 300 多次時(shí)性能依舊要好很多。在大約重復(fù) 220 次之處,字符串緩存帶來的性能提高超過了 StringBuilder 方法 (BLDR) 固有的開銷,在這一點(diǎn)以上,在此 ASP 頁面中使用 StringBuilder 所需的額外開銷是值得的。
圖 3:響應(yīng)時(shí)間結(jié)果圖
圖 4:省略 CAT 的響應(yīng)時(shí)間結(jié)果圖
圖 3 和圖 4 中的圖表顯示了按“到第一字節(jié)的時(shí)間”測量的響應(yīng)時(shí)間(以毫秒為單位)。因?yàn)闃?biāo)準(zhǔn)字符串連接方法 (CAT) 的響應(yīng)時(shí)間增加過快,所以又提供了未包括此方法的圖表(圖 4),以便分析其他方法之間的差異。有一點(diǎn)值得注意,多重 Response.Write 方法 (RESP) 和 StringBuilder 方法 (BLDR) 隨重復(fù)次數(shù)的增加呈現(xiàn)一種近似線性的增長,而標(biāo)準(zhǔn)連接方法 (CAT) 和加括號(hào)的方法 (PCAT) 則在超過一定的閾值之后開始迅速增加。
小結(jié)
本文著重講述了如何在 ASP 環(huán)境中應(yīng)用不同的字符串構(gòu)建技術(shù),這些內(nèi)容同樣適用于所有使用 Visual Basic 代碼創(chuàng)建大型字符串的方案,例如手動(dòng)創(chuàng)建 XML 文檔。以下原則可以幫助您確定哪種方法最適合您的需要。
- 首先嘗試加括號(hào)的“
”方法,尤其是在處理現(xiàn)有代碼時(shí)。這種方法對(duì)代碼結(jié)構(gòu)的影響微乎其微,但您會(huì)發(fā)現(xiàn)應(yīng)用程序的性能將顯著增強(qiáng),甚至?xí)鲱A(yù)定目標(biāo)。
- 在不破壞所需的封裝級(jí)別的情況下使用 Response.Write。使用此方法,可以避免不必要的內(nèi)存內(nèi)字符串處理,從而提供最佳的性能。
- 使用 StringBuilder 構(gòu)建真正大型或連接數(shù)目較多的字符串。
盡管您可能未看到本文所示的這種性能增長,但我已在真實(shí)的 ASP Web 應(yīng)用程序中使用了這些技巧,只需要很少的額外投入就可以在性能和可縮放性方面獲得很大的提高。