返回文章列表

Delphi記憶體管理與多執行緒最佳化

本文探討 Delphi 記憶體管理與多執行緒最佳化策略,比較內建記憶體管理器、FastMM4、FastMM5 和 TBBMalloc 的效能差異,並提供程式碼範例與圖表說明,幫助開發者提升程式效能。從自訂記憶體管理器到整合第三方函式庫,文章涵蓋了多種最佳化技巧,並以 SlowCode

Delphi 效能調校

在 Delphi 開發高效能應用程式時,記憶體管理和多執行緒程式設計至關重要。Delphi 內建的記憶體管理器在多執行緒環境下可能存在效能瓶頸,因此需要考慮使用 FastMM4、FastMM5 或 TBBMalloc 等第三方記憶體管理器進行最佳化。FastMM4 透過釋放堆積疊機制提升記憶體釋放效率,FastMM5 則提供更多組態選項以平衡速度和記憶體使用效率,而 TBBMalloc 作為 Intel oneTBB 的一部分,也展現了高效的記憶體管理能力。選擇合適的記憶體管理器需要根據實際應用場景進行測試和比較。此外,預先分配記憶體空間、避免頻繁的記憶體重新分配等技巧,也能有效提升程式效能。理解多執行緒程式設計中的行程、執行緒概念以及同步、互鎖等機制,是開發高效能多執行緒應用程式的關鍵。

記憶體管理最佳化:自訂與第三方記憶體管理器

在開發高效能的多執行緒應用程式時,記憶體管理是一個至關重要的議題。Delphi 內建的記憶體管理器在某些情況下可能無法滿足高效率的需求,因此瞭解如何替換或最佳化記憶體管理器就顯得尤為重要。

自訂記憶體管理器

為了更好地理解記憶體管理的內部工作原理,我們可以建立一個自訂的記憶體管理器。下面是一個簡單的範例,展示瞭如何記錄記憶體分配的相關資訊:

procedure Write(n: NativeUInt); overload;
var
  buf: array [1..18] of AnsiChar;
  i: Integer;
  digit: Integer;
begin
  buf[18] := #0;
  for i := 17 downto 2 do
  begin
    digit := n mod 16;
    n := n div 16;
    if digit < 10 then
      buf[i] := AnsiChar(digit + Ord('0'))
    else
      buf[i] := AnsiChar(digit - 10 + Ord('A'));
  end;
  buf[1] := '$';
  Write(@buf);
end;

內容解密:

  1. 函式目的:此函式將 NativeUInt 型別的值轉換為十六進位字串並寫入緩衝區。
  2. 實作細節:使用迴圈將數值轉換為十六進位表示,並存入 buf 陣列中。最後在 buf 的第一個位置寫入 $ 符號,表示這是一個十六進位數值。
  3. 輸出:呼叫 Write(@buf) 將轉換後的十六進位字串輸出。

然而,這種自訂實作存在多執行緒環境下的執行緒安全問題,並且頻繁的 WriteFile 呼叫會導致效能問題。解決方案包括新增鎖定機制以確保執行緒安全,以及使用緩衝區來減少 I/O 操作。

使用 FastMM4 最佳化記憶體管理

FastMM4 是一個開源的記憶體管理器,能夠提供比 Delphi 內建記憶體管理器更好的效能。以下是如何使用 FastMM4 的步驟:

  1. 下載並整合 FastMM4:從 GitHub 下載 FastMM4 原始碼,並將其新增至專案中。

    program ParallelAllocation;
    uses
      FastMM4 in 'FastMM\FastMM4.pas',
      Vcl.Forms,
      ParallelAllocationMain in 'ParallelAllocationMain.pas' {frmParallelAllocation};
    
  2. 啟用記憶體管理器日誌記錄:定義 LogLockContention 條件符號,並重新編譯專案。

圖表翻譯:

此圖展示了使用 FastMM4 日誌記錄功能後,程式執行的鎖定競爭情況。從圖中可以看出,程式在哪些記憶體操作上花費了最多的時間。

圖表解說

圖表翻譯: 此圖示展示了記憶體分配的基本流程,包括檢查記憶體池、分配記憶體以及在必要時呼叫系統 API。

替換預設記憶體管理器的探討

在多執行緒程式設計中,記憶體管理是一個至關重要的議題。預設的記憶體管理器在某些情況下可能會成為程式效能的瓶頸。本文將探討替換預設記憶體管理器的必要性、相關技術方案及其效能比較。

為何替換預設記憶體管理器?

預設的記憶體管理器在單執行緒環境下表現良好,但在多執行緒環境下,由於對記憶體分配和釋放的競爭,可能會導致效能下降。FastMM4 是一個廣泛使用的替代記憶體管理器,它針對多執行緒環境進行了最佳化。

FastMM4 的最佳化

FastMM4 透過引入「釋放堆積疊(release stack)」的概念來最佳化記憶體釋放的過程。當 FreeMem 無法鎖定分配器時,它將待釋放的記憶體區塊位址存入釋放堆積疊中,並快速離開。當成功鎖定分配器後,它不僅釋放自己的記憶體區塊,也會處理釋放堆積疊中的待釋放區塊。

程式碼範例:FastMM4 釋放堆積疊的使用

// 啟用釋放堆積疊
{$DEFINE UseReleaseStack}
// 在專案設定中移除 LogLockContention 定義

內容解密:

  • UseReleaseStack 定義用於啟用 FastMM4 的釋放堆積疊功能。
  • 移除 LogLockContention 定義是因為它會降低程式效能。

FastMM5:新一代記憶體管理器

FastMM5 是 FastMM4 的後繼者,提供了更好的多執行緒效能和執行時的組態功能。它支援三種不同的模式:速度優先、記憶體使用效率優先和平衡模式。FastMM5 可以從其 GitHub 頁面下載。

FastMM5 與 FastMM4 的效能比較

圖表翻譯: 此圖示呈現了 FastMM4 和 FastMM5 的效能比較,顯示了兩者在不同場景下的表現。

TBBMalloc:Intel 的高效記憶體管理器

TBBMalloc 是 Intel oneAPI Threading Building Blocks (oneTBB) 函式庫的一部分,提供了一個高效的記憶體管理器。它採用 Apache 2.0 授權,允許在開源和商業應用中使用。

程式碼範例:使用 TBBMalloc

// cmem.pas
function scalable_getmem(Size: nativeUInt): Pointer; cdecl;
external 'tbbmalloc' name 'scalable_malloc';

// 在專案中使用 TBBMalloc
program ParallelAllocation;
uses
  cmem in 'tbbmalloc\cmem_xe\cmem.pas',
  Vcl.Forms,
  ParallelAllocationMain in 'ParallelAllocationMain.pas';

內容解密:

  • scalable_getmem 函式是 TBBMalloc 提供的記憶體分配介面。
  • 在專案中使用 TBBMalloc 需要包含 cmem.pas 單元。

不同記憶體管理器的比較

透過比較不同記憶體管理器在 ParallelAllocation 測試中的表現,我們可以看到它們在單執行緒和多執行緒環境下的效能差異。

不同記憶體管理器的效能比較

圖表翻譯: 此圖示展示了不同記憶體管理器在單執行緒和多執行緒環境下的效能比較,揭示了它們在不同場景下的表現差異。

記憶體管理深度解析

在軟體開發領域中,記憶體管理是一項至關重要的技術,尤其是在高效能運算與多執行緒程式設計中。本章節將探討Delphi中的記憶體管理機制,並透過例項分析最佳化記憶體使用的方法。

從字串與陣列談起

Delphi中的字串與動態陣列是由編譯器自動管理的,這意味著開發者無需手動分配與釋放記憶體。然而,這種自動管理機制在某些情況下可能導致效能問題。舉例來說,逐一元素擴充字串或陣列會引發頻繁的記憶體重新分配,嚴重影響程式效能。

最佳實踐:預先分配記憶體

為避免效能問題,建議在已知資料大小的情況下,預先分配足夠的記憶體空間。這種做法可以顯著減少記憶體重新分配的次數,從而提升程式效能。

記憶體管理函式詳解

Delphi提供了一系列記憶體管理函式,用於手動分配、調整和釋放記憶體。這些函式主要分為兩類別:

  1. 一般記憶體管理函式:用於分配和釋放原始記憶體區塊。
  2. 受控資料管理函式:專門用於管理字串、介面和動態陣列等受控資料型別。

正確使用這些函式對於最佳化記憶體使用至關重要。

記錄型別與動態記憶體分配

記錄(Record)是一種值型別,在Delphi中廣泛用於資料儲存。相較於物件,記錄具有建立快速、記憶體佔用少等優勢。結合泛型技術,記錄可以靈活地與動態記憶體分配機制配合使用。

深入理解FastMM4記憶體管理器

FastMM4是Delphi預設的記憶體管理器,其內部實作複雜且高效。透過對FastMM4工作原理的剖析,我們可以更好地理解其在單執行緒與多執行緒環境下的表現。

多執行緒環境下的效能挑戰

在多執行緒程式設計中,FastMM4可能會成為效能瓶頸。為此,我們需要掌握檢測與最佳化多執行緒環境下記憶體管理效能的方法。

替換預設記憶體管理器

Delphi允許開發者替換預設的記憶體管理器,以適應特定應用場景的需求。本章節展示瞭如何建立一個記錄記憶體操作的日誌包裝器,並介紹了FastMM5和TBBMalloc這兩種替代記憶體管理器。

為何不總是替換FastMM4?

儘管其他記憶體管理器在某些方面表現更優,但FastMM4強大的除錯功能使其成為開發階段的理想選擇。除非經過充分測試,否則不建議在生產環境中輕易替換記憶體管理器。

案例分析:最佳化SlowCode程式

本章節以SlowCode程式為例,展示瞭如何透過最佳化記憶體使用來提升程式效能。透過預先分配適當大小的陣列,SlowCode_Sieve_v3版本在處理1億個數字時僅比v2版本快了微小的幅度,這表明記憶體重新分配並非該程式的主要效能瓶頸。

進入平行世界的第一步

如果你從頭到尾仔細閱讀這本文,一章都不漏掉,那麼你已經學到了很多東西。我討論了演算法、最佳化技術、記憶體管理等,但我刻意迴避了平行程式設計(或稱為多執行緒程式設計)。

我這樣做是有很好的理由的。平行程式設計是困難的。不管你是一個優秀的程式設計師,也不管支援工具有多好,平行程式設計總是會給程式帶來很多奇怪的錯誤:這些錯誤難以重現,也很難被發現。這就是為什麼我想讓你先探索其他選項。

如果你的程式可以在不採用平行程式設計的情況下達到足夠的效能,那麼就這樣做吧!傳統的非平行(單執行緒)程式碼總是比平行程式碼包含更少的錯誤和隱藏的陷阱。

有時這是不可能的,你必須在程式碼中引入平行程式設計技術。要成功做到這一點,不僅僅需要知道如何進行平行程式設計,更重要的是要知道在平行程式碼中絕對不能做什麼。

本章涵蓋的主題

  • 什麼是平行程式設計和多執行緒?
  • 平行程式碼中最常見的錯誤原因是什麼?
  • 我們應該如何在平行世界中處理使用者介面?
  • 什麼是同步,為什麼它既好又壞?
  • 什麼是互鎖,何時它比同步更好?
  • 我們如何從畫面中移除同步?
  • Delphi 為程式設計師提供了哪些通訊工具?
  • 還有哪些其他函式庫可以幫助你?

技術需求


Second-Edition/tree/main/ch7。

行程和執行緒

作為一名程式設計師,你可能已經對行程(process)有一定的瞭解。在作業系統看來,行程大致相當於一個應用程式。當使用者啟動一個應用程式時,作業系統會建立並啟動一個新的行程。該行程擁有應用程式的程式碼和該程式碼使用的所有資源——記憶體、檔案控制程式碼、裝置控制程式碼、通訊端、視窗等。

當程式正在執行時,系統還必須追蹤當前的執行位址、CPU 暫存器的狀態和程式堆積疊的狀態。然而,這些資訊並不是行程的一部分,而是屬於該行程的執行緒的一部分。即使是最簡單的程式也至少使用一個執行緒。

換句話說,行程代表了程式的靜態資料,而執行緒代表了動態的部分。在程式的生命週期中,執行緒描述了它的執行線路。如果我們知道執行緒在每個時刻的狀態,我們就可以完全重建執行的所有細節。

多執行緒

所有作業系統都支援每個行程至少一個執行緒(主執行緒),但有些作業系統更進一步,支援一個行程中的多個執行緒。實際上,大多數現代作業系統都支援多執行緒,這種方法被稱為多執行緒。

透過多執行緒,作業系統管理多個透過相同程式碼的執行路徑。這些路徑可以同時執行(也可以不同時執行——但這方面的內容稍後再談)。

注意

當程式啟動時建立的預設執行緒稱為主執行緒。之後建立的其他執行緒稱為工作執行緒或背景執行緒。

在大多數作業系統(包括 Windows、OS X、iOS 和 Android)中,行程是很重的。建立和載入一個新的行程需要很長的時間(至少在作業系統層面上,一切都是以微秒來衡量的)。

相比之下,執行緒是很輕的。新的執行緒幾乎可以立即建立——作業系統所要做的就是為堆積疊分配一些記憶體,並設定一些核心使用的控制結構。

行程和執行緒的重要區別

另一個重要的點是,行程是隔離的。作業系統盡力將一個行程與另一個行程分開,以便一個行程中的錯誤(或惡意)程式碼不能使另一個行程當機(或讀取其私有資料)。然而,執行緒並沒有這種保護。

如果你足夠年長,記得 Windows 3 那個時代,當時情況並非如此,你一定能體會這種隔離帶給使用者的穩定性。相比之下,一個行程中的多個執行緒分享該行程的所有資源——記憶體、檔案控制程式碼等。因此,多執行緒本質上是很脆弱的——一個執行緒中的錯誤很容易導致另一個執行緒當機。

多工

起初,作業系統是單工的。換句話說,一次只能執行一個任務(即行程),只有當它完成工作(當任務終止)時,才能排程新的任務(啟動)。

一旦硬體足夠快,多工就被發明出來。大多數電腦仍然只有一個處理器,但透過作業系統的神奇操作,看起來這個處理器正在同時執行多個程式。

每個程式都被給予一小段時間來完成它的工作。之後,它被暫停,另一個程式取而代之。經過一段不確定的時間(取決於系統負載、更高優先順序任務的數量等),該程式可以再次執行,作業系統將從它被暫停的位置繼續執行它,同樣只給它一小段時間。

有多工的兩種非常不同的方法。在協作式多工中,行程本身告訴作業系統什麼時候它準備好被暫停。這簡化了作業系統,但給寫得不好的程式提供了破壞整個電腦的機會。還記得 Windows 3 嗎?那就是協作式多工的最糟糕表現。

程式碼範例1:

// 使用TThread.CreateAnonymousThread建立匿名執行緒 TThread.CreateAnonymousThread(procedure begin // 模擬耗時操作 Sleep(1000); // 在主執行緒中更新UI TThread.Synchronize(nil, procedure begin // 更新UI操作 Button1.Enabled := True; end); end).Start;

內容解密:

此範例展示瞭如何使用 TThread.CreateAnonymousThread 建立匿名執行緒,並在其中模擬耗時操作。在耗時操作完成後,透過 TThread.Synchronize 方法將更新 UI 的操作同步到主執行緒中,以避免跨執行緒操作 UI 元件導致的問題。

  1. TThread.CreateAnonymousThread:此方法用於建立匿名執行緒。它接受一個無引數的匿名方法,在該方法內部可以寫入需要在背景執行的程式碼。
  2. Sleep(1000):模擬耗時操作,例如網路請求或大型資料處理。這裡使用了 Sleep 函式來暫停當前執行緒1秒鐘。
  3. TThread.Synchronize:用於將某些操作同步到主執行緒。這裡用於更新 UI 元件,因為 Delphi 的 UI 元件不是執行緒安全的,不能直接在背景執行緒中更新它們。
  4. Button1.Enabled := True;:將按鈕重新啟用,這是在主執行緒中安全地更新 UI 元件的操作。

行程與執行緒關係圖

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Delphi記憶體管理與多執行緒最佳化

package "資料視覺化流程" {
    package "資料準備" {
        component [資料載入] as load
        component [資料清洗] as clean
        component [資料轉換] as transform
    }

    package "圖表類型" {
        component [折線圖 Line] as line
        component [長條圖 Bar] as bar
        component [散佈圖 Scatter] as scatter
        component [熱力圖 Heatmap] as heatmap
    }

    package "美化輸出" {
        component [樣式設定] as style
        component [標籤註解] as label
        component [匯出儲存] as export
    }
}

load --> clean --> transform
transform --> line
transform --> bar
transform --> scatter
transform --> heatmap
line --> style --> export
bar --> label --> export

note right of scatter
  探索變數關係
  發現異常值
end note

@enduml

圖表翻譯: 此圖表展示了一個行程內包含多個執行緒的關係,包括一個主執行緒和多個工作執行緒。這些執行緒分享同一個行程中的資源,如記憶體和檔案控制程式碼等。

更好的方法是搶佔式多工,其中每個行程都被分配了一定的時間(通常在 Windows 中約為幾十毫秒),然後被搶佔;也就是說,硬體計時器觸發,從行程中接管控制權,並將其交還給作業系統,然後作業系統可以排程下一個行程。

這種方法被用於目前的 Windows、macOS 和所有其他現代桌面和行動作業系統。這樣,多工系統即使只有一個處理器核心,也可以看起來同時執行多個行程。如果電腦內有多個核心,則多個行程可以真正同時執行。

同樣適用於執行緒。單工系統預設僅限於每個行程一個執行緒。有些多工系統是單執行緒的(也就是說,它們每個行程只能執行一個執行緒),但所有現代作業系統都支援多執行緒——它們可以在一個行程內執行多個執行緒。我對多工的所有說明同樣適用於執行緒。實際上,被排程的是執行緒,而不是行程。