英特尔多核平台编程优化大赛报告
前言
本次優化使用的CPU是Intel?Xeon?5130?主頻為2.0GHz?同Intel酷睿2一樣是基于Core?Microarchitecture?的雙核處理器。本次優化在Intel的工具幫助下主要針對Core?Microarchitecture?系列處理器進行優化。但是由于未知原因,Intel?VTune?Analyzers并不能在該系統下正常工作。所以,所有使用Intel?VTune?Analyzers的測試均使用另外一個奔騰D?820的系統測試。
第一章主要介紹了程序的串行優化。其中有關于Intel編譯器使用,以及Intel?Math?Kernel?Library使用,Intel?VTune?Analyzers使用的介紹。在借助Intel工具的幫助下,結合Intel?Core?Microarchitectured的特性。設計出了針對L1?Cache進行優化的,高效率的串行代碼。程序的執行時間從優化前的4.765秒達到了優化后的0.765秒。
第二章主要介紹了程序的并行化。首先討論了2種并行算法的優缺點。然后選擇了適合本程序的并行算法進行優化。并且在最后分析了并行化時的性能瓶頸。通過并行化,程序達到了0.437秒。
第三章主要介紹了程序的匯編優化。首先介紹了計算的數學理論。然后介紹了匯編代碼的編寫。最后進行了性能分析。通過該步優化程序在保留小數點后7位精度的前提下達到了0.312秒的好成績。并且在Intel酷睿2?E6600?上測試達到了0.25秒。
附錄A?說明了本次報告的目錄結構和優化方法。
附錄B?列出了進行本次競賽所參考的文獻。
?
目錄
一、串行優化
1.1?代碼的基本修改和優化
1.2?基于Intel編譯器的優化
1.3?使用Intel?VTune?Analyzers進行性能分析
1.3.1?Intel?VTune?Analyzers概述
1.3.2?基于SAMPLING方式的分析
1.3.3?對于本次程序的分析
1.4?優化computePot函數
1.5?使用Intel?Math?Kernel?Library
1.6?根據Cache大小優化Intel?Math?Kernel?Library調用
1.7?優化updatePositions函數
1.8?其他優化以及性能分析
二、并行優化
2.1?并行優化概述
2.2?優化方案一
2.3?優化方案二
2.4?并行實現
2.5?性能分析
三、匯編級優化
3.1?優化目標
3.2?數學理論
3.3?匯編碼實現
3.4?性能分析
3.5?總結
附錄A?目錄結構和編譯方法
附錄B?參考文獻
一、串行優化
1.1?代碼的基本修改和優化
首先根據主辦方的要求把代碼的輸出精度改為小數點后7位。
| if?(i%10?==?0)?printf("%5d:?Potential:?%20.7f/n",?i,?pot); |
在進行任何優化前代碼的執行時間是4.765秒。
接著把項目轉換成使用Intel?C++?Compiler,代碼的執行時間是4.531秒。
然后執行最基本的優化,把代碼中的pow函數優化成乘法。代碼如下:
?
| distx?=?(r[0][j]?-?r[0][i])*(r[0][j]?-?r[0][i]); disty?=?(r[1][j]?-?r[1][i])*(r[1][j]?-?r[1][i]); distz?=?(r[2][j]?-?r[2][i])*(r[2][j]?-?r[2][i]); |
執行時間依然為4.531秒。說明Intel編譯器已經將pow函數優化掉了。
1.2?基于Intel編譯器的優化
這里介紹本程序中基于Intel編譯器優化技術。其中有些優化參數是可以確定的,有些優化參數需要在程序的不同階段反復調試以確定最優方案,而有些優化技術是在后面的優化中使用的。
編譯器優化級別
Intel的編譯器共有如下一些主要的優化級別:
u???????/O1:實現最基本的優化
u???????/O2:基于代碼速度實現常規優化,這個也是默認的優化級別
u???????/O3:在/O2的基礎上實現進一步的優化,包括Cache預讀,標量轉換等等,但是在某些情況下反而會減慢代碼的執行速度。
u???????/Ox:實現最大化的優化,包括自動內聯函數的確定,全局優化,使用EBP作為通用寄存器等。
u???????/fast:等同于/O3,?/Qipo,?/Qprec-div-,?and?/QxP。
通過測試,目前選用/O3,但是隨著代碼的更改,需要重新測試,選擇合適的優化級別。
針對特定處理器進行優化
Intel的編譯器一共支持如下3種針對特定處理器的優化:
u???????/G:使用這個優化選項,Intel將針對特定的CPU進行優化,但是其代碼依然可以在所有的CPU上執行。
u???????/Qx:使用這個優化選項,Intel將針對特定的CPU進行優化,并且產生的代碼無法使用在不兼容的CPU上。
u???????/Qax:使用這個優化選項,Intel將針對特定的CPU進行優化,并且產生多份代碼,在運行時根據CPU類型自動選擇最優的代碼。
由于本程序只需要運行在基于Core?Microarchitecture?的處理器上,而無需考慮兼容性。所以本程序選擇/Qx選項。并且針對運行時的酷睿2處理器,選擇/QxT。但是在進行VTune測試時,由于測試平臺為奔騰D?820,所以暫時使用/QxP的參數。
使用IPO
使用/Qipo可以啟用Intel編譯器的過程間優化(Interprocedural?Optimizations)。通過過程間優化,編譯器可以通過使用寄存器優化函數調用、內聯函數展開、過程間常數傳遞、跨多文件優化等方式進一步優化程序。
此外,Intel編譯器支持多文件的過程間優化,而由于本程序只有一個文件,所以并不需要使用。
但是IPO優化卻會對本程序的調試帶來極大的麻煩。所以本程序開發時不使用IPO優化,只有在最后的版本中才嘗試使用IPO優化能否提高效率。
使用GPO
Intel編譯器支持GPO(Profile-Guided?Optimization)。GPO由一下三步組成。
第一步:使用/Qprof-gen編譯程序,產生能記錄運行細節的特殊程序。
第二步:運行第一步產生的程序,生成動態信息文件(.dyn)。
第三步,使用/Qprof-use,結合動態信息文件重新編譯程序,產生更優化的程序。
通過使用GPO,Intel編譯器可以更詳細得了解程序的運行情況,從而根據實際情況產生更優化的代碼。比如優化條件跳轉,使得CPU分支預測的能力更準確,又如決定哪些函數需要內聯,哪些不要內聯等。
此外,基于GPO還有很多的工具方便用戶開發程序。比如Code-Coverage?Tool可以進行代碼覆蓋測試。
由于GPO收集的信息和特定的程序有關,而本程序一直在修改。所以本程序只在每個版本的最后部分使用GPO進行優化。
循環展開
循環展開(Loop?Unrolling)通過在把循環語句中的內容展開從而使執行的代碼速度更快。循環展開可以提高代碼的并行程度,減少條件轉移次數從而提高速度。另外,對于Pentium?4處理器,其分支預測功能可以精確得預測出16次迭代以內的循環,所以,如果能把循環展開到迭代次數在16次以內,對于特定的CPU可以提高分支預測準確度。
但是循環展開必須有一個度,并不是展開層數越多越好,展開層數多了,可能反而影響代碼的執行速度。所以通常的做法是讓編譯器自己決定循環展開的層數。
Intel編譯器對于循環展開有如下選項:
u???????/Qunrolln:執行循環展開n層。
u???????/Qunroll:讓Intel編譯器自己決定循環展開的層數。
此外Intel編譯器還提供在了程序中使用編譯制導語句規定某個特定循環的展開次數。如下例指示for循環展開n層。
?
| #pragma?unroll(n) for(i=0;i<10000;i++){……} |
所以本程序使用/Qunroll參數,讓Intel編譯器自己決定使用循環展開的層數。但是在程序的最終優化時,如果發現Intel編譯器的循環展開并不是最優的,則通過在特定循環前加上編譯制導語句,使用最佳的循環展開層數。
浮點計算優化
Intel編譯器提供了很多基于浮點數的優化參數,有提供精度的,也有提高速度的。對于本程序,主要使用如下優化參數。
u???????/fp:?fast或/fp:?fast=1:這兩個參數的等價的,同時也是默認的參數。他告訴編譯器進行快速浮點計算優化。
u???????/fp:?fast=2:這個參數比/fp:?fast=1提供更高的優化級別,同時也可能帶來更大的精度損失。
本程序使用/fp:?fast=2優化,但是如果發生精度問題,可以考慮使用/fp:?fast=1。
自動并行化
Intel的編譯器支持自動并行化(Auto-parallelization)。通過/Qparallel可以打開編譯器的自動并行化,編譯器會在分析了用戶的串行程序后,自動選擇可以并行的部分進行并行化。自動并行化的有點是方便,不需要用戶懂得專業知識,不需要更改原來的串行程序。但是缺點也是顯而易見的,由于編譯器并不知道用戶的程序邏輯,所以無法很好得進行并行化。在對本程序試用/Qparallel后發現,效果并不好。所以本程序不只用/Qparallel進行自動并行化。
使用OpenMP并行化
OpenMP是一種通用的并行程序設計語言,其通過在源代碼中添加編譯制導語句,提示編譯器如何進行程序的并行化。OpenMP具有書寫方便,不需要改變源代碼結構等多種優點。Intel的編譯器支持OpenMP。本次程序并不打算使用OpenMP進行并行化,而打算使用Windows?Thread。但是由于本程序需要使用到Intel?Math?Kernel?Library,而Intel?Math?Kernel?Library中的代碼支持OpenMP并行化。所以有必要使用一些基本的OpenMP設置函數。
需要使用OpenMP,需要在編譯時加上/Qopenmp選項。并且在源代碼中包含”?omp.h”文件。
OpenMP提供了函數omp_set_num_threads(nthreads)設置OpenMP使用的線程數,由于其設置會影響到Intel?Math?Kernel?Library,所以將其設置成1,禁止Intel?Math?Kernel?Library的自動并行化。
向量化
Intel的編譯器支持向量化(Vectorization)。可以把循環計算部分使用MMX,SSE,SSE2,SSE3,SSSE3等指令進行向量化,從而大大提高計算速度。這也是本程序串行化時的主要優化點。前面提到的針對處理器的/QaxT優化選項已經打開了向量化。將代碼向量化還有許多需要注意的地方,具體的注意點和方法將在后面具體的代碼中說明。這里先給出一些對向量化有用的編譯制導語句以及選項。
u???????/Qrestrict選項:當Intel編譯器遇到循環中使用指針時,由于多個指針可能指向同一個地址,所以其無法保證指針指向內容的唯一性。故Intel編譯器無法確定循環內數據是否存在依賴性。這是可以通過使用/Qrestrict選項與restrict關鍵字,指示某個指針指向內容的唯一性。從而能解決數據依賴性不確定的問題。
u???????#pragma?vector編譯制導語句:該編譯制導語句一共包含3個。#pragma?vector?always用于指示編譯器忽略其他因素,進行向量化。#pragma?vector?aligned用于指示編譯器進行向量化時使用對齊的數據讀寫方式。#pragma?vector?unaligned用于指示編譯器進行向量化時使用不對齊的數據讀寫方式。由于在使用SSE類指令進行向量化時,需要同時處理多個數據,所以每次讀寫的數據長度很長,可以達到128bit。所以將要處理的數據按照128bit(16byte)對齊,使用對齊的讀寫指令是可以提高程序運行速度的。但是需要注意的是對于實際沒有對齊的數據使用#pragma?vector?aligned會造成程序運行錯誤。
使用變量對齊指示
Intel編譯器提供了__declspec(align(n))用于在定義變量時指定其需要進行n字節對齊。變量對齊對于向量化計算的讀取速度有很大關系。對于向量化計算一般使用__declspec(align(16))進行對齊。另外也可以使用__declspec(align(64))指定變量對齊到Cache的行首。關于Cache的行對齊的詳細討論請見后文的分析。
數據預讀
通常數據是放在內存中,當要計算時才讀入CPU進行計算。由于內存到CPU的傳輸需要很長時間,所以CPU中有多級Cache機制。Intel編譯器支持數據預讀優化選項。通過/Qprefetch打開數據預讀優化,編譯器會在使用數據前先插入預讀指令,讓CPU先把數據預讀到Cache中,從而加快數據的訪問速度。該選項默認情況下是打開的。此外Intel還提供了數據預讀的編譯制導語句,通過使用#pragma?prefetch語句,用戶可以人為得在程序中增加數據預讀指令。但是需要注意的是,數據預讀指令并不是越多越好的。不恰當的數據預讀指令會占用內存帶寬,把有用的數據從Cache中擠出去,反而影響速度。并且Core?Microarchitecture體系結構已經支持給予硬件的數據預讀指令。所以本程序傾向于使用給予硬件的數據預讀機制。而由于/Qprefetch默認的打開的,也沒有必要特意關閉該選項,Intel編譯器有能力判斷哪些地方可以通過合適的數據訪問模式激活硬件數據預讀機制,哪些地方需要額外添加數據預讀指令。
產生調試信息
通過使用/Zi選項產生調試信息以幫助調試。默認為關閉。在本程序的開發階段,打開此選項。在開發完成后關閉此選項。
使用全局優化
通過使用/Og選項打開編譯器的全局優化功能。改選項需要在本程序不同的開發階段分別嘗試是否打開以確定最優優化選項。
針對Windows程序優化
通過使用/GA選項可以打開Intel編譯器的針對Windows程序優化的功能。其實通過打開/GA選項,Intel可以提高訪問Windows下thread-local?storage(TLS)變量的速度。TLS變量通過__declspec(thread)來定義。在本程序中,并不打算使用TLS變量。但還是打開/GA選項。
?
內聯函數擴展
Intel編譯器可以通過/Obn來定義內聯函數的擴展級別。當n為0禁止用戶定義的內核函數的擴展。當n為1時,根據用戶定義的inline關鍵字進行擴展。當n為2時,根據Intel編譯器的自動判斷進行擴展。本次程序使用/Ob2選項。
FTZ與DAZ
在計算機內浮點數是由尾數和指數組成的。尾數通常被規范化成[1,2)之間。但是當數字接近0時,由于其指數已經無法將尾數規范成[1,2)之間,所以需要在尾數表示成0.0000xx的形式。這種表示形式稱為不規范的形式。其會影響CPU的浮點計算速度。并且由于這種數非常接近0,所有有時將其表示成0并不會影響計算的結果。所以CPU的浮點控制器有2個用于控制對于不規范數處理的選項。FTZ用于將計算結果中的不規范數表示成0,DAZ用于在讀入不規范數時將其表示成0。Intel編譯器提供了內置的宏來方便用戶設置這兩個模式。這兩個宏分別是_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON)和_MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON)。用戶在程序中設置了這兩個模式將有助于提高浮點計算速度。但是實際上對于本程序,由于已經使用了/O3以及SSE指令集優化。所以Intel編譯器已經設置好了FTZ模式,用戶不必另外設置FTZ。并且由于本程序中所有的數都是計算得來的,所以只要計算時使用了FTZ,那讀取數據時就不會碰到不規范的數據,所以用戶也沒必要設置DAZ。
?
?
編譯器報告
編譯器報告雖然不能直接提供優化,但是卻可以讓用戶了解編譯器處理程序的信息,給用戶更改源代碼提供了很多有用的信息。對于本程序,向量化是非常重要的一步,而編譯器報告可以指出某個地方是由于什么原因造成沒有向量化。所以本使用使用/Qvec-report3參數對向量優化進行報告。
使用Intel編譯器函數進行精確時間測量
Intel編譯器提供了許多特殊的函數。這類函數一般都對應一條或者幾條匯編語言。其可以讓用戶以比匯編語言方便的方式寫出性能接近匯編語言的代碼。其中最主要的是對SIMD類指令的支持。當然其中還有很多其他功能的函數。比如_rdtsc()函數。
需要注意的是要使用這些函數必需打開/Oi選項。這個選項默認是打開的。
當程序需要進行精確時間測量,比如優化后需要知道某段特定的代碼到底快了多少毫米時,使用Windows的時間函數已經無法滿足精度要求。這是用戶可以使用Intel?VTune?Analyzers進行測量(具體使用方法將在后面介紹)。其實CPU已經提供了一個特殊的機器指令rdtsc,使用這條指令可以讀出CPU自從啟動以來的時鐘周期數。由于現在的CPU主頻已經是上GHz了。所以,其計時精度可以達到納秒級。Intel提供的_rdtsc()函數使得用戶不必再使用匯編語言,可以像調用函數一樣得到CPU的時鐘周期數。例子代碼如下:
注:以下代碼摘自“Intel?C++?Compiler?Documentation”
?
| #include?<stdio.h> int?main() { ?__int64?start,?stop,?elaspe; ?int?i; ?int?arr[10000]; ?start=?_rdtsc();? ?for(i=0;?i<10000;?i++) ?{ ????arr[i]=i; ?} ?stop=?_rdtsc(); ?elaspe?=?stop?-start; ?printf("Processor?cycles/n?%I64u/n",?elaspe); ?return?0; } |
優化結果
經過以上編譯器選項的調整,程序的運行速度已經達到了2.25秒。
1.3?使用Intel?VTune?Analyzers進行性能分析
1.3.1?Intel?VTune?Analyzers概述
Intel?VTune?Analyzers用于監視程序或者系統的各種性能,從而為用戶優化程序提供有價值的數據。同時Intel?VTune?Analyzers也能分析其收集的信息,給出用戶優化程序的建議。Intel?VTune?Analyzers即支持本地的數據收集,也支持遠程的數據收集。在本程序中,我們只需使用其本地數據收集功能。Intel?VTune?Analyzers共支持3種數據收集機制。每種機制都有其自己的適用范圍,詳細介紹如下:
u???????SAMPLING:其通過使用CPU內部的監視功能來檢測系統底層的各種性能事件。使用這個功能無需在執行代碼中插入特定的指令,因此其幾乎沒有探針效應。其無法給出函數間的調用關系。但是可以把相應的事件關聯到程序中某行源代碼或者匯編代碼上。該方法通常適用于對某段程序的微調或者針對特定性能事件的調整上。
u???????CALL?GRAPH:其通過在程序中插入特殊的指令,來記錄每個函數執行的時間。函數間的調用關系等。其有一定的探針效應。該方法通常用于對于整個比較龐大的程序,進行分析,找出其中具有性能瓶頸的函數。
u???????COUNTER?MONITOR:其無需在程序內部插入特殊的指令,因此其幾乎沒有探針效應。該方法即無法顯示函數間的調用關系,也沒法把事件定位到具體的某行代碼中。該方式是用于測試整個系統的某些性能,比如CPU占用率,內存帶寬等。通常用于系統級的調試。
對于本程序。由于程序結構簡單。無需進行函數間調用的分析。而主要需要進行基于特定代碼的分析。特別是后期需要針對CPU內部的事件特性進行源代碼級甚至是匯編級的調試。所以本次優化主要采用SAMPLING方式。
1.3.2基于SAMPLING方式的分析
原理:Intel的CPU有一組性能檢測寄存器,由于記錄各種影響性能的事件。程序首先通過編程設定需要檢測的事件,并且設定觸發中斷的計數值。當CPU中被檢測的事件達到預設的值后觸發相應的中斷。Intel?VTune?Analyzers中的SAMPLING就是使用CPU的性能檢測功能幫助用戶分析程序的性能。其中有關于內存訪問的事件,分支預測的事件,指令執行的事件等等。由于不同的CPU支持不同的性能事件,所以在不同的CPU上使用VTune時,所能監視的事件并不相同。
使用注意事項:SAMPLING一共支持2種統計。一種是Event,其是直接測量得到的值。另外一種是Event?Ratio,其是基于多個Event計算得到的,有時更有實際意義,更直觀。需要注意的是,每個Event都有一個預設的值,當這個預設的值到了以后,CPU引起中斷,VTune進行統計。而這個值的設置不能太大,否則統計到的事件不夠多,無法分析。也不能太小,否則頻繁引起中斷,會加大探針效應。用戶可以在每個Event上手工設置合適的Sample?After值,也可以通過選項卡上的選項,讓VTune先運行一遍程序,然后根據實際的事件數量來校準觸發值。對于本程序,這點尤其需要引起注意。因為本程序優化到后面時間非常短,如果不校準觸發值,分析的效果會不理想。需要注意的是Clockticks和Instructions?Retired這兩個最基本的事件,默認是不校準觸發值的,我們需要把他們調整成自動校準。此外對于某個Event的發生,大部分的中斷點并不是精確的。即真正發生該事件的指令在所記錄事件指令的前幾條。但是有一部分屬于精確事件,引起這類事件的指令正好是發生中斷的前一條。
1.3.3對于本次程序的分析
本程序首先使用VTune最基本的3個事件(Clockticks、Instructions?Retired和CPI)進行程序耗時分析。其結果如圖:
說明程序中耗時最長的是computePot函數。
1.4?優化computePot函數
在對computePot函數向量化前,我們可以注意到distx,disty,distz三個變量都是臨時變量。先將這3個變量去掉,從而可以使得Intel編譯器能夠更靈活得進行中間結果優化。另外最完成循環的i雖然是從0開始的,但是實際0和1并不進行計算,所以把外層循環的i設置層從2開始。代碼如下:
?
| ???for(?i=2;?i<NPARTS;?i++?)?{ ??????for(?j=0;?j<i-1;?j++?)?{ ????????dist?=?sqrt(?(r[0][j]?-?r[0][i])*(r[0][j]?-?r[0][i])?+?(r[1][j]?-?r[1][i])*(r[1][j]?-?r[1][i])?+?(r[2][j]?-?r[2][i])*(r[2][j]?-?r[2][i])?); ????????pot?+=?1.0?/?dist; ??????} ???} |
此時編譯器顯示內層循環已經向量化了。但是這個絕非我們的目標。為了提高計算開根號倒數的速度,為了使用Intel?Math?Kernel?Library,我們需要把開根號倒數的計算先存在一組向量中,再一同計算。既將dist變量變成,dist數組,然后再對dist數組統一計算,再求和。代碼如下:
?
| ???for(?i=2;?i<NPARTS;?i++?)?{ ??????for(?j=0;?j<i-1;?j++?)?{ ???????????dist[j]?=?(r[0][j]?-?r[0][i])*(r[0][j]?-?r[0][i])?+?(r[1][j]?-?r[1][i])*(r[1][j]?-?r[1][i])?+?(r[2][j]?-?r[2][i])*(r[2][j]?-?r[2][i]); ??????} ??????for(?j=0;?j<i-1;?j++?)?{ ??????????????dist[j]?=?1.0?/?sqrt(dist[j]); ??????} ??????for(?j=0;?j<i-1;?j++?)?{ ??????????????pot?+=?dist[j]; ??????} ???} |
Intel編譯器提示,內部的3個循環都進行了向量化。此時出現了令人驚喜的成績。程序的執行時間突然降到了1.453秒。使用VTune進行分析,發現Intel編譯器對于開根號倒數的計算自動調用了內部的向量化代碼庫。注意此時,還沒有使用Intel?Math?Kernel?Library,所以這個向量代碼庫是Intel編譯器內置的,雖然效率沒有使用Intel?Math?Kernel?Library高,但是速度已經提高了很多。調用Intel編譯器內置的向量庫的結果如圖:
1.5?使用Intel?Math?Kernel?Library
Intel?Math?Kernel?Library中提供了一部分的向量函數(Vector?Mathematical?Functions)。這類函數提供了對于普通數學計算函數的快速的向量化計算。VML中有一個向量函數就是計算開根號倒數的。
Intel的VML庫中提供了如下函數來計算整個向量中各個數的開根號倒數:
vdInvSqrt(?n,?a,?y?)
其中n表示計算的元素個數。a是指向輸入計算數據數組的頭指針。y是指向輸出計算數據數組的頭指針。其中a和y可以相同。
要使用該函數,首先需要在頭文件中包含”mkl.h”,并且鏈接mkl_c.lib文件和libguide40.lib文件。
除了基本計算功能外,VML還提供了一個設置模式的函數,用于設置特定的計算模式:
vmlSetMode?(?mode?)
其中的mode是一個預定義宏。在我們的程序中,需要設置如下模式:
VML_LA:VML的所有向量函數都提供了2個精度的版本。精度低的版本計算速度也相對比較快。本程序只需要保留小數點后7位精度。低精度的版本符合要求,所以設定VML使用低精度的版本。
VML_DOUBLE_CONSISTENT:該選項用于控制FPU的計算精度為double,其實由于我們這次使用的函數基本上是使用SSE2指令集進行計算的,和FPU沒什么關系。但是也可能存在使用FPU的可能,所以設定VML使FPU的精度為double。
VML_ERRMODE_IGNORE:該選項用于關閉VML的錯誤處理功能,本程序不需要進行錯誤處理。
VML_NUM_THREADS_OMP_FIXED:VML函數都能使用OpenMP,根據特定的硬件環境進行并行化。而我們并不需要其進行并行化。所以使用該選項和前面提到的omp_set_num_threads(1)結合。關閉VML的自動并行化功能。
具體的代碼如下:
?
| ???for(?i=2;?i<NPARTS;?i++?)?{ ??????for(?j=0;?j<i-1;?j++?)?{ ???????????dist[j]?=?(r[0][j]?-?r[0][i])*(r[0][j]?-?r[0][i])?+?(r[1][j]?-?r[1][i])*(r[1][j]?-?r[1][i])?+?(r[2][j]?-?r[2][i])*(r[2][j]?-?r[2][i]); ??????} ????????vdInvSqrt(i-1,dist,dist); ??????for(?j=0;?j<i-1;?j++?)?{ ??????????????pot?+=?dist[j]; ??????} ???} |
優化后出現了令人可惜可賀的成績:0.796秒。
1.6?根據Cache大小優化Intel?Math?Kernel?Library調用
在上面的程序中對于MKL函數的調用是每次內部循環都執行一次調用,我們知道每次執行函數的調用都是需要開銷的,那是否有更優化的調用MKL方法那?下面這段話摘自Intel?Math?Kernel?Library的說明文檔上:
?
| There?are?two?extreme?cases:?so-called?"short"?and?"long"?vectors?(logarithmic?scale?is?used?to?show?both?cases).?For?short?vectors?there?are?cycle?organization?and?initialization?overheads.?The?cost?of?such?overheads?is?amortized?with?increasing?vector?length,?and?for?vectors?longer?than?a?few?dozens?of?elements?the?performance?remains?quite?flat?until?the?L2?cache?size?is?exceeded?with?the?length?of?the?vector. |
下面這副性能分析圖片摘自Intel?Math?Kernel?Library的網站上:
從這段文字和這副圖片中,我們了解到對于MKL函數的調用時,所處理的向量不能太短,否則函數的建立時間開銷將是非常大的,也不能太長,操作了L2?Cache,否則函數執行時訪問內存的開銷是很大的。并且通過圖片了解到不合適的長度對于函數的性能將產生指數級影響。
根據理論計算:每次執行computePot函數,總共需要執行的計算量為(1+998)*998/2=498501個。每個double類型占用8個字節,所有總共需要占用的空間為498501*8=3988008byte=3894KB。而這次進行競賽的測試平臺的CPU的L2?Cache大小為2M,由于有2個線程同時計算,平均每個線程分到的L2?Cache為1M。由于L2?Cache可能還被其他數據占據。所以為了保證所計算的數據在L2?Cache中,最好每次計算的向量長度在512KB左右。故把整個computePot函數的計算量分成8份。每份計算量的中間結果向量長度為3894KB/8=486KB。
但是實際情況并非如此,進行這種優化后,程序的執行速度反而降低了。通過分析發現原來CPU中的L1?Cache大小為32KB。數組r有3000個元素,如果每次迭代都進行vdInvSqrt調用。那dist的長度為1000個元素左右。加起來正好可以全部在L1?Cache中。而如果合并起來調用vdInvSqrt,則由于vdInvSqrt過長。其L1?Cache中存放不下,需要存放在L2?Cache中,從而反而影響了速度。看來,對于本程序,不應該根據L2?Cache進行優化,而應該根據L1?Cache進行優化。但是對于只有幾個或者幾十個數據就調用MKL函數,其開銷還是很大的。因此本程序使用了折中的方法,對于前面非常小的幾十個數據,湊足1000個放在一起進行計算,而后面的數據還是按照原來的方式計算。具體實現的代碼如下:
?
| ???for(?i=2,k=0;?i<47;?i++?)?{ ??????for(?j=0;?j<i-1;?j++,k++?)?{ ?????????dist[k]?=?(r[0][j]?-?r[0][i])*(r[0][j]?-?r[0][i])?+?(r[1][j]?-?r[1][i])*(r[1][j]?-?r[1][i])?+?(r[2][j]?-?r[2][i])*(r[2][j]?-?r[2][i]); ??????} ???} ???vdInvSqrt(k,dist,dist); ???for(?j=0;?j<k;?j++?)?{ ??????pot?+=?dist[j]; ???} ???for(?i=47;?i<NPARTS;?i++?)?{ ??????for(?j=0;?j<i-1;?j++?)?{ ?????????dist[j]?=?(r[0][j]?-?r[0][i])*(r[0][j]?-?r[0][i])?+?(r[1][j]?-?r[1][i])*(r[1][j]?-?r[1][i])?+?(r[2][j]?-?r[2][i])*(r[2][j]?-?r[2][i]); ??????} ??????vdInvSqrt(i-1,dist,dist); ??????for(?j=0;?j<i-1;?j++?)?{ ?????????pot?+=?dist[j]; ??????} ???} |
通過該不優化,程序的性能略微有所提高,達到了0.781秒。
1.7?優化updatePositions函數
雖然updatePositions函數執行的時間非常短。但還是值得優化的。
首先進行的是基于數學的優化。我們發現在updatePositions和initPositions中,都有加0.5的計算。但是從后面的computePot的相減計算中發現,這個0.5是被抵消的,既不加0.5對結果沒有影響。故去掉該加0.5的計算。另外updatePositions和initPositions中都有除以RAND_MAX的計算。而通過提取公因子的變換發現,如果此處不除以RAND_MAX而將最后的pot乘以RAND_MAX,則最后結果相同。故去掉該處的除以RAND_MAX的計算,而以在pot上一次乘以RAND_MAX為替換。具體代碼如下:
?
| void?initPositions()?{ ???int?i,?j; ? ???for(?i=0;?i<DIMS;?i++?) ??????for(?j=0;?j<NPARTS;?j++?) ?????????r[i][j]?=?(double)?rand(); } void?updatePositions()?{ ???int?i,?j; ? ???for(?i=0;?i<DIMS;?i++?) ??????for(?j=0;?j<NPARTS;?j++?) ?????????r[i][j]?-=?(double)?rand(); } 在main函數中: ??????pot?=?0.0; ??????computePot(); ??????pot*=(double)RAND_MAX; ??????if?(i%10?==?0)?printf("%5d:?Potential:?%20.7f/n",?i,?pot); ? |
其次需要進行updatePositions內rand函數的優化。雖然rand函數本身的執行時間非常短,但是其頻繁得進行調用卻影響了性能。通過查找Microsoft?Visual?Studio?.NET?2005中提供的源代碼。將其中的rand函數提取出來,進行必要的修改,并且加上inline屬性。從而加快程序的調用速度。具體代碼如下:
?
| int?holdrand=1; inline?int?myrand?(){ ????????return(?((holdrand?=?holdrand?*?214013L+?2531011L)?>>?16)?&?0x7fff?); } ? |
經過上述優化,代碼的執行速度已經達到了0.765秒。
1.8?其他優化以及性能分析
至此,該程序串行優化部分已經一本完成。但是還有一點細小的地方需要優化。
變量對齊對于數據讀取速度是非常重要的。尤其是使用SIMD指令集進行優化后,對于對齊的變量,可以使用對齊的讀寫指令提高速度。一般對于SIMD指令需要進行16字節對齊。但是對于本程序,由于后面要進行多線程優化,而多線程執行時基于Cache?Line的共享沖突會對讀寫造成很大的損失。故本程序使用64字節對齊。代碼如下:
?
| __declspec(align(64))?int?holdrand=1; __declspec(align(64))?double?r[DIMS][NPARTS]; __declspec(align(64))?double?pot; __declspec(align(64))?double?dist[1048]; |
在computePot函數的第一次迭代中。有一處進行pot累加的地方,使用了k變量作為循環條件。但是其實該變量的確切值是可以計算出來的。通過計算出該變量的確切值,可以讓Intel編譯器在編譯時就知道循環的次數,從而有助于優化。具體代碼如下(注意1035這個值):
?
| ???for(?i=2,k=0;?i<47;?i++?)?{ ??????for(?j=0;?j<i-1;?j++,k++?)?{ ?????????dist[k]?=?(r[0][j]?-?r[0][i])*(r[0][j]?-?r[0][i])?+?(r[1][j]?-?r[1][i])*(r[1][j]?-?r[1][i])?+?(r[2][j]?-?r[2][i])*(r[2][j]?-?r[2][i]); ??????} ???} ???vdInvSqrt(k,dist,dist); ???for(?j=0;?j<1035;?j++?)?{ ??????pot?+=?dist[j]; ???} |
此外再調整以下編譯器的某些優化參數,選擇合適的使用。比如使用哪個編譯級別,是否打開全局優化,使用IPO,使用GPO等。
至此本程序的串行優化全部完成。使用Intel?VTune?Analyzers的分析結果為:
?
| Full?Name | CPI | Clockticks?events | Clockticks?% |
| void?updatePositions(void) | 3.214080375 | 8274621 | 0.287907869 |
| int?computePot(void) | 1.294881302 | 926757552 | 32.24568138 |
| mkl_vml_core_t7_vml_dInvSqrt_50 | 0.91981472 | 1925228486 | 66.9865643 |
(注:此分析數據是在奔騰D?820上測得)
從以上數據上表明updatePositions函數說執行的事件非常短,低于1%,computePot函數的執行時間在三分之一左右。mkl_vml_core_t7_vml_dInvSqrt_50的執行時間在三分之二左右。這些數據對下面一步并行化采用的策略是非常重要的。
?
二、并行優化
2.1?并行優化概述
在進行本程序的并行優化前先談談并行優化需要注意的問題。在并行優化中經常用到數據重復和計算重復的方法。所謂數據重復,就是為了保證多個線程能同時進行計算,就把數據復制多份來提高并行度。所謂計算重復,就是有時使用計算換通信的方法,提高并行度。
在對本程序進行優化前需要注意的是。測試平臺使用的是基于Core?Microarchitecture結構的。這個結構的雙核CPU是共享L2?Cache的。但是當數據在一個核中進行修改,另外一個核去讀他時,需要消耗幾十個時鐘周期的延遲。其代價的非常高的。這里需要注意的是,數據在Cache中是按行進行存放的,也就是說,CPU看待數據有沒有被修改過是根據Cache?Line的。所以2個分別被不同的核修改的數據如果存在于同一行Cache中,訪問時的效率就會非常低。也就是發生了共享沖突。所以在分配變量時要盡量把不同性質的變量分配到不同的Cache?Line中。我們的測試平臺的L1?Cache和L2?Cache都是每行64byte的。所以前一章中的變量對齊都使用了64byte對齊。同樣,在程序并行化時也需要考慮這種情況。
2.2?優化方案一
此方案使用數據重復的方法。程序可以定義2個r數組。以及2個pot數組。通過定義2個r數組,使得主線程可以在從線程使用一個r數組計算時同時更新第二個r數組。即主線程先更新r數組,然后主線程和從線程同時開始計算。但是從線程的計算量比主線程大一點。這樣當主線程計算完后,可以繼續更新第二個r數組,而此時從線程還在計算原來r數組的內容。當主線程更新完第二個r數組時,從線程正好完成前面的計算,并和主線程一同計算第二個r數組,依次類推。同時2個pot數組,一個給主線程計算每步的中間結果,另一個給從線程計算每步的中間結果。等計算結束后,再將其結果相加,打印。
優點:使用該方法的優點是顯而易見的,理論上線程可以做到完全同步。
缺點:使用該方法的缺點是,從線程每次計算需要從主線程計算好的r數組中讀取內容,由于是2個核,所以其訪問延遲非常大。此外使用2個數值,每次迭代都需要將指針指向使用的數組,增加了程序的設計難度。同時計算任務分配的調優也是非常繁瑣的。
由于在前一章中,我們發現updatePositions函數所花費的時間非常短。所以做到線程間的完全平衡意義并不大。
2.3?優化方案二
在前一個方案中,我們提到了線程的完全平衡的算法。同時我們發現完全平衡的意義不大。因此我們設計適合本程序的更優的方案。既然updatePositions函數所花費的時間非常短。那2個線程同時執行updatePositions造成的額外開銷也是可以忽略的。本方案使用了數據重復和計算重復的方法。同樣使用2個r數組,但是2個線程同時進行重復計算,并且2個線程分區完成不同的迭代步驟的computePot計算。即主線程完成整個r數組的更新,但是只計算其中的奇數次迭代。從線程同樣完成整個r數組的更新,但是只進行偶數次迭代。并且同樣使用了一個pot數組,2個線程分別將自己的計算結果先存儲到pot數組中。等最后同步的時候再打印。
優點:使用該方案,程序的設計相對來說比較簡單,負載均衡的調整也很容易。程序只需要很少的同步操作(在本程序中,只使用了2次同步)。并且重要的是。由于2個線程都在各自的CPU上使用各自的數據進行計算,所以最大化得避免了共享沖突的發生。同時也保留了前一章優化中針對L1?Cache的命中率。
缺點:該方案的缺點是存在重復計算。但是通過前面VTune的測試,已經發現其重復計算量非常小,可以忽略。
2.4?并行實現
本程序使用方案二進行并行化。首先將所有需要計算的數據和函數都復制2份,代碼如下:
?
| int?computePot1(void); void?initPositions1(void); void?updatePositions1(void); int?computePot2(void); void?initPositions2(void); void?updatePositions2(void); __declspec(align(64))?int?holdrand1=1; __declspec(align(64))?double?r1[DIMS][NPARTS]; __declspec(align(64))?double?pot1; __declspec(align(64))?double?dist1[1048]; __declspec(align(64))?int?holdrand2=1; __declspec(align(64))?double?r2[DIMS][NPARTS]; __declspec(align(64))?double?pot2; __declspec(align(64))?double?dist2[1048]; __declspec(align(64))?double?potfinal[264]; ? |
其中的potfinal數組記錄每次迭代的計算結果,用于最后的數組。
在主函數的并行中。我們發現由于偶數次迭代比奇數次迭代需要多算一次。故本程序的偶數次迭代在進行到快完成前先釋放一個同步鎖。使得主線程可以先輸出一部分數據。而從線程在執行完所有的偶數次迭代后再釋放一個同步鎖,使主線程輸出剩余的數據。由于輸出數據也有一點的耗時。所以使用這種方法可以提高一點并行度。另外在本代碼中使用了SetThreadAffinityMask分別設置不同的線程對應各自的CPU,以防止線程在不同的CPU中切換從而影響L1?Cache命中率。具體代碼如下:
?
| DWORD?WINAPI?mythread(?void?*myarg?){ ?????int?i; ?????SetThreadAffinityMask(GetCurrentThread(),?2); ?????initPositions2(); ?????updatePositions2(); ?????for(i=0;i<=190;i+=2){ ????????pot2?=?0.0; ????????computePot2(); ????????pot2*=(double)RAND_MAX; ????????potfinal[i]=pot2; ????????updatePositions2(); ????????updatePositions2(); ?????} ?????ReleaseSemaphore(semmiddle,?1,?NULL); ?????for(i=192;i<=NITER;i+=2){ ????????pot2?=?0.0; ????????computePot2(); ????????pot2*=(double)RAND_MAX; ????????potfinal[i]=pot2; ????????updatePositions2(); ???????updatePositions2(); ?????} ?????ReleaseSemaphore(semafter,?1,?NULL); ?????return?0; }//從線程 |
?
| int?main()?{ ???int?i; ???int?myarg=0; ???clock_t?start,?stop; ???omp_set_num_threads(1); ???vmlSetMode(VML_LA); ???vmlSetMode(VML_DOUBLE_CONSISTENT); ???vmlSetMode(VML_ERRMODE_IGNORE); ???vmlSetMode(VML_NUM_THREADS_OMP_FIXED); ???semmiddle?=?CreateSemaphore(NULL,?0,?1,?NULL); ???semafter?=?CreateSemaphore(NULL,?0,?1,?NULL); ???CreateThread(0,?8*1024,?mythread,?(void?*)&myarg,?0,?NULL); ???SetThreadAffinityMask(GetCurrentThread(),?1); ???initPositions1(); ???start=clock(); ???for(i=1;i<NITER;i+=2){ ????????pot1?=?0.0; ????????updatePositions1(); ????????updatePositions1(); ????????computePot1(); ????????pot1*=(double)RAND_MAX; ????????potfinal[i]=pot1; ???} ???WaitForSingleObject(semmiddle,?INFINITE); ???for(i=0;i<=190;i+=10) ????????printf("%5d:?Potential:?%20.7f/n",?i,?potfinal[i]); ???WaitForSingleObject(semafter?,?INFINITE); ???i=200; ???printf("%5d:?Potential:?%20.7f/n",?i,?potfinal[i]); ???stop=clock(); ???printf?("Seconds?=?%10.9f/n",(double)(stop-start)/?CLOCKS_PER_SEC); }//主線程 |
2.5?性能分析
并行化后的性能并不沒有像理論中這么高只有0.437秒。于是我們開始查找原因。通過使用Intel?Threading?Checker我們發現,VML庫中存在著訪問沖突。圖片如下:
?
當然這個錯誤有可能是Intel?Threading?Checker的誤報。因為程序每次執行都沒有發現不正確的結果,并且VML函數的文檔上說明是線程安全性的。
由于兼容性原因,本系統無法使用Intel?VTune?Analyzers進行每個函數的耗時分析。于是使用Intel編譯器提供的內置函數_rdtsc()記錄不同部分所花費的CPU時鐘周期。結果發現VML函數的總執行時間大概增加了0.088秒左右。說明VML函數在用戶使用Windows?Thread函數并行化訪問時,其同步開銷可能有一定的影響。
?
三、匯編級優化
3.1?優化目標
本程序主要的執行時間在computePot函數與VML庫中。對于computePot函數,通過查看Intel編譯器產生的匯編碼發現其已經很優了。而對于VML函數由于其需要滿足通用性,所以本程序應該可以設計出最適合本程序的計算函數來。
3.2?數學理論
Intel的CPU支持的SSE2指令中,有2條是用于計算雙精度浮點的開根號倒數的。sqrtpd指令可以同時計算2個double型的開根號,其吞吐率為28個時鐘周期。divpd指令用于計算2個數的除法,即用于計算倒數,其吞出率為17個時鐘周期。由此可以計算出,如果當當使用這2條指令計算雙精度數的開根號倒數,那即使使用匯編語言,忽略其他開銷。計算每個元素的時鐘周期也有(17+28)/2=22.5。而Intel的VML庫計算每個元素的只需要10多個時鐘周期,說明其肯定是通過其他快速的數學計算方法得到的。所以要優化vdInvSqrt函數,關鍵是找到更快速的數學計算方法。在Quake?3在源代碼中有如下一段具有傳奇色彩的代碼:
?
| float?InvSqrt(float?x){ ???????float?xhalf?=?0.5f*x; ???????int?i?=?*(int*)&x;?//?get?bits?for?floating?value ???????i?=?0x5f3759df?-?(i>>1);?//?gives?initial?guess?y0 ???????x?=?*(float*)&i;?//?convert?bits?back?to?float ???????x?=?x*(1.5f-xhalf*x*x);?//?Newton?step,?repeating?increases?accuracy ???????return?x; } |
(注:以上代碼的注釋摘自CHRIS?LOMONT的《FAST?INVERSE?SQUARE?ROOT》文章中)
在上面的代碼中最后一條是典型的牛頓迭代,可以根據精度要求進行多次迭代。這段代碼神奇的地方在于初始值的估算上,只用了減法和移位2個簡單的操作,達到了非常接近的估算值。我們稱0x5f3759df為幻數(magic?number)。CHRIS?LOMONT在他的《FAST?INVERSE?SQUARE?ROOT》文章中給出了對于這個幻數的解釋和計算方法。并且計算出了理論上最優的適用于double類型的幻數為0x5fe6ec85e7de30da。說們我們的代碼中可以使用該方法進行計算,示例代碼如下:
?
| double?myinvsqrt?(double?x) { ?????double?xhalf?=?0.5*x; ?????__int64?i?=?*(__int64*)&x; ?????i?=?0x5fe6ec85e7de30da?-?(i>>1); ?????x?=?*(double*)&i; ?????x?=?x*(1.5-xhalf*x*x); ?????x?=?x*(1.5-xhalf*x*x); ?????x?=?x*(1.5-xhalf*x*x); ?????x?=?x*(1.5-xhalf*x*x); ?????return?x; } |
但是不幸的是,根據調試,需要達到比賽要求的小數點后7位精度,必需進行4此牛頓迭代也行。而4此牛頓迭代的計算量使得這個方法對于Intel的VML函數來說毫無優勢可言。那能否降低牛頓迭代的次數那?
我們發現如果以上代碼只進行3次牛頓迭代,那誤差只有小數點最后的1,2位。CHRIS?LOMONT在他的文中提到他說計算出來的理論最優值,而這個幻數只是在線性估計時是最優的。在多次牛頓迭代中,這個值并不是最優的。CHRIS?LOMONT并沒有給出對于多次牛頓迭代最優幻數的計算方法,他在文章中對于float類型的實際最優值也是窮舉得到的。我們同樣在理論最優值0x5fe6ec85e7de30da的基礎上進行了一定的窮舉操作,發現的確有更優的幻數。但是即使使用了更優的幻數,還是無法在3次牛頓迭代基礎上達到精度要求。但是我們發現所有的數值都偏小。于是我們可以在三次牛頓迭代后再乘一個比1大一點點的偏移量。從而能做到3次牛頓迭代就能達到精度要求。示例代碼如下:
?
| double?myinvsqrt?(double?x) { ?????double?xhalf?=?0.5*x; ?????__int64?i?=?*(__int64*)&x; ?????i?=?newmagicnum?-?(i>>1); ?????x?=?*(double*)&i; ?????x?=?x*(1.5-xhalf*x*x); ?????x?=?x*(1.5-xhalf*x*x); ?????x?=?x*(1.5-xhalf*x*x); ?????x?=?x*offset ?????return?x; } |
由于時間原因,這里并沒有對newmagicnum和offset進行詳細的計算與統計。只給出一個對于本程序相對較優的newmagicnum值0x5fe6d250b0000000。
在上面的代碼中只進行了3次牛頓迭代。對于Intel的VML來說也沒有什么優勢可言。那能不能再減少一次牛頓迭代,只進行2次迭代就達到精度要求那?
我們知道要進行2次牛頓迭代就達到精度要求就必須對其初始值的估計更加準確。而使用上面的方法估計的初始值已經無法滿足該準確性。這是通過查找《Intel?64?and?IA-32?Architectures?Optimization?Reference?Manual》,我們發現SSE指令集中有一條RSQRTPS的指令用于同時計算四個單精度浮點數的開根號倒數,而其在Core?Microarchitecture上的延遲為3個周期,吞吐率為2個周期。也就是說我們可以在極短的時間內就算出單精度類型的開根號倒數值(看來在現在的CPU上,當初Quake?3那段具有傳奇色彩的代碼已經沒有用了)。于是我們想到了先使用單精度類型精度初值估算,然后再使用牛頓迭代。實驗結果表明該方法只需要進行2次牛頓迭代就能滿足小數點后7位的精度要求。示例代碼如下:
?
| double?myinvsqrt?(double?x) { ?????double?xhalf?=?0.5*x; ?????float?xf=(float)x; ?????__asm{ ?????????movss?xmm1,xf; ?????????rsqrtss?xmm1,xmm1; ?????????movss?xf,xmm1; ?????} ?????x=(double)xf; ?????x?=?x*(1.5-xhalf*x*x); ?????x?=?x*(1.5-xhalf*x*x); ?????return?x; } |
不幸的是由于該代碼涉及到了復雜的算法以及類型轉換,Intel的編譯器并無法將其很好的并行化。所以只有依靠手工使用匯編語言將其優化。
3.3?匯編碼實現
在實現匯編碼前先要將原來的代碼進行優化,將牛頓迭代中的減法變成加法,代碼如下:
?
| double?myinvsqrt?(double?x) { ?????double?xhalf?=?-0.5*x; ?????float?xf=(float)x; ?????__asm{ ?????????movss?xmm1,xf; ?????????rsqrtss?xmm1,xmm1; ?????????movss?xf,xmm1; ?????} ?????x=(double)xf; ?????x?=?x*(1.5+xhalf*x*x); ?????x?=?x*(1.5+xhalf*x*x); ?????return?x; } |
進行這種轉變是一點都不影響計算結果的。但是確可以提高計算速度。這是因為,如果執行的是減法,匯編語言的減法指令會將結果存在原來存放被減數(即1.5)的寄存器中。從而覆蓋掉了原來的常數1.5,使得每次計算必須重新讀入該參數。而優化成加法后則沒有這個問題。
下面列出了本次匯編語言優化時使用的主要的匯編指令及其延遲,吞吐率和使用的計算部件。這些數據對優化匯編代碼有幫助。
| 指令名 | 延遲 | 吞吐率 | 計算部件 |
| movapd | 1 | 0.33 | FP_MOVE |
| cvtpd2ps | 4 | 1 | FP_ADD,MMX_SHFT |
| cvtps2pd | 2 | 2 | FP_ADD,MMX_SHFT,MMX_ALU |
| shufps | 2 | 1 | MMX_SHFT |
| rsqrtps | 3 | 2 | MMX_MISC |
| mulpd | 5 | 1 | FP_MUL |
| addpd | 3 | 1 | FP_ADD |
(注:以上數據摘自《Intel?64?and?IA-32?Architectures?Optimization?Reference?Manual》)
在進行優化前,還有一點需要注意的是。rsqrtps函數是4個元素一算的,所以本程序使用4個元素作為一次計算單元來向量化。而用戶輸入的數據并不可能是正好4個元素。對于Intel編譯器以及VML函數庫來所,其使用的解決方法稱為”?Strip-mining?and?Cleanup”。即先按照4個數據一組進行計算。對于剩下的個別數據再進行單獨計算。這對于通用化的程序來說是必須的。但是在我們的程序中,多計算幾個并不會影響結果。而對于單獨幾個的數據如果另外處理不但會增加程序設計的復雜性,而且性能也可能會降低。所以本程序使用過渡計算的方法。即對于需要計算的數據中不足4個的,補滿4個將其后面的數據計算掉。但是此時需要注意,由于dist變量是全局變量,默認的值為全0。如果過渡計算遇到0的值,速度可能會受到影響。所以本程序需要在一開始,將會被過渡計算使用到,但是從來不會被初始化的存儲單元,初始化成1。具體代碼如下:
?
| void?myinvsqrt?(double?*start,double?*end) { ?????__asm{ ?????????mov?esi,start; ?????????mov?edi,end; ?????????test?edi,0x0000001f; ?????????jz?myalign; ?????????and?edi,0xffffffe0; ?????????add?edi,32; myalign: myagain: ?????????movapd?xmm0,[esi]; ?????????movapd?xmm3,[esi+16]; ?????????cvtpd2ps?xmm6,xmm0; ?????????cvtpd2ps?xmm7,xmm3; ?????????shufps?xmm6,xmm7,01000100b; ?????????rsqrtps?xmm6,xmm6; ?????????cvtps2pd?xmm1,xmm6; ?????????shufps?xmm6,xmm6,01001110b; ?????????cvtps2pd?xmm4,xmm6; ?????????mulpd?xmm0,mulcc; ?????????mulpd?xmm3,mulcc; ? ?????????movapd?xmm2,xmm1; ?????????movapd?xmm5,xmm4; ?????????mulpd?xmm1,xmm1; ?????????mulpd?xmm4,xmm4; ?????????mulpd?xmm1,xmm0; ?????????mulpd?xmm4,xmm3; ?????????addpd?xmm1,addcc; ?????????addpd?xmm4,addcc; ?????????mulpd?xmm1,xmm2; ?????????mulpd?xmm4,xmm5;//前半段 |
?
?
| ?????????movapd?xmm2,xmm1; ?????????movapd?xmm5,xmm4; ?????????mulpd?xmm1,xmm1; ?????????mulpd?xmm4,xmm4; ?????????mulpd?xmm1,xmm0; ?????????mulpd?xmm4,xmm3; ?????????addpd?xmm1,addcc; ?????????addpd?xmm4,addcc; ?????????mulpd?xmm1,xmm2; ?????????mulpd?xmm4,xmm5; ?????????movapd?[esi],xmm1; ?????????movapd?[esi+16],xmm4; ? ?????????add?esi,32; ?????????cmp?esi,edi; ?????????jne?myagain; ?????} } //后半段 ? myinvsqrt(dist1,dist1+k);?//調用方法 |
對于本函數的調用方法為分別傳入其需要計算數據的頭指針和尾指針。
3.4?性能分析
使用匯編語言優化后,程序跑出了驚人的0.312秒的好成績。并且所有的輸出數據全部都滿足小數點后7位的精度要求。在使用Intel?Threading?Checker和Intel?Threading?Profiler分析程序時也得到了相對比較好的結果。如下圖:
?
在Intel?Threading?Checker的檢測中,沒有發現程序有任何沖突。在使用Intel?Threading?Profiler的分析中,表現出了程序良好的并行性。
最后,在另外一臺Intel酷睿2?E6600的機器上測試時,程序達到了0.25秒的好成績,并且所有數據輸出精度都達到了小數點后7位。
3.5?總結
在本次優化比賽中。我花了幾個星期仔細鉆研Intel的工具使用方法,并且結合Intel的CPU特性對源代碼進行優化。在經過了漫長的調優后,終于在保留小數點后7位精度的七位精度的情況下達到了0.25秒的成績。這里需要說明的是,在本程序的最后優化時雖然沒有使用Intel的VML庫,但并不是意味著VML庫不好。VML庫的通用化和高效率是有目共睹的。而是由于VML庫是通用庫,其需要考慮很多情況,而針對本程序自己設計的計算函數卻不用考慮各自情況。所以設計有針對性的函數才能提高速度。當然要設計這種函數對用戶的要求太高,需要了解數學理論,匯編語言,以及優化的方法。所以對于一般的用戶還是使用VML庫比較好。
最后需要說明的是,由于本次競賽的時間有限。很多地方都沒有得到更好的優化。比如那段匯編語言就能針對Intel?CPU的指令延遲特性進一步優化。如果大賽能給出更多的時間,那有可能可以優化得更好。
?
?
附錄A?目錄結構和編譯方法
本報告壓縮文件的根目錄下分別有version1,version2,version3三個目錄,分別對應第一,第二,第三章中的,串行優化,并行優化,匯編優化三個不同階段的版本。其中version3是最終版。
在每個version目錄下,有Microsoft?Visual?Studio的項目文件,可以使用Microsoft?Visual?Studio直接打開。在對應的Release目錄下有已經編譯好的可執行文件。程序的優化選項可以在Microsoft?Visual?Studio中看到,具體的解釋可以在第一章中查找。
此外,在壓縮文件的根目錄下有最終板的可執行文件potential_serial(final).exe方便進行測試。
?
附錄B?參考文獻
Intel?C++?Compiler?Documentation
Intel?MKL?Reference?Manual
Intel?MKL?Technical?User?Notes
Getting?Started?Guide?for?Intel?MKL
Getting?Started?with?the?VTune?Performance?Analyzer
VTune?Performance?Environment?Help
Intel?Thread?Profiler?for?Windows
Intel?Thread?Checker?for?Windows
Intel?64?and?IA-32?Architectures?Software?Developer’s?Manual?Volume?1?Basic?Architecture
Intel?64?and?IA-32?Architectures?Software?Developer’s?Manual?Volume?2A?Instruction?Set?Reference,?A-M
Intel?64?and?IA-32?Architectures?Software?Developer’s?Manual?Volume?2B?Instruction?Set?Reference,?N-Z
Intel?64?and?IA-32?Architectures?Software?Developer’s?Manual?Volume?3A?System?Programming?Guide
Intel?64?and?IA-32?Architectures?Software?Developer’s?Manual?Volume?3B?System?Programming?Guide
Intel?64?and?IA-32?Architectures?Optimization?Reference?Manual
Using?Spin-Loops?on?Intel?Pentium?4?Processor?and?Intel?Xeon?Processor
FAST?INVERSE?SQUARE?ROOT????CHRIS?LOMONT
總結
以上是生活随笔為你收集整理的英特尔多核平台编程优化大赛报告的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 预编码的基本原理
- 下一篇: 使用ESP32读取数字硅麦的数据