[英]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時間可能要長一點,但“掛鍾時間”應該是短。 我很確定你的代碼有一些瓶頸,其中一個線程阻塞另一個線程。
這是因為這些事情中的一個或多個 - 我先列出它們,然后詳細說明如下:
如果有一個線程接受鎖定,而另一個線程想要使用該線程鎖定的資源,則必須等待。 這顯然意味着線程沒有做任何有用的事情。 只需鎖定一小段時間即可將鎖保持在最低限度。 使用一些代碼來識別鎖是否包含您的代碼,例如:
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。 如果您遇到這種性能下降,原因肯定會非常明顯。
其他答案都解決了可能導致症狀的一般指導原則。 我會給出我自己的,希望不是多余的版本。 然后我將談談如何在討論所有內容的情況下找到問題的根源。
通常,您希望多個線程執行得更好的原因有幾個:
上面列舉的主要原因是,您希望多個線程執行得不好,都是基於資源爭用:
好到目前為止沒有多少新的。 它可以是上述任何一種或不同的。 問題是,對於您的情況,您如何才能發現額外時間的來源。 有幾個策略:
我個人的直覺:
在追蹤線程未按計划運行的原因后,可能會出現正確的痛苦。 人們可以分析地這樣做,或者可以使用工具來顯示正在發生的事情。 我已經在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.