簡體   English   中英

多線程C ++程序的性能不佳

[英]Poor performance in multi-threaded C++ program

我有一個在Linux上運行的C ++程序,在該程序中創建一個新線程來完成一些獨立於主線程的計算上昂貴的工作(計算工作通過將結果寫入文件來完成,最終會非常大)。 但是,我的表現相對較差。

如果我直接實現程序(不引入其他線程),它將在大約2小時內完成任務。 使用多線程程序,執行相同的任務大約需要12個小時(僅在生成一個線程的情況下進行了測試)。

我已經嘗試了一些東西,包括pthread_setaffinity_np將線程設置為單個CPU(在我正在使用的服務器上可用的24個),以及pthread_setschedparam來設置調度策略(我只嘗試過SCHED_BATCH) )。 但到目前為止,這些影響可以忽略不計。

這種問題是否有任何一般原因?

編輯:我添加了一些我正在使用的示例代碼,希望是最相關的部分。 函數process_job()實際上是計算工作的功能,但這里包含的內容太多了。 基本上,它讀入兩個數據文件,並使用它們在內存中的圖形數據庫上執行查詢,其中結果在幾個小時內寫入兩個大文件。

編輯第2部分:只是為了澄清,問題不在於我想使用線程來提高我所擁有的算法的性能。 但是,我想同時運行我的算法的許多實例。 因此,我希望算法在放入線程時以類似的速度運行,就像我根本不使用多線程一樣。

編輯第3部分:感謝所有建議。 我正在進行一些單元測試(看哪些部件正在減速),正如一些人所建議的那樣。 由於程序需要一段時間才能加載和執行,因此需要花時間查看測試中的任何結果,因此我對遲到的響應表示道歉。 我認為我想澄清的主要觀點是線程可能導致程序運行緩慢的可能原因。 從我從評論中收集的內容來看,它根本就不應該。 當我能找到合理的分辨率時,我會發布,再次感謝。

(最終)編輯第4部分:事實證明問題與線程無關。 在這一點上描述它會過於繁瑣(包括使用編譯器優化級別),但是這里發布的想法非常有用和受到贊賞。

struct sched_param sched_param = {
    sched_get_priority_min(SCHED_BATCH)
};

int set_thread_to_core(const long tid, const int &core_id) {
   cpu_set_t mask;
   CPU_ZERO(&mask);
   CPU_SET(core_id, &mask);
   return pthread_setaffinity_np(tid, sizeof(mask), &mask);
}

void *worker_thread(void *arg) {
   job_data *temp = (job_data *)arg;  // get the information for the task passed in
   ...
   long tid = pthread_self();
   int set_thread = set_thread_to_core(tid, slot_id);  // assume slot_id is 1 (it is in the test case I run)
   sched_get_priority_min(SCHED_BATCH);
   pthread_setschedparam(tid, SCHED_BATCH, &sched_param);
   int success = process_job(...);  // this is where all the work actually happens
   pthread_exit(NULL);
}

int main(int argc, char* argv[]) {
   ...
   pthread_t temp;
   pthread_create(&temp, NULL, worker_thread, (void *) &jobs[i]);  // jobs is a vector of a class type containing information for the task
   ...
   return 0;
}

如果你有足夠的CPU內核,並且還有很多工作要做,那么多線程運行的時間不應超過單線程模式 - 實際的CPU時間可能要長一點,但“掛鍾時間”應該是短。 我很確定你的代碼有一些瓶頸,其中一個線程阻塞另一個線程。

這是因為這些事情中的一個或多個 - 我先列出它們,然后詳細說明如下:

  1. 線程中的某些鎖定會阻止第二個線程運行。
  2. 線程之間共享數據(真或假“共享”)
  3. 緩存顛簸。
  4. 某些外部資源的競爭導致顛簸和/或阻塞。
  5. 設計糟糕的代碼一般......

線程中的某些鎖定會阻止第二個線程運行。

如果有一個線程接受鎖定,而另一個線程想要使用該線程鎖定的資源,則必須等待。 這顯然意味着線程沒有做任何有用的事情。 只需鎖定一小段時間即可將鎖保持在最低限度。 使用一些代碼來識別鎖是否包含您的代碼,例如:

while (!tryLock(some_some_lock))
{
    tried_locking_failed[lock_id][thread_id]++;
}
total_locks[some_lock]++;

打印鎖定的某些統計信息有助於識別鎖定存在爭議的位置 - 或者您可以嘗試“在調試器中按下斷點並查看您的位置”的舊技巧 - 如果線程一直在等待某些鎖定,那么這就是什么阻止進步......

線程之間共享數據(真或假“共享”)

如果兩個線程使用[並經常更新它的值]相同的變量,那么兩個線程將不得不交換“我已更新此”消息,並且CPU必須從其他CPU獲取數據才能繼續用它來使用變量。 由於“數據”在“每個緩存行”級別上共享,而緩存行通常為32個字節,如下所示:

int var[NUM_THREADS]; 
...
var[thread_id]++; 

將被歸類為稱為“虛假共享”的東西 - 更新的ACTUAL數據在每個CPU中是唯一的,但由於數據在相同的32字節區域內,因此核心仍將更新相同的內存。

緩存顛簸。

如果兩個線程執行大量內存讀寫操作,則CPU的緩存可能會不斷丟棄良好的數據,以便用另一個線程的數據填充它。 有一些技術可用於確保兩個線程不在“鎖步”中運行,CPU使用哪部分緩存。 如果數據是2 ^ n(2的冪)並且相當大(高速緩存大小的倍數),則為每個線程“添加偏移量”是個好主意 - 例如1KB或2KB。 這樣,當第二個線程讀取到數據區域的相同距離時,它將不會完全覆蓋第一個線程當前正在使用的相同緩存區域。

某些外部資源的競爭導致顛簸和/或阻塞。

如果兩個線程正在從硬盤,網卡或其他共享資源讀取或寫入,則可能導致一個線程阻塞另一個線程,這反過來意味着性能降低。 在開始使用其他線程之前,代碼還可能檢測到不同的線程並執行一些額外的刷新以確保以正確的順序或類似方式寫入數據。

代碼中內部也可能存在處理資源(用戶模式庫或內核模式驅動程序)的鎖定,這些鎖定在多個線程使用相同資源時阻塞。

一般設計不好

這是“很多其他可能出錯的事情”的“收獲”。 如果需要一個線程中的一個計算的結果來推進另一個線程,顯然,在該線程中不能完成很多工作。

工作單元太小,因此所有時間都花在啟動和停止線程上,並且沒有做足夠的工作。 例如,你說小的數字是“計算這是否是一個素數”每個線程,一次一個數字,它可能需要更長的時間來給線程的數字而不是計算“是這個實際上是一個素數“ - 解決方案是給每個線程提供一組數字(可能是10,20,32,64等),然后一次性報告整個批次的結果。

還有很多其他“糟糕的設計”。 如果不理解你的代碼,就很難肯定。

完全有可能你的問題不是我在這里提到的問題,但很可能是其中之一。 希望這個asnwer有助於確定原因。

讀取CPU緩存以及為什么要關心為什么從一個線程到多個線程的算法的簡單端口通常會導致性能大大降低和負面可伸縮性。 專門針對並行性設計的算法負責過度交互操作,錯誤共享和緩存污染的其他原因。

以下是您可能需要研究的一些事項。

1°)您是否在工作線程和主線程之間輸入任何關鍵部分(鎖,信號量等)? (如果您的查詢修改圖形,則應該是這種情況)。 如果是這樣,那可能是多線程開銷的來源之一:競爭鎖的線程通常會降低性能。

2°)你使用的是24核機器,我認為它是NUMA(非統一內存訪問)。 由於您在測試期間設置了線程關聯,因此應密切關注硬件的內存拓撲。 查看/ sys / devices / system / cpu / cpuX /中的文件可以幫助您(請注意cpu0和cpu1不一定靠近,因此不一定共享內存)。 使用內存的線程應該使用本地內存(在與它們正在執行的內核相同的NUMA節點中分配)。

3°)您正在大量使用磁盤I / O. 那是哪種I / O? 如果每個線程每次都執行某些同步I / O,您可能需要考慮異步系統調用,以便操作系統負責將這些請求調度到磁盤。

4°)在其他答案中已經提到了一些緩存問題。 根據經驗,虛假分享可能會像你觀察到的那樣傷害表演。 我的最后一條建議(應該是我的第一篇)是使用分析器工具,例如Linux Perf或OProfile。 如果您遇到這種性能下降,原因肯定會非常明顯。

其他答案都解決了可能導致症狀的一般指導原則。 我會給出我自己的,希望不是多余的版本。 然后我將談談如何在討論所有內容的情況下找到問題的根源。

通常,您希望多個線程執行得更好的原因有幾個:

  • 一項工作依賴於某些資源(磁盤,內存,緩存等),而其他部分可以獨立於這些資源或所述工作負載進行。
  • 您有多個CPU核心可以並行處理您的工作負載。

上面列舉的主要原因是,您希望多個線程執行得不好,都是基於資源爭用:

  • 磁盤爭用:已經詳細解釋並且可能是一個可能的問題,特別是如果您一次編寫小緩沖區而不是批處理
  • 如果將線程安排到同一核心上,則會產生CPU時間爭用:如果您正在設置關聯,則可能不是您的問題。 但是,你仍然應該仔細檢查
  • 緩存顛簸:如果你有親和力,同樣可能不是你的問題,但如果這是你的問題,這可能會非常昂貴。
  • 共享內存:再次詳細討論並且似乎不是您的問題,但審核代碼以查看它並不會有什么壞處。
  • NUMA:再次談到。 如果您的工作線程固定到不同的核心,您將需要檢查它需要訪問的工作是否是主核心的本地工作。

好到目前為止沒有多少新的。 它可以是上述任何一種或不同的。 問題是,對於您的情況,您如何才能發現額外時間的來源。 有幾個策略:

  • 審核代碼並尋找明顯的區域。 不要花太多時間這樣做,因為如果你開始編寫程序通常沒有用。
  • 重構單線程代碼和多線程代碼以隔離一個process()函數,然后在關鍵檢查點進行配置以嘗試解釋差異。 然后縮小范圍。
  • 將資源訪問重構為批次,然后在控件和實驗上分析每個批次以說明差異。 這不僅會告訴您需要集中精力的區域(磁盤訪問與內存訪問與在一些緊密循環中花費的時間),執行此重構甚至可能會提高整體運行時間。 例:
    • 首先將圖形結構復制到線程本地內存(在單線程情況下執行直接復制)
    • 然后執行查詢
    • 然后設置異步寫入磁盤
  • 嘗試找到具有相同症狀的最低可重現工作負載。 這意味着更改算法以執行其已有功能的子集。
  • 確保系統中沒有其他噪音可能導致差異(如果其他用戶在工作核心上運行類似的系統)。

我個人的直覺:

  • 您的圖形結構對於您的工作核心不是NUMA友好的。
  • 內核實際上可以將您的工作線程安排在親和核心之外。 如果你沒有為你固定的核心啟用isolcpu,就會發生這種情況。

我不能告訴你你的程序有什么問題,因為你沒有足夠的共享來進行詳細的分析。

我可以告訴你的是,如果這是我的問題,我首先嘗試的是在我的應用程序上運行兩個探查器會話,一個在單線程版本上,另一個在雙線程配置上。 分析器報告應該讓您非常了解額外時間的去向。 請注意,您可能不需要對整個應用程序運行進行概要分析,具體取決於問題,在您分析幾秒鍾或幾分鍾后,時差可能會變得明顯。

至於Linux的分析器選擇,您可能需要考慮oprofile或作為第二選擇gprof

如果您發現需要幫助來解釋分析器輸出,請隨意將其添加到您的問題中。

在追蹤線程未按計划運行的原因后,可能會出現正確的痛苦。 人們可以分析地這樣做,或者可以使用工具來顯示正在發生的事情。 我已經在ftrace中取得了很好的成績,Linux是Linux的dtrace的克隆版(這反過來基於VxWorks,Greenhill的Integrity操作系統和Mercury計算機系統公司在一段時間內所做的事情。)

特別是我找到了這個網頁非常有用: http://www.omappedia.com/wiki/Installing_and_Using_Ftrace ,特別是這個這個節。 不要擔心它是一個OMAP導向的網站; 我在X86 Linux上使用它就好了(盡管你可能需要構建一個包含它的內核)。 還要記住,GTKWave查看器主要用於查看來自VHDL開發的日志跟蹤,這就是它看起來“奇怪”的原因。 只是有人意識到它對於sched_switch數據也是一個可用的查看器,並且保存了他們寫一個。

使用sched_switch跟蹤器,您可以看到線程運行的時間(但不一定是為什么),這可能足以為您提供線索。 通過仔細檢查其他一些示蹤劑可以揭示“為什么”。

如果從使用1個線程開始變慢,可能是由於使用線程安全庫函數或線程設置的開銷。 為每個作業創建一個線程會導致很大的開銷,但可能沒有你提到的那么多。 換句話說,它可能是某些線程安全庫函數的一些開銷。

最好的辦法是分析您的代碼以找出花費的時間。 如果它在庫調用中,請嘗試查找替換庫或自行實現。 如果瓶頸是線程創建/銷毀嘗試重用線程,例如在C ++ 11中使用OpenMP任務或std :: async。

有些庫非常討厭線程安全開銷。 例如,許多rand()實現使用全局鎖,而不是使用線程本地prgn。 這種鎖定開銷遠大於生成數字,並且在沒有分析器的情況下很難跟蹤。

減速也可能源於您所做的微小變化,例如聲明變量volatile,這通常不是必需的。

我懷疑你是在一台配備單核處理器的機器上運行的。 這個問題在這種系統上無法並行化。 您的代碼一直在使用處理器,該處理器具有固定數量的周期。 它實際上運行得更慢,因為額外的線程增加了昂貴的上下文切換到問題。

在單處理器機器上很好地並行化的唯一問題是那些允許一條執行路徑運行而另一條路徑被阻塞等待I / O的問題,以及允許一個線程獲得的情況(例如保持響應式GUI)一些處理器時間比盡快執行代碼更重要。

如果您只想運行算法的許多獨立實例,您只需向群集提交多個作業(具有不同的參數,可由單個腳本處理)嗎? 這將消除分析和調試多線程程序的需要。 我對多線程編程沒有多少經驗,但如果你使用MPI或OpenMP,那么你必須為書籍編寫更少的代碼。 例如,如果需要一些常見的初始化例程,並且進程可以獨立運行,那么您可以通過在一個線程中初始化並進行廣播來實現。 無需維護鎖等。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM