大多數 Active Server Pages (ASP) 應用程式都要通過字串串連來建立呈現給使用者的 HTML 格式的資料。本文對幾種建立此 HTML 資料流的方法進行了比較,在特定情況下,某些方法在效能方面要優於其他方法。本文假定您已經具備一定的 ASP 和 Visual Basic 編程方面的知識。
目錄
- 簡介
- ASP 設計
- 字串串連
- 快捷的解決方案
- StringBuilder
- 內建方法
- 測試
- 結果
- 小結
簡介
編寫 ASP 頁面時,開發人員實際上是建立一個格式化的文字資料流,通過 ASP 提供的 Response 對象寫入 網頁用戶端。建立此文字資料流的方法有多種,而您選擇的方法將對 Web 應用程式的效能和可縮放性產生很大影響。很多次,在我協助客戶最佳化其 Web 應用程式的效能時,發現其中一個比較有效方法是更改 HTML 流的建立方式。本文將介紹幾種常用技術,並測試它們對一個簡單的 ASP 頁面的效能所產生的影響。
ASP 設計
許多 ASP 開發人員都遵循良好的軟體工程原則,儘可能地將其代碼模組化。這種設計通常使用一些包含檔案,這些檔案中包含對頁面的特定不連續部分進行格式化產生的函數。這些函數的字串輸出(通常是 HTML 表格代碼)可以通過各種組合建立一個完整的頁面。某些開發人員對此方法進行了改進,將這些 HTML 函數移到 Visual Basic COM 組件中,希望充分利用已編譯的代碼提供的額外效能。
儘管這種設計方法很不錯,但建立組成這些不連續 HTML 程式碼組件的字串所使用的方法將對 Web 網站的效能和可縮放性產生很大的影響,無論實際的操作是在 ASP 包含檔案中執行還是在 Visual Basic COM 組件中執行。
字串串連
請看以下 WriteHTML 函數的代碼片斷。名為 Data 的參數只是一個字串數組,其中包含一些要格式化為表格結構的資料(例如,從資料庫返回的資料)。
Function WriteHTML( Data )Dim nRepFor 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>"NextWriteHTML = sHTMLEnd Function
這是很多 ASP 和 Visual Basic 開發人員建立 HTML 程式碼時常用的方法。sHTML 變數中包含的文本返回到調用代碼,然後使用 Response.Write 寫入用戶端。當然,這還可以表示為直接嵌入不包含 WriteHTML 函數的頁面的類似代碼。此代碼的問題是,ASP 和 Visual Basic 使用的字串資料型別(BSTR 或 Basic 字串)實際上無法更改長度。這意味著每當字串長度更改時,記憶體中字串的原始表示形式都將遭到破壞,而且將建立一個包含新字串資料的新的表示形式:這將增加分配記憶體和解除配置記憶體的操作。當然,ASP 和 Visual Basic 已為您解決了這一問題,因此實際開銷不會立即顯現出來。分配記憶體和解除配置記憶體要求基本運行時代碼解除各個專用鎖定,因此需要大量開銷。當字串變得很大並且有大塊記憶體要被快速連續地分配和解除配置時,此問題變得尤為明顯,就像在大型字串串連期間出現的情況一樣。儘管這一問題對單使用者環境的影響不大,但在伺服器環境(例如,在 Web 服務器上啟動並執行 ASP 應用程式)中,它將導致嚴重的效能和可縮放性問題。
下面,我們回到上述程式碼片段:此代碼中要執行多少個字串分配操作?答案是 16 個。在這種情況下,“&
”運算子的每次應用都將導致變數 sHTML
所指的字串被破壞和重新建立。前面已經提到,字串分配的開銷很大,並且隨著字串的增大而增加,因此,我們可以對上述代碼進行改進。
快捷的解決方案
有兩種方法可以緩解字串串連的影響,第一種方法是嘗試減小要處理的字串的大小,第二種方法是嘗試減少執行字串分配操作的數目。請參見下面所示的 WriteHTML 代碼的修訂版本。
Function WriteHTML( Data )Dim nRepFor 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>" )NextWriteHTML = sHTMLEnd Function
乍一看,可能很難發現這段代碼與上一個程式碼範例的差別。其實,此代碼只是在 sHTML = sHTML &
後的內容外面加上了括弧。這實際上是通過更改優先順序,來減小大多數字串串連操作中處理的字串大小。在最初的程式碼範例中,ASP 編譯器將查看等號右邊的運算式,並從左至右進行計算。結果,每次重複都要進行 16 個串連操作,這些操作針對不斷增長的 sHTML
進行。在新版本中,我們提示編譯器更改操作順序。現在,它將按從左至右、從括弧內到括弧外的順序計算運算式。此技術使得每次重複包括 15 個串連操作,這些操作針對的是不會增長的較小字串,只有一個是針對不斷增長的大的 sHTML
。圖 1 顯示了這種最佳化方法與標準串連方法在記憶體使用量模式方面的比較。
圖 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 = 1000Private Const DEF_GROWTH As Long = 1000' 緩衝區大小和增長Private m_nInitialSize As LongPrivate m_nGrowth As Long' 緩衝區和緩衝區計數器Private m_sText As StringPrivate m_nSize As LongPrivate m_nPos As LongPrivate Sub Class_Initialize() ' 設定大小和增長的預設值 m_nInitialSize = DEF_INITIALSIZE m_nGrowth = DEF_GROWTH ' 初始化緩衝區 InitBufferEnd 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 = GrowthEnd Sub' 初始化緩衝區Private Sub InitBuffer() m_nSize = -1 m_nPos = 1End 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 IfEnd Sub' 將緩衝區大小調整到當前使用的大小Private Sub Shrink() If m_nSize > m_nPos Then m_nSize = m_nPos - 1 m_sText = RTrim$(m_sText) End IfEnd 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 nArgEnd Sub ' 返回當前字串資料並調整緩衝區大小Public Function ToString() As String If m_nPos > 0 Then Shrink ToString = m_sText Else ToString = "" End IfEnd Function' 清除緩衝區並重新初始化Public Sub Clear() InitBufferEnd Sub
此類中使用的基本原則是,在類層級將變數 (m_sText
) 用作字串緩衝區,並使用 Space$ 函數以空白字元填充此緩衝區以將其設定為特定的大小。如果要將更多文本與現有文本串連在一起,則在檢查緩衝區的大小足以存放新文本後,使用 Mid$ 函數在正確位置插入文本。ToString 函數將返回當前儲存在緩衝區中的文本,並將緩衝區的大小調整為能夠容納此文本的正確長度。使用 StringBuilder 的 ASP 代碼如下所示:
Function WriteHTML( Data )Dim oSBDim nRepSet oSB = Server.CreateObject( "StringBuilderVB.StringBuilder" )' 用大小和增長係數初始化緩衝區oSB.Init 15000, 7500For 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>"NextWriteHTML = oSB.ToString()Set oSB = NothingEnd Function
使用 StringBuilder 需要一定的開銷,因為每次使用此類時都必須建立它的執行個體,並且在建立第一個類執行個體時必須載入包含此類的 DLL。對 StringBuilder 執行個體進行額外方法調用時也需要開銷。使用加括弧的“&
”方法時,StringBuilder 如何執行取決於多個因素,包括串連的數目、要構建的字串的大小以及選擇的 StringBuilder 字串緩衝區的初始化參數的效能。請注意,在多數情況下,將緩衝區中所需的空間量估計得略高一些要遠遠好於讓其不斷增長。
內建方法
ASP 包含一種非常快捷的建立 HTML 程式碼的方法,只需多次調用 Response.Write。Write 函數使用隱式最佳化的字串緩衝區,此緩衝區能夠提供非常優秀的效能特性。修改後的 WriteHTML 代碼如下所示:
Function WriteHTML( Data )Dim nRepFor 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>"NextEnd 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 應用程式中使用了這些技巧,只需要很少的額外投入就可以在效能和可縮放性方面獲得很大的提高。