跳轉到內容

Visual Basic/最佳化 Visual Basic

來自 Wikibooks,開放世界的開放書籍

最佳化是使程式更快、更小、更簡單、更少資源佔用等的藝術和科學。當然,更快通常與更簡單和更小相沖突,因此最佳化是一個平衡行為。

本章旨在包含一個工具包,其中包含可用於加速某些型別程式的技術。每種技術都將附帶演示改進的程式碼。請注意,程式碼出於兩個原因故意不那麼理想:最佳化的程式碼幾乎總是更難理解,而且目的是讓讀者練習這些技術以獲得更深入的理解。您應該始終能夠重寫提供的程式碼以獲得更大的改進,但這有時會導致可讀性下降。在速度至關重要的程式碼中,優先選擇速度而不是清晰度,在速度不那麼重要的程式碼中,您應該始終選擇最易維護的風格;請記住,多年後來到您的程式碼的另一位程式設計師依賴於您使程式碼說出它的意思。當您最佳化程式碼時,您通常需要格外小心地添加註釋,以確保維護程式碼的人不會因為他不理解您為什麼使用看似過於複雜的方法而扔掉您辛苦賺來的速度提升。

如果您更改程式碼以提高速度,您應該編寫測試來證明新程式碼確實比舊程式碼快。通常這可以像在過程之前和之後記錄時間並取差值一樣簡單。在更改程式碼之前和之後進行測試,看看改進是否值得。

請記住,程式碼首先要能工作。讓它工作,然後讓它更快地工作。

緩慢獲得正確答案的程式碼幾乎總是優於快速獲得錯誤答案的程式碼。如果答案是較大答案的一小部分,以至於錯誤難以發現,這一點尤其如此。

透過編寫測試、驗證函式和斷言來確保事物正常工作。當您更改程式碼以使其更快時,測試、驗證函式和斷言的結果不應該改變。

整數和長整型

[edit | edit source]

在 VB6 中,通常情況下,所有整型變數都被宣告為 Long 的程式將比使用 Integer 的相同程式執行得更快。

使用

 Dim I as Long

不使用

 Dim I as Integer

但是,重要的是要意識到,速度的最終仲裁者是在實際情況下的效能測量;請參閱下面的測試部分。

使用 Integer 的唯一原因是在按引用將引數傳遞給無法更改的元件中定義的函式時。例如,VB6 GUI 引發的許多事件使用 Integer 引數而不是 Long。對於某些第三方 DLL 也是如此。大多數 Windows API 函式需要 *Long* 引數。

測試

[edit | edit source]

這裡有一個簡單的測試,說明在最佳化的世界中,事情並不總是像看起來那樣。該測試是一個簡單的模組,它執行兩個子例程。這兩個例程是相同的,只是在一箇中所有整型變數都被宣告為 Long,而在另一箇中所有整型變數都被宣告為 Integer。這些例程只是簡單地反覆遞增一個數字。當在我的計算機上在 VB6 IDE 中執行時,結果為(連續三次執行,單位為秒)

 Long         11.607
 Integer       7.220
 Long         11.126
 Integer       7.211
 Long         11.006
 Integer       7.221


這似乎與我關於 Long 比 Integer 更快的斷言相矛盾。但是,當編譯並執行 IDE 之外時,結果為

 Long         0.711
 Integer      0.721
 Long         0.721
 Integer      0.711
 Long         0.721
 Integer      0.721

請注意,在編譯後執行時,Long 和 Integer 的時間是無法區分的。這說明了關於最佳化和基準測試的幾個重要要點。

時間抖動
在搶佔式多工作業系統上對演算法進行計時始終是一個統計學練習,因為您無法(至少在 VB6 中不能)強制作業系統不中斷您的程式碼,這意味著使用這些測試的簡單技術進行計時包括在其他程式中或至少在系統空閒迴圈中花費的時間。
瞭解您要測量什麼
在 IDE 中測量的計時至少比編譯為原生代碼時測量的計時長十倍(在這種情況下,最佳化為 *快速* 程式碼,整數邊界檢查 *開啟*)。Long 和 Integer 的相對計時差異在編譯程式碼和解釋程式碼之間有所不同。

有關更多基準測試,請參閱 Donald Lessau 的優秀網站:VBSpeed.

以下是程式碼

 Option Explicit
 
 Private Const ml_INNER_LOOPS As Long = 32000
 Private Const ml_OUTER_LOOPS As Long = 10000
 
 Private Const mi_INNER_LOOPS As Integer = 32000
 Private Const mi_OUTER_LOOPS As Integer = 10000
 
 
 Public Sub Main()
   
   Dim nTimeLong As Double
   Dim nTimeInteger As Double
   
   xTestLong nTimeLong
   xTestInteger nTimeInteger
   
   Debug.Print "Long", Format$(nTimeLong, "0.000")
   Debug.Print "Integer", Format$(nTimeInteger, "0.000")
   
   MsgBox "   Long: " & Format$(nTimeLong, "0.000") & vbCrLf _
          & "Integer: " & Format$(nTimeInteger, "0.000")
   
 End Sub
 
 
 Private Sub xTestInteger(ByRef rnTime As Double)
   
   Dim nStart As Double
   
   Dim iInner As Integer
   Dim iOuter As Integer
   Dim iNum As Integer
   
   nStart = Timer
   For iOuter = 1 To mi_OUTER_LOOPS
     iNum = 0
     For iInner = 1 To mi_INNER_LOOPS
       iNum = iNum + 1
     Next iInner
   Next iOuter
   
   rnTime = Timer - nStart
   
 End Sub
 
 
 Private Sub xTestLong(ByRef rnTime As Double)
   
   Dim nStart As Double
   
   Dim lInner As Long
   Dim lOuter As Long
   Dim lNum As Long
   
   nStart = Timer
   For lOuter = 1 To ml_OUTER_LOOPS
     lNum = 0
     For lInner = 1 To ml_INNER_LOOPS
       lNum = lNum + 1
     Next lInner
   Next lOuter
   
   rnTime = Timer - nStart
   
 End Sub

字串

[edit | edit source]

如果您有許多執行大量字串連線的程式碼,您應該考慮使用字串構建器。字串構建器通常作為類提供,但原理非常簡單,不需要任何面向物件的封裝。

字串構建器解決的問題是反覆分配和釋放記憶體所使用的時間。這個問題的出現是因為 VB 字串被實現為指向記憶體位置的指標,並且每次您連線兩個字串時,實際上發生的是您為結果字串分配一個新的記憶體塊。即使新字串替換了舊字串,也會發生這種情況,如以下語句所示

 s = s & "Test"

分配和釋放記憶體的行為在 CPU 週期方面非常昂貴。字串構建器透過維護一個比實際字串更長的緩衝區來工作,這樣要新增到結尾的文字只需簡單地複製到記憶體位置。當然,緩衝區必須足夠長才能容納結果字串,因此我們首先計算結果的長度,並檢查緩衝區是否足夠長;如果不是,我們分配一個新的更長的字串。

字串構建器的程式碼可以非常簡單

 Private Sub xAddToStringBuf(ByRef rsBuf As String, _
                             ByRef rlLength As Long, _
                             ByRef rsAdditional As String)
   
   If (rlLength + Len(rsAdditional)) > Len(rsBuf) Then
     rsBuf = rsBuf & Space$(rlLength + Len(rsAdditional))
   End If
   Mid$(rsBuf, rlLength + 1, Len(rsAdditional)) = rsAdditional
   rlLength = rlLength + Len(rsAdditional)
   
 End Sub

此子例程取代了字串連線運算子(*&*)。請注意,還有一個額外的引數 *rlLength*。這是必需的,因為緩衝區的長度與字串的長度不同。

我們像這樣呼叫它

 dim lLen as long
 xAddToString s, lLen, "Test"

lLen 是字串的長度。如果您檢視下面的執行時間表,您可以看到,對於 Count 的值約為 100 時,簡單方法更快,但高於此,簡單連線的時間大致呈指數增長,而字串構建器的時間大致呈線性增長(在 IDE 中測試,1.8GHz CPU,1GB 記憶體)。您的機器上的實際時間會不同,但總體趨勢應該相同。

重要的是批判性地看待這樣的測量結果,並嘗試瞭解它們是否適用於您正在編寫的應用程式。如果您在記憶體中構建長文字字串,那麼字串構建器是一個有用的工具,但如果您只連線幾個字串,那麼本機運算子會更快更簡單。

測試

[edit | edit source]

反覆連線單詞 *Test* 會得到以下結果

時間(秒)
計數 簡單 構建器
10 0.000005 0.000009
100 0.000037 0.000043
1000 0.001840 0.000351
5000 0.045 0.002
10000 0.179 0.004
20000 0.708 0.008
30000 1.583 0.011
40000 2.862 0.016
50000 4.395 0.019
60000 6.321 0.023
70000 13.641 0.033
80000 27.687 0.035

ByRef 與 ByVal

[edit | edit source]

許多關於 VB 的書籍告訴程式設計師始終使用 ByVal。他們以 ByVal 既快又安全為理由。

您可以透過 測試 的結果看到 ByVal 稍微快一些。

空函式

 xTestByRef     13.4190000000017 
 xTestByVal     13.137999999999 

在函式中使用簡單的算術表示式

 xTestAddByRef  15.7870000000039 
 xTestAddByVal  15.3669999999984 

您還可以看到差異很小。在解釋編碼基準時要小心,即使是最簡單的基準也可能產生誤導,如果您認為存在瓶頸,那麼始終明智的做法是對實際程式碼進行分析。

另一個說法是 ByVal 更安全。通常解釋為,所有引數都以 ByVal 宣告的函式無法更改呼叫方變數的值,因為 VB 複製而不是使用指標。然後,程式設計師可以自由地以任何他或她認為合適的方式使用傳入的引數,特別是可以為它們分配新值而不干擾呼叫方中的原始值。

這種安全性的好處通常會被編譯器無法執行型別檢查的事實所抵消。在 Visual Basic Classic 中,變數在必要且可能的情況下會在字串數字型別之間自動轉換。如果引數以 ByVal 宣告,則呼叫方可以在期望Long的地方提供String。編譯器將默默地編譯必要的指令以將String轉換為Long並繼續。在執行時,您可能發現問題,也可能沒有發現問題。如果字串包含數字,則它將被轉換,程式碼將“工作”,如果它不包含數字,則程式碼將在呼叫函式的位置失敗。

 Function IntByVal(ByVal z as Double) as Long
   IntByVal = Int(z)
 End Function
 
 Function IntByRef(ByRef z as Double) as Long
   IntByRef = Int(z)
 End Function
 
 Dim s As String
 's = "0-471-30460-3"
 s = "0"
 Debug.Print IntByVal(s)
 Debug.Print IntByRef(s)
 

如果您嘗試編譯上面的程式碼片段,您將看到編譯器在該行停止

 Debug.Print IntByRef(s)

它突出顯示s並顯示一個訊息框,內容為“ByRef 引數型別不匹配”。如果您現在註釋掉該行並再次執行,您將不會收到錯誤。現在取消註釋此行

 s = "0-471-30460-3"

並註釋掉此行

 's = "0"

再次執行程式。現在程式失敗,但只在執行時失敗,並顯示“執行時錯誤 13:型別不匹配”。

道德是

  • 使用 ByRef 使編譯器在函式呼叫中對引數進行型別檢查,
  • 除非是輸出引數,否則永遠不要為函式的引數賦值。

檢視 編碼標準 章節中的 過程引數 部分,瞭解有關如何命名引數的一些建議,以便始終清楚哪些是輸入引數,哪些是輸出引數。

測試

[edit | edit source]

使用空函式

[edit | edit source]
 Option Explicit
 
 Private Const mlLOOPS As Long = 100000000
 Private mnStart As Double
 Private mnFinish As Double
 
 Public Sub main()
   xTestByRef 1#, 2#
   Debug.Print "xTestByRef", mnFinish - mnStart
   xTestByVal 1#, 2#
   Debug.Print "xTestByVal", mnFinish - mnStart  
 End Sub
   
 Private Sub xTestByRef(ByRef a As Double, ByRef b As Double)
   Dim lLoop As Long
   Dim n As Double
   mnStart = Timer
   For lLoop = 1 To mlLOOPS
     n = xByRef(a, b)
   Next lLoop    
   mnFinish = Timer
 End Sub
 
 Private Sub xTestByVal(ByVal a As Double, ByVal b As Double)  
   Dim lLoop As Long
   Dim n As Double
   mnStart = Timer
   For lLoop = 1 To mlLOOPS
     n = xByVal(a, b)
   Next lLoop
   mnFinish = Timer
 End Sub
 
 Private Function xByRef(ByRef a As Double, ByRef b As Double) As Double
 End Function
 
 Private Function xByVal(ByVal a As Double, ByVal b As Double) As Double
 End Function

使用簡單的算術

[edit | edit source]
 Attribute VB_Name = "modMain"
 Option Explicit
   
 Private Const mlLOOPS As Long = 100000000
 Private mnStart As Double
 Private mnFinish As Double
 
 Public Sub main()  
   xTestAddByRef 1#, 2#
   Debug.Print "xTestAddByRef", mnFinish - mnStart
   xTestAddByVal 1#, 2#
   Debug.Print "xTestAddByVal", mnFinish - mnStart
 End Sub
 
 Private Sub xTestAddByRef(ByRef a As Double, ByRef b As Double)
   Dim lLoop As Long
   Dim n As Double
   mnStart = Timer
   For lLoop = 1 To mlLOOPS
     n = xAddByRef(a, b)
   Next lLoop
   mnFinish = Timer
 End Sub
 
 Private Sub xTestAddByVal(ByVal a As Double, ByVal b As Double)  
   Dim lLoop As Long
   Dim n As Double
   mnStart = Timer
   For lLoop = 1 To mlLOOPS
     n = xAddByVal(a, b)
   Next lLoop
   mnFinish = Timer
 End Sub
 
 Private Function xAddByRef(ByRef a As Double, ByRef b As Double) As Double
   xAddByRef = a + b
 End Function
   
 Private Function xAddByVal(ByVal a As Double, ByVal b As Double) As Double
   xAddByVal = a + b
 End Function

集合

[edit | edit source]

集合是非常有用的物件。它們允許您編寫比其他情況更簡單的程式碼。例如,如果您需要儲存使用者提供的數字列表,您可能事先不知道會提供多少個數字。這使得使用陣列變得困難,因為您必須要麼分配一個不必要的大陣列,以確保沒有任何合理的使用者會提供更多,要麼必須隨著新數字的出現而不斷 Redim 陣列。

集合允許您避免所有這些,因為它會根據需要擴充套件。但是,這種便利性是在某些情況下執行時間增加的代價。對於許多程式(可能大多數程式)來說,這個價格是可以接受的,但有些程式執行時間太長,您必須嘗試從每一行程式碼中榨取最後一點效能。

這經常發生在進行科學計算的程式中(不要告訴我 C 會是更好的語言,因為 C 和所有其他語言都適用相同的約束和最佳化)。

集合是一種便捷工具的原因之一是,您可以使用字串作為鍵,並透過鍵而不是索引檢索專案。但是,每次您請求集合中的專案時,集合都必須先確定您是否提供了一個 Integer(或 Byte 或 Long)或一個 String,這當然需要一小段時間。如果您的應用程式沒有使用透過 String 鍵查詢專案的功能,那麼使用陣列而不是集合將獲得更快的程式,因為 VB 不需要浪費時間檢查您是否提供了一個 String 鍵而不是一個整數索引。

如果您想讓集合只儲存一個沒有鍵的專案列表,那麼您可以使用以下方式用陣列來模擬這種行為(注意,此程式碼不是最佳的,最佳化它留作學生的練習)。

 Public Sub Initialize(ByRef a() As Variant, _
                       ByRef Count As Long, _
                       ByRef Allocated As Long)
   Allocated = 10
   Redim a(1 to Allocated) 
   Count = 0
 End Sub
   
 Public Sub Add(ByRef NewItem as Variant, _
                ByRef a() As Variant, _
                ByRef Count As Long, _
                ByRef Allocated As Long)
   Count = Count + 1    
   If Allocated < Count then
     Allocated = Count
     Redim Preserve a(1 to Allocated)    
   End If
   a(Count) = NewValue
 End Sub

要使用上面的程式碼,您必須宣告一個 Variant 陣列和兩個 Long。呼叫 Initialize 子例程來啟動所有操作。現在,您可以根據需要呼叫 Add 子例程,陣列將根據需要擴充套件。

關於這個看似簡單的子例程,有一些需要注意的地方

  • 一旦陣列大小超過 10,每次新增專案時都會分配一個新的陣列(替換原始陣列),
  • 陣列的大小(記錄在 Allocated 變數中)始終與 Count 相同,當 Count 超過 10 時,
  • 沒有提供刪除專案的規定,
  • 專案按新增的順序儲存。
  • 所有引數都以 Byref 宣告

程式設計師不太可能想在生產程式碼中包含此確切的例程。一些原因是

  • NewItem 宣告為 Variant,但程式設計師通常知道型別,
  • 初始分配使用文字整數進行硬編碼,
  • 子例程的名稱過於簡單,它可能會與同一名稱空間中的其他子例程衝突。
  • 沒有提供刪除專案的規定。
  • 三塊獨立的資訊必須放在一起,但它們沒有繫結在一起。

模擬集合的效能取決於對 Add、Item 和 Remove 的呼叫頻率(參見 #Exercises)。最佳化後的版本必須針對其將被使用的用途進行最佳化。如果沒有透過鍵進行查詢,那麼就沒有必要為它提供函式,尤其沒有必要提供資料結構來使其高效。

練習

[edit | edit source]
  • 擴充套件本節中介紹的程式碼,以更詳細地模擬 Collection 類。實現 Item 和 Remove 方法,檢視 VB 幫助檔案以瞭解確切方法宣告的詳細資訊,但不要覺得有義務完全複製它們。
  • 編寫一個測試程式來練習所有功能並計時,以便您可以判斷何時使用 Collection,何時使用模擬版本。
  • 您應該能夠想到至少兩種不同的 Remove 實現。考慮不同實現的後果。
  • 編寫一個明確描述您的 Collection 類版本滿足的要求。
  • 比較內建的 Collection 類和新類的行為,注意任何差異,並舉例說明差異很重要和不重要的使用情況。
  • 如果您還沒有這樣做,請實現透過鍵查詢。
  • 解釋一下,為什麼使用完全相同的介面來模擬 Collection 類的所有功能不可能在至少一個特定用例中產生顯著的效能改進。

字典

[edit | edit source]

在 VB 中,字典實際上是由指令碼執行時元件提供的。它類似於集合,但它還提供了一種檢索所使用鍵的方法。此外,與集合不同,鍵可以是任何資料型別,包括物件。集合和字典的主要區別如下

  • 字典具有 Keys 和 Items 屬性,它們返回變體陣列
  • 鍵可以是任何資料型別,而不僅僅是字串
  • 使用 For Each 列舉將返回 Keys,而不是 Items
  • 有一個內建的 Exists 方法。

與集合一樣,字典的便利性有時會被它們的執行時開銷所抵消。字典經常被用作集合而不是集合的原因之一僅僅是因為 Exists 方法。這允許您避免覆蓋現有資料或避免嘗試訪問不存在的資料。但是,如果資料項數量很少,那麼簡單的線性搜尋可能更快。在這種情況下,少量可能是多達 50 個專案。您可以編寫一個簡單的類,可以用作字典,如下所示;注意,沒有嘗試複製字典的所有行為,這是有意為之的。

 'cDict.cls
 Option Explicit
 
 Private maItems() As String
 Private mlAllocated As Long
 Private mlCount As Long
 
 Public Sub Add(Key As String, Item As String)
   
   mlCount = mlCount + 1
   If mlAllocated < mlCount Then
     mlAllocated = mlAllocated + mlCount
     ReDim Preserve maItems(1 To 2, 1 To mlAllocated)
   End If
   maItems(1, mlCount) = Key
   maItems(2, mlCount) = Item
   
 End Sub
 
 Public Property Get Count() As Long
   Count = mlCount
 End Property
 
 Public Function Exists(Key As String) As Boolean
   
   Exists = IndexOf(Key)
   
 End Function
 
 Public Function IndexOf(Key As String) As Long
   
   For IndexOf = 1 To mlCount
     If maItems(1, IndexOf) = Key Then
       Exit Function
     End If
   Next IndexOf
   IndexOf = 0
   
 End Function
 
 Public Property Let Item(Key As String, RHS As String)
   
   Dim lX As Long
   lX = IndexOf(Key)
   If lX Then
     maItems(2,lX) = RHS
   Else
     Add Key, RHS
   End If
   
 End Property
 
 Public Property Get Item(Key As String) As String
   Item = maItems(2,IndexOf(Key))
 End Property
 
 Public Sub Remove(Key As String)
   
   Dim lX As Long
   lX = IndexOf(Key)
   maItems(1, lX) = maItems(1, mlCount)
   maItems(2, lX) = maItems(2, mlCount)
   mlCount = mlCount - 1
 
 End Sub
 
 Public Sub RemoveAll()
   mlCount = 0
 End Sub
 
 Public Sub Class_Initialize()
   mlAllocated = 10
   ReDim maItems(1 To 2, 1 To mlAllocated)
 End Sub
 

一個簡單的測試例程可以用來演示此類比 VB 字典在某些任務中更快。例如,將 32 個專案新增到字典中,然後再次刪除它們,使用 cDict 會更快,但是如果您將專案數量加倍,VB 字典會更好。道德是:為手頭的任務選擇正確的演算法。

以下是測試例程

 Option Explicit
 
 Public gsModuleName As String
 
 
 Private mnStart As Double
 Private mnFinish As Double
 
 Private Const mlCount As Long = 10000
 
 
 Public Sub main()
   
   Dim litems As Long
   litems = 1
   Do While litems < 100
     litems = litems * 2
     Debug.Print "items=", litems
     Dim lX As Long
     mnStart = Timer
     For lX = 1 To mlCount
       xTestDictionary litems
     Next lX
     mnFinish = Timer
     Debug.Print "xTestDictionary", "Time: "; Format$(mnFinish - mnStart, "0.000")
     
     mnStart = Timer
     For lX = 1 To mlCount
       xTestcDict litems
     Next lX
     mnFinish = Timer
     Debug.Print "xTestcDict     ", "Time: "; Format$(mnFinish - mnStart, "0.000")
   Loop
   
   
 End Sub
 
 
 
 Private Sub xTestDictionary(ByRef rlItems As Long)
   
   Dim d As Dictionary
   Set d = New Dictionary
   
   Dim lX As Long
   Dim c As Double
   For lX = 1 To rlItems
     d.Add Str$(lX), Str$(lX)
   Next lX
   For lX = 1 To rlItems
     d.Remove Str$(lX)
   Next lX
   
 End Sub
 
 
 Private Sub xTestcDict(ByRef rlItems As Long)
   
   Dim d As cDict
   Set d = New cDict
   
   Dim lX As Long
   Dim c As Double
   For lX = 1 To rlItems
     d.Add Str$(lX), Str$(lX)
   Next lX
   For lX = 1 To rlItems
     d.Remove Str$(lX)
   Next lX
   
 End Sub
 
 

以下是我電腦在 IDE 中的結果(秒)

 items=         2 
 xTestDictionary             Time: 1.602
 xTestcDict                  Time: 0.120
 items=         4 
 xTestDictionary             Time: 1.663
 xTestcDict                  Time: 0.200
 items=         8 
 xTestDictionary             Time: 1.792
 xTestcDict                  Time: 0.361
 items=         16 
 xTestDictionary             Time: 2.023
 xTestcDict                  Time: 0.741
 items=         32 
 xTestDictionary             Time: 2.504
 xTestcDict                  Time: 1.632
 items=         64 
 xTestDictionary             Time: 3.455
 xTestcDict                  Time: 4.046
 items=         128 
 xTestDictionary             Time: 5.387
 xTestcDict                  Time: 11.437

練習

[edit | edit source]
  • 編寫一個 cDict 類的版本,允許儲存其他資料型別,例如使用 Variant 引數。
  • 使用類似的測試例程檢查效能。
  • 編寫一個新的測試例程來執行其他測試。新類和 VB 字典的相對效能是否發生變化?
  • 如果你嘗試檢索或刪除一個不存在的專案會發生什麼?其行為與字典的行為相比如何?

除錯物件

[edit | edit source]

除錯物件有兩個方法

Print
將它的引數列印到立即視窗。
Assert
如果它的引數為false,則暫停程式。

這兩種方法只有在 IDE 中執行時才有效;至少這是傳統智慧。不幸的是,對於 Debug.Print 來說並非完全如此。當程式作為編譯後的可執行檔案執行時,此方法不會列印任何內容,但如果引數是函式呼叫,它們仍然會被計算。如果函式呼叫非常耗時,你會發現編譯後的版本執行速度不如預期快。

可以做兩件事

  • 在釋出產品之前刪除所有 Debug.Print 語句。
  • 只使用變數或常量作為 Debug.Print 的引數

如果你的程式既非常 CPU 密集又處於持續開發中,那麼第二種方法可能更可取,這樣你就不必不斷新增和刪除行。

Debug.Assert 不會遇到這個問題,因此完全可以安全地斷言複雜且耗時的函式的真值。斷言引數只有在 IDE 中執行時才會被計算。

練習

[edit | edit source]
  • 編寫一個簡短的程式來演示 Debug.Print 即使在編譯後也會計算函式。
  • 修改它以顯示 Debug.Assert 不會遇到這個問題。
  • 顯示如果引數是常量或變數,則在編譯後的版本中 Debug.Print 的執行時間為零。

物件例項化

[edit | edit source]

對於簡單的概念來說,這些詞很長。本節討論建立物件的執行時成本。這個術語的專業術語是物件例項化,意思是建立一個例項。一個例項與一個的關係就像一臺機器與它的計劃的關係。對於任何給定的類,你可以有任意數量的例項。

如果一個物件的構建需要很長時間,並且你在程式執行期間建立和銷燬了大量的物件,那麼你可以透過不銷燬它們而是將它們放在一個預製物件列表中以備後用,從而節省一些時間。

想象一個模擬動物生態的程式。可能會有兩種動物類:食草動物和食肉動物。

如果你想在有生之年看到任何結果,模擬顯然必須比現實生活速度更快,所以大量的食草動物尤其會出生、繁殖和被吃掉。如果這些動物中的每一個都由一個物件來表示,並且在它所表示的動物被殺死時被銷燬,那麼系統將反覆分配和釋放記憶體,這在 VB6 中是一項相對昂貴的業務。在這樣的程式中,你知道需要不斷建立相同型別的物件,所以你可以透過重用死亡的物件來避免一些記憶體分配開銷,而不是銷燬它們並建立新的物件。有很多方法可以做到這一點。你選擇哪一種取決於你有多少種不同的物件類。如果很少,那麼你可以為每個物件建立單獨的程式碼,這樣就可以非常高效地調整程式碼。另一方面,如果你有數十種不同的類(也許你已經將模擬擴充套件到包括競爭的食草動物),你會很快發現你遇到了維護問題。

解決方案是建立一個描述通用物件池的類和一個介面,每個類都可以實現該介面。

在我描述這個類和介面之前,這裡是對需求的總結

  • 只需要一個池類。
  • 被“池化”的類只需要對初始化和終止程式碼進行一些小的修改以適應池化概念。
  • 使用池化物件的程式碼不需要更改,除了呼叫池的 GetObject 函式而不是使用 New 運算子。

這項技術依賴於 VB6 具有確定性終結這一事實。另一個專業術語,它只是意味著 VB6 會在物件變為未使用時立即銷燬(終結)它們。每次 VB6 確定一個物件不再使用時,它都會呼叫該物件的 Class_Terminate 方法。我們可以做的是在每個類中新增一段簡單的程式碼,將終止的物件放到可用物件列表中。VB 將看到該物件現在正在使用,並且不會釋放它所使用的記憶體。稍後,我們不是使用New 運算子建立新物件,而是請求物件池提供一個不再使用的物件。即使該物件的設定時間非常短,這也會比使用New 快,因為它避免了記憶體分配和釋放。

這裡是一個示例物件池類和一個可以使用它的類

 'ObjectPool.cls
 Private moPool as Collection
 Public oTemplate As Object
 
 Private sub Class_Initialize()
   set moPool = New Collection
 End Sub
 
 Public Function GetObject() as Object
   if moPool.Count then
     Set GetObject = moPool(1)
     moPool.Remove(1)
   Else
     Set GetObject = oTemplate.NewObject
   End If
 End Function
 
 Public Sub ReturnToPool(Byref oObject as Object)
   moPool.Add oObject
 End Sub

要使用這個類,在bas 模組中宣告一個公共變數,併為它分配一個新的 ObjectPool 物件

 'modPool.bas
 Public goPool As ObjectPool
 
 Public Sub Initialize()
   Set goPool = New ObjectPool
   Set goPool.oTemplate = New Herbivore
 End Sub

現在修改你的 Herbivore 類,透過在 Class_Terminate 方法中新增對 ReturnToPool 的呼叫,並新增一個 NewObject 方法

 Private Sub Class_Terminate()
   goPool.ReturnToPool Me
 End Sub
 
 Public Function NewObject() as Object
   Set NewObject = New Herbivore
 End Function

對於一些簡單的場景,這可能甚至有效。但是,它有幾個缺陷,至少有一個是主要問題。問題在於你得到的物件不一定像一個閃亮的新物件。現在對於某些應用程式來說,這並不重要,因為客戶端程式碼會重新初始化所有內容,但是許多程式依賴於該語言的自動將新分配的變數設定為零的功能。為了滿足對客戶端最小更改的要求,我們應該透過在 Herbivore 中新增 ReInitialize 方法來擴充套件 ObjectPool 和池化物件

 Public Sub ReInitialize()
   ' Do whatever you need to do to make the object 
   ' look like new (reset some attributes to zero, 
   ' empty strings, etc).
 End Sub

不要在ReInitialize 中做任何不必要的工作。例如,如果物件的屬性之一是動態分配的陣列,那麼可能不需要清除它;設定一個標誌或計數器以指示實際上沒有使用任何陣列元素就足夠了。

現在修改 ObjectPool 的 GetObject 方法

 Public Function GetObject() as Object
   if moPool.Count then
     Set GetObject = moPool(1)
     GetObject.ReInitialize
     moPool.Remove(1)
   Else
     Set GetObject = oTemplate.NewObject
   End If
 End Function

現在,在你使用 New Herbivore 的所有地方,都使用 goPool.GetObject 代替。如果 Herbivore 物件引用了其他物件,你可能需要(也可能不需要)透過在 Class_Terminate 方法中將它們設定為 Nothing 來釋放這些引用。這取決於物件的 behaviour 和程式的其餘部分,一般來說,你應該儘可能推遲執行昂貴的操作。

使用物件池可以提高某些型別程式的效能,而無需程式設計師徹底改變程式設計。但是,不要忘記,你也許可以透過使用更好的演算法來獲得類似或更大的改進;同樣,計時測試是瞭解該領域的關鍵。不要假設你知道瓶頸在哪裡,透過分析程式來證明它在哪裡。

練習

[edit | edit source]
  • 編寫一個簡單的程式,建立和銷燬大量物件,並計時。現在修改它以使用 ObjectPool。
  • 為池化物件定義一個介面並實現它。消除As Object 的使用是否會改變效能?
  • 注意,moPool 集合僅僅用作 FIFO 棧,即沒有利用透過鍵查詢專案的 capability。是否有更快的替代方案?
  • 棧的 FIFO 行為是否重要,也就是說,它是刻意設計的特性還是僅僅無關緊要?

一般提示和技巧

[edit | edit source]

儘可能將程式碼移出迴圈

[edit | edit source]

迴圈始終是你程式碼中最重要要最佳化的部分。始終嘗試將盡可能多的程式碼移出迴圈。這樣程式碼就不會重複,可以節省一些 CPU 週期。一個簡單的例子

 For i = 1 to 50
   x = b		' Stays the same with every loop, get it outside of the loop!
   k = j + i
 Next i

更改為

 x = b	'Moved outside the loop
 For i = 1 to 50
   k = j + i
 Next i

這似乎顯而易見,但你會驚訝地發現有多少程式設計師這樣做。簡單的規則是,如果它沒有在每次迴圈迭代中改變,那麼就將其移出迴圈,因為它不需要在那裡。你只想在迴圈中包含必須在那裡的程式碼。你可以從迴圈中清除的指令越多,我們就可以執行得越快。

迴圈展開

[edit | edit source]

迴圈展開可以消除一些比較和跳轉指令。(比較和跳轉指令用於建立迴圈,你在 Visual Basic 中看不到它們,它們是你在 ASM 中學習的幕後內容。)它還利用了現代 CPU 可以一次獲取多個指令的能力。簡而言之,透過展開迴圈,你可以獲得良好的速度提升。

但我們需要注意迴圈展開的一些問題。現代計算機最大的瓶頸是記憶體。因此,英特爾和 AMD 等 CPU 設計者透過在他們的 CPU 上使用快取來解決這個問題。這基本上是一個記憶體位置,CPU 可以比標準記憶體更快地訪問它。你希望展開的迴圈能完全放入快取中,如果不能,它可能會降低你的程式碼速度。因此,你可能需要在展開迴圈時嘗試使用 gettickcount 函式。

迴圈示例

 For i = 1 To 100
   b = somefun(b)
 Next I

展開的示例

 For i = 1 To 100 Step 2
   b = somefun(b)
   b = somefun(b)
 Next I

根據你的操作,你可以獲得高達 25% 的速度提升,你只需要進行試驗。

儘量避免使用除法

[編輯 | 編輯原始碼]

除法指令是 CPU 上最昂貴的指令之一,如果不是最昂貴的。乘法比除法速度更快!

 B = 40 / 2

 B = 40 * 0.5

更慢。你也可以使用減法來開發一些有趣的演算法來獲得結果,這些演算法比使用除法快得多。如果你在迴圈中使用除法,必須將其更改以加快程式碼速度。(我原本也想建議嘗試使用位移運算進行除法,但我忘記了某些版本的 Visual Basic 不包含位移運算子)。

巢狀條件

[編輯 | 編輯原始碼]

在巢狀條件分支中,例如 Select Case 和巢狀的 If 語句,將最有可能為真的部分放在巢狀中的最前面,將最不可能為真的部分放在最後面。

避免使用 Variant 變數

[編輯 | 編輯原始碼]

Variant 變數在剛開始學習 Visual Basic 時非常方便,但這是一個需要改掉的習慣。Variant 變數在執行時會轉換為其適當的資料型別,這可能非常昂貴。

宣告變數時要小心

[編輯 | 編輯原始碼]

如果你在宣告每個變數時沒有使用 as something,那麼它就是一個 Variant!例如

 Dim a, b, c as string.
   a = A   'variant
   b = A   'variant
   c = A   'string

我看到有些人使用這種表示法

 Dim x
 x = blah

這是絕對不行的!它可能有效,但會降低你的速度。

減少公共表示式

[編輯 | 編輯原始碼]

有時,你的兩個不同變數會使用相同計算的一部分。與其對兩個變數執行完整的計算,不如消除冗餘計算。

 x = a * b + c
 y = a * b + d

 t = a * b
 x = t + c
 y = t + d

如果你的冗餘昂貴計算在迴圈中使用,尤其如此。

在計算中使用 Long 或 Integer

[編輯 | 編輯原始碼]

Long 是一個 32 位數字,在 32 位處理器上更自然。避免使用其他變數,例如 Double、Single 等。

在迴圈中使用行內函數

[編輯 | 編輯原始碼]

與其呼叫函式,不如將程式碼放在迴圈中。如果你的程式碼在足夠多的迴圈中重複,這會使你的程式變大,並且只應該在關鍵位置執行此操作。原因是呼叫函式的開銷。在程式呼叫函式之前,它必須將某些東西推送到堆疊中。至少它會推送指令指標(即返回地址)。記憶體訪問速度很慢,因此我們希望在關鍵位置避免這種情況。

避免在迴圈中使用屬性

[編輯 | 編輯原始碼]

屬性的訪問速度比變數慢得多,因此使用變數代替。

 For i = 1 to 50
    text1.text = text1.text + b(i)
 Next i

 For i = 1 to 50
   strbuffer = strbuffer + b(i)
 Next i
 text1.text = strbuffer

從磁碟載入所有需要的資料

[編輯 | 編輯原始碼]

與其一次載入一個檔案,不如一次載入所有檔案。這將避免使用者以後的延遲。

充分利用 Timer 控制元件

[編輯 | 編輯原始碼]

你可以在等待使用者時進行後臺處理。利用這段時間預取資料、進行需要的計算等等。

儘量減少物件中的點表示法

[編輯 | 編輯原始碼]

你在物件中使用的每個點都會讓 Visual Basic 進行一次呼叫。

 Myobject.one.two.three

 Myobject.one

一次分配足夠的記憶體

[編輯 | 編輯原始碼]

當你建立動態陣列,並且想要新增尚未分配的元素時,確保為所有元素分配足夠的記憶體,而不是一次分配一個。如果你不知道需要多少元素,將你分配的記憶體乘以 2。分配記憶體是一個昂貴的過程。

避免在迴圈中使用內建函式

[編輯 | 編輯原始碼]

如果你有一個需要字串長度的迴圈演算法,確保將字串大小快取到緩衝區中,而不是在每次迴圈迭代中呼叫函式 len()。

 For i = 1 to 100
   sz = len(string)
   'Do processing
 Next i

 sz = len(string)
 For i = 1 to 100
   'Do Processing with sz
 Next i

慢得多。

[編輯 | 編輯原始碼]

每次更新控制元件屬性時,都會使其重新繪製。因此,如果你正在開發顯示覆雜圖形的內容,可能需要減少這種情況的發生次數。

上一個:有效程式設計 目錄 下一個:示例
華夏公益教科書