線程化的性能障礙:它們將給 OpenMP 代碼帶來什麼影響?

 原文地址:http://www.intel.com/cd/ids/developer/apac/zho/recent/323448.htm

 

當今,多核處理器正成爲主流,開發人員必須使其代碼實現線程化,才能並行運行代碼。OpenMP1 可以提供實現應用程序線程化的有效方法。但是,關於線程化代碼的性能,您都應該瞭解什麼,使用 OpenMP 時該性能又意味着什麼?在之前的文章2中,我們發現所有線程化方法的啓動成本都相同,但是 OpenMP 與常見的 Windows* 線程化相比具有一定的性能優勢,原因在於它使用了線程池。

在決定如何以及在何處線程化代碼時,瞭解 OpenMP 性能是至關重要的。爲進行線程化而更改架構平衡和算法時尤爲如此。在應用程序中對 OpenMP 代碼進行原型設計和評測前,應當瞭解 OpenMP 線程化可能對代碼帶來怎樣的性能影響。

在本文中,我們將對使用 OpenMP 實現線程化進行全方位的解讀,並瞭解其對代碼性能帶來的影響。我們將介紹一些極爲常見的 OpenMP 指令。同時,我們還將探討手動調度代碼時的運行時成本,如果循環體在運行時變化較大,則該成本是必不可少的。如果您要線程化現有的序列化代碼(或有這方面的打算)、修改和評測現有的並行代碼或從頭新建並行設計,本文將非常有用。線程化現有代碼可能最難,因此我們將重點關注這一方面。

您可能出於以下三個原因而線程化應用程序。每個對性能評測的要求都不同。

  1. 能更快的執行同一工作:
    如果應用程序負載固定(如,對靜態照片應用某種效果),我們可以通過線程化更快地完成工作。評測此代碼時,我們將記錄執行時間和通過線程化實現的加速比。
  2. 執行更多的工作:
    如果將應用程序擴展到執行更多個負載相同的工作(例如,更新較大的像素緩衝區),或將不同的工作添加到負載中(例如,對遊戲管道添加粒子效果),則我們應該進行線程化以通過應用程序完成更多的工作。通常我們將其作爲吞吐量進行評測。要評測此代碼,就要評測吞吐量,吞吐量是針對全部執行時間評測的工作量。
  3. 抵銷慢操作所花費的時間:
    如果應用程序需要進行長時間的操作(如加載文件),則通過線程化可事先執行這些操作,以便在需要結果時能夠立刻提供。有多種方法可用於評測此代碼,但主要評測是用以定性的評測;用戶能否從這些操作中察覺到延遲?能否輕鬆地進行評測,取決於負載情況。很多應用程序(尤其是遊戲)已開始使用線程化(以及一些其他技術,如異步磁盤 I/O)來抵消慢操作所花費的時間。


任意組合這些因素,可能會在代碼中獲得更高的性能。確定哪些因素與您相關。實現線程化的方式和使用的評測指標將取決於您所排定的優先級。

一些代碼組合使用這些方法。例如,假設您要將遊戲的着色管道的物理部分移到一個單獨的線程。您可能希望執行此操作,以使代碼能夠運行更加完整的物理模擬。此代碼在着色管道中,因此它可在對象數據流經管道時對其執行操作。如果降低對一致性和時間的要求,並讓物理子系統在數據副本(一個不同步的幀)上工作,則您便能夠將物理部分移動到一個單獨的線程。這也要求具有一個額外緩衝層,這樣物理操作便具有要使用的單獨數據。

進行此更改時出現了一個奇怪的現象:附加緩衝成本通常遠遠低於原來的物理計算,這樣其餘着色管道的運行速度大大加快了。此更改可以提高遊戲的幀頻,但是您也可以使用它向管道添加更豐富的步驟。

此類更改具有雙重優勢,在本案例中,可以帶來更高的幀頻和更豐富的物理模擬。

線程化代碼時,尋找一種分解代碼的方法,以便各個部分的代碼可以獨立運行,並將這些部分分配到不同的線程。將代碼分成獨立的功能(功能分解)或能夠在單獨數據塊上獨立執行同一工作的代碼(數據分解或域分解)。如何找到在代碼中進行線程化的機會?
 
Amdahl 法則

Amdahl 法則描述了任意給定代碼所能實現的加速比的理論可能性。

對於代碼 F 的串行成分,理論上預期可以在 N 個處理器上實現加速比:

       

如果線程化 20% 的代碼(80% 保持串行),則在 4 個處理器上可以實現最大加速比:



我們還可以使用 Amdahl 法則預測加速比的上限(將 N 設置爲 ∞)。我們更多將該法則用於預測在擁有兩個或四個處理器內核的典型情況下的加速比,在這些情況下我們希望線程化能帶來較大的優勢。
 
基本規則是確定花費最多時間的串行代碼所在的區域,然後對其進行線程化。我們可以使用 Amdahl 法則3(請見邊欄)預測可從拆分代碼中實現的最大加速比。

也就說,在理想情況下,可以將所有串行代碼轉化爲並行代碼。要開始轉化,必須儘可能地將代碼向最外層循環線程化。通常,循環迭代中(以及循環迭代之間)的依賴關係會使轉化變得很困難。查找代碼的這些部分,但是如果依賴關係使得線程化無法進行,請不要感到驚訝。有些算法使您可以輕鬆地進行線程化("密集並行"問題4),但是多數代碼具有依賴關係,這將阻止您線程化整個應用程序。

從另一方面來看,如果代碼中長運行循環在最低級別沒有依賴關係,則線程化就變得很容易。我們在後文將討論一些有關每線程開銷的問題,但是僅當使用線程池(例如 OpenMP 就是使用線程池)時此方法纔可行。如果每次運行低級循環時都要新建線程,則創建線程所耗費的成本將超過所有收益。

應該從代碼的最常執行部分(熱點)開始線程化,但是使用 OpenMP 可以向代碼中所有最低級別循環添加線程。各個線程化所提升的性能都很小,但是如果都加起來,則總共提升的性能將相當可觀。
我應該如何評測線程化的代碼?
您應該評測所有線程化的代碼。有幾種不同的方法可以執行此操作,具體取決於您想評測什麼。在某些情況下,最佳做法是安裝本地計時代碼或配置代碼;您的代碼可能已經具有內置配置機制,或者您可能希望使用 Windows PerformanceCounter API 調用。如果您希望查看應用程序的整體性能和代碼中所有熱點的詳細評測,應該考慮使用類似於 VTune™ 性能分析器的工具。您應該制定如下策略:評測原始代碼並以此作爲基準,然後評測代碼中的後續更改。

在評測線程化應用程序的性能時,應該注意以下幾點。首先,需要了解代碼的串行部分和代碼的並行部分的執行時間。

串行時間:在單個處理器內核上的單個線程中運行代碼 通過將串行代碼封裝入計時代碼對其進行評測
並行時間:在多個線程上運行代碼,因此可以同時在多個處理器內核上運行此代碼 圍繞線程化部分進行評測,並記錄線程計數,請參閱下文

評測串行代碼非常簡單,因此我們主要介紹並行代碼。

圖 1:並行循環示例

在圖 1 的代碼中,評測所示塊花費的(包括 OpenMP #pragma)的並行時間。同時記錄線程計數,這樣可以看出代碼從單核計算機到雙核計算機甚至多核計算機的加速情況。記錄線程計數的簡單方法是使用 omp_get_max_threads() 調用。也請參閱下文對加速比的討論。

很少有不經過一些大轉換而嚮應用程序添加重要線程的情況。有時候,整個算法都會更改。循環通常包含數據依賴關係。要處理好依賴關係從而可以線程化,可能需要爲每個線程緩衝額外的數據副本,然後添加同步代碼以解決所有差異。

此類大幅度更改可能會使代碼運行速度加快或減慢,具體取決於如何執行更改。使用此方法時請務必小心。

轉換代碼時可以嘗試進行其他更改,例如重構代碼。通常情況下,這是一個好方法,但是它將使評測性能更改的難度加大。一般的做法是儘量將這些活動彼此分開。在一次運行過程中進行重構,然後在另一次運行過程中進行線程化(反之亦然)。

評測此類轉換的難度比較大。很難評測各個代碼塊,因爲您在不斷地對其進行更改。評測經轉換的代碼的最佳方法是具有一個可以在更改前、更改過程中和更改後都可以評測的應用程序級標準。
線程化將給代碼帶來何種開銷?
在真正瞭解代碼的性能之前,您要考慮其他幾個因素。

首先,考慮開銷。在進行線程化過程中,總是會增加一些開銷;我們增加的是固定的啓動開銷和每線程開銷。在某些情況下(通常是在小循環和低迭代計數中),這些開銷數額太高,以至於使線程化代碼變得沒有意義。在這些情況下,我們最好將代碼保留爲其原始串行形式。

下面列出了線程化代碼的一些開銷因素。多數情況下,在 OpenMP 代碼中我們可以忽略這些因素,但是在此表的後面,我們將探討不能忽略這些因素的情況。

因素 說明 如何檢測/評測它是否是個問題? 如何處理?
線程庫啓動開銷 代碼啓動時的一次性開銷。對於多數代碼都不明顯。與應用程序啓動的其他部分捆綁在一起 除典型代碼內評測外,將串行與線程化應用程序運行相比較

不能由開發人員進行調整
線程啓動開銷 創建線程的時間。OpenMP 使用線程池的一次性成本。 對線程化代碼的多次運行過程進行評測,請參閱下文 線程化更高和/或更大的循環(在可行之處)以抵銷此開銷
每線程(循環調度)開銷 在各個線程上線程化工作的庫調度塊所花費的時間 僅在與性能緊密相關的代碼或不常用的調度代碼中評測,請參閱下文 調整代碼中的調度,請參閱下文
鎖定管理開銷 管理關鍵部分上的鎖定所花費的時間。當對 OpenMP 實現相互進行性能評測時,有時會使用5 使用類似 VTune 性能分析器的工具,監視將被頻繁調用的鎖定調用。多數情況下,大量鎖定的代碼會出現較大的阻塞問題,請參閱下文。 減少阻塞現象以減少鎖定爭用和管理,當爭用較低時,請參閱下文了解其他可選辦法

圖 2:OpenMP 中的開銷因素

這些是線程化代碼中的開銷因素。多數情況下,在 OpenMP 代碼中我們可以忽略這些因素,我們將進一步探討不能忽略這些因素的情況。

讓我們看看這些不同的開銷都是在何處發生的。



圖 3:線程開銷位置

評測圖 3 中顯示的這些開銷。評測每線程和庫啓動開銷很容易,但是評測線程啓動開銷則需要進行幾種不同的測試。首先,評測恰好運行一次 OpenMP parallel for 指令所需的時間。然後,評測運行該指令兩次所需的時間。由於 OpenMP 使用線程池,因此第一個循環應包含所有線程啓動開銷。最終,第二次迭代所花費的時間應遠遠少於第一次迭代所花費時間的 2 倍。第二次迭代所增加的時間是運行一次循環迭代的"本地"時間;將第一次循環迭代所花費的時間減去此時間,即可計算出線程啓動開銷。

早期的研究2發現線程啓動開銷平均爲 170-190 毫秒(在雙核計算機上創建兩個線程時)。值得高興的是多數 OpenMP 實現(包括最新的英特爾和 Microsoft 編譯器的 OpenMP 實現)都使用了線程池。您的代碼只需支出線程開銷一次,即在第一次使用線程時。這樣,對於多數線程和調用模式來說,線程啓動開銷非常低。

這是 OpenMP 與典型的 Windows 線程化相比所具有的獨特優勢之一;只要有可能,OpenMP 運行時即會自動使用線程池。由於典型的 Windows 線程化不使用線程池,因此每個線程都有線程啓動開銷。可能很多應用程序都禁止這種行爲。如果選擇 Windows 線程化而非 OpenMP,可以將 Windows 線程與線程池結合使用以避免此開銷,但是操作更爲複雜。

OpenMP 庫可以檢測代碼在任意給定計算機上的最佳線程數。這是 OpenMP 與典型的 Windows 線程化相比的另一個優勢。

有很多跡象表明在管理鎖定方面與 Windows 本地線程相比,在英特爾 C++ 編譯器中實現的 OpenMP 更有效。這是一個好消息,但可能不會對您的代碼產生大的影響。但是最好確保鎖定代碼不在代碼的熱點列表中(使用類似 VTune 性能分析器的工具),這很容易實現。這是因爲經常在出現資源爭用的位置使用鎖定,因此等候資源時,代碼一定會阻塞。在這些情況下,阻塞是一個更爲重要的考慮因素,因此基本上無需評測鎖定管理開銷。

在典型的循環代碼中(迭代計數大小適中、計算集長度適中),這些開銷都沒有什麼影響。繼續閱讀有關阻塞問題(通常是更爲重要的考慮因素)的討論。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章