电竞比分网-中国电竞赛事及体育赛事平台

分享

英特爾多核平臺(tái)編程優(yōu)化大賽報(bào)告

 jijo 2009-05-06

原創(chuàng) 英特爾多核平臺(tái)編程優(yōu)化大賽報(bào)告收藏

 該文檔從word中直接粘貼,少了圖片。
 在寫該文檔時(shí)比較匆忙,其中有很多打錯(cuò)的字,還請(qǐng)見(jiàn)涼。
 如果發(fā)現(xiàn)該文檔有相關(guān)內(nèi)容有錯(cuò)誤,還望指出。
 
 
 
 
 
 
英特爾多核平臺(tái)編程優(yōu)化大賽報(bào)告
 
 
 
 
 
 
 
 
 
代碼優(yōu)化前所需時(shí)間:4.765
代碼優(yōu)化后所需時(shí)間:0.25秒(保留小數(shù)點(diǎn)后7位精度)
 
 

 
 
 
 
 
 
 
前言
本次優(yōu)化使用的CPU是Intel Xeon 5130 主頻為2.0GHz 同Intel酷睿2一樣是基于Core Microarchitecture 的雙核處理器。本次優(yōu)化在Intel的工具幫助下主要針對(duì)Core Microarchitecture 系列處理器進(jìn)行優(yōu)化。但是由于未知原因,Intel VTune Analyzers并不能在該系統(tǒng)下正常工作。所以,所有使用Intel VTune Analyzers的測(cè)試均使用另外一個(gè)奔騰D 820的系統(tǒng)測(cè)試。
第一章主要介紹了程序的串行優(yōu)化。其中有關(guān)于Intel編譯器使用,以及Intel Math Kernel Library使用,Intel VTune Analyzers使用的介紹。在借助Intel工具的幫助下,結(jié)合Intel Core Microarchitectured的特性。設(shè)計(jì)出了針對(duì)L1 Cache進(jìn)行優(yōu)化的,高效率的串行代碼。程序的執(zhí)行時(shí)間從優(yōu)化前的4.765秒達(dá)到了優(yōu)化后的0.765秒。
第二章主要介紹了程序的并行化。首先討論了2種并行算法的優(yōu)缺點(diǎn)。然后選擇了適合本程序的并行算法進(jìn)行優(yōu)化。并且在最后分析了并行化時(shí)的性能瓶頸。通過(guò)并行化,程序達(dá)到了0.437秒。
第三章主要介紹了程序的匯編優(yōu)化。首先介紹了計(jì)算的數(shù)學(xué)理論。然后介紹了匯編代碼的編寫。最后進(jìn)行了性能分析。通過(guò)該步優(yōu)化程序在保留小數(shù)點(diǎn)后7位精度的前提下達(dá)到了0.312秒的好成績(jī)。并且在Intel酷睿2 E6600 上測(cè)試達(dá)到了0.25秒。
附錄A 說(shuō)明了本次報(bào)告的目錄結(jié)構(gòu)和優(yōu)化方法。
附錄B 列出了進(jìn)行本次競(jìng)賽所參考的文獻(xiàn)。
 

目錄
一、串行優(yōu)化.... 4
1.1 代碼的基本修改和優(yōu)化.... 4
1.2 基于Intel編譯器的優(yōu)化.... 4
1.3 使用Intel VTune Analyzers進(jìn)行性能分析.... 8
1.3.1 Intel VTune Analyzers概述... 8
1.3.2 基于SAMPLING方式的分析... 9
1.3.3 對(duì)于本次程序的分析... 9
1.4 優(yōu)化computePot函數(shù).... 10
1.5 使用Intel Math Kernel Library. 11
1.6 根據(jù)Cache大小優(yōu)化Intel Math Kernel Library調(diào)用.... 12
1.7 優(yōu)化updatePositions函數(shù).... 13
1.8 其他優(yōu)化以及性能分析.... 14
二、并行優(yōu)化.... 17
2.1 并行優(yōu)化概述.... 17
2.2 優(yōu)化方案一.... 17
2.3 優(yōu)化方案二.... 17
2.4 并行實(shí)現(xiàn).... 18
2.5 性能分析.... 20
三、匯編級(jí)優(yōu)化.... 23
3.1 優(yōu)化目標(biāo).... 23
3.2 數(shù)學(xué)理論.... 23
3.3 匯編碼實(shí)現(xiàn).... 25
3.4 性能分析.... 28
3.5 總結(jié).... 29
附錄A 目錄結(jié)構(gòu)和編譯方法.... 30
附錄B 參考文獻(xiàn).... 30
 

 
1.1 代碼的基本修改和優(yōu)化
首先根據(jù)主辦方的要求把代碼的輸出精度改為小數(shù)點(diǎn)后7位。

if (i%10 == 0) printf("%5d: Potential: %20.7f\n", i, pot);

在進(jìn)行任何優(yōu)化前代碼的執(zhí)行時(shí)間是4.765秒。
接著把項(xiàng)目轉(zhuǎn)換成使用Intel C++ Compiler,代碼的執(zhí)行時(shí)間是4.531秒。
然后執(zhí)行最基本的優(yōu)化,把代碼中的pow函數(shù)優(yōu)化成乘法。代碼如下:

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]);

執(zhí)行時(shí)間依然為4.531秒。說(shuō)明Intel編譯器已經(jīng)將pow函數(shù)優(yōu)化掉了。
1.2 基于Intel編譯器的優(yōu)化
這里介紹本程序中基于Intel編譯器優(yōu)化技術(shù)。其中有些優(yōu)化參數(shù)是可以確定的,有些優(yōu)化參數(shù)需要在程序的不同階段反復(fù)調(diào)試以確定最優(yōu)方案,而有些優(yōu)化技術(shù)是在后面的優(yōu)化中使用的。
編譯器優(yōu)化級(jí)別
Intel的編譯器共有如下一些主要的優(yōu)化級(jí)別:
u       /O1:實(shí)現(xiàn)最基本的優(yōu)化
u       /O2:基于代碼速度實(shí)現(xiàn)常規(guī)優(yōu)化,這個(gè)也是默認(rèn)的優(yōu)化級(jí)別
u       /O3:在/O2的基礎(chǔ)上實(shí)現(xiàn)進(jìn)一步的優(yōu)化,包括Cache預(yù)讀,標(biāo)量轉(zhuǎn)換等等,但是在某些情況下反而會(huì)減慢代碼的執(zhí)行速度。
u       /Ox:實(shí)現(xiàn)最大化的優(yōu)化,包括自動(dòng)內(nèi)聯(lián)函數(shù)的確定,全局優(yōu)化,使用EBP作為通用寄存器等。
u       /fast:等同于/O3, /Qipo, /Qprec-div-, and /QxP。
通過(guò)測(cè)試,目前選用/O3,但是隨著代碼的更改,需要重新測(cè)試,選擇合適的優(yōu)化級(jí)別。
針對(duì)特定處理器進(jìn)行優(yōu)化
Intel的編譯器一共支持如下3種針對(duì)特定處理器的優(yōu)化:
u       /G:使用這個(gè)優(yōu)化選項(xiàng),Intel將針對(duì)特定的CPU進(jìn)行優(yōu)化,但是其代碼依然可以在所有的CPU上執(zhí)行。
u       /Qx:使用這個(gè)優(yōu)化選項(xiàng),Intel將針對(duì)特定的CPU進(jìn)行優(yōu)化,并且產(chǎn)生的代碼無(wú)法使用在不兼容的CPU上。
u       /Qax:使用這個(gè)優(yōu)化選項(xiàng),Intel將針對(duì)特定的CPU進(jìn)行優(yōu)化,并且產(chǎn)生多份代碼,在運(yùn)行時(shí)根據(jù)CPU類型自動(dòng)選擇最優(yōu)的代碼。
由于本程序只需要運(yùn)行在基于Core Microarchitecture 的處理器上,而無(wú)需考慮兼容性。所以本程序選擇/Qx選項(xiàng)。并且針對(duì)運(yùn)行時(shí)的酷睿2處理器,選擇/QxT。但是在進(jìn)行VTune測(cè)試時(shí),由于測(cè)試平臺(tái)為奔騰D 820,所以暫時(shí)使用/QxP的參數(shù)。
使用IPO
使用/Qipo可以啟用Intel編譯器的過(guò)程間優(yōu)化(Interprocedural Optimizations)。通過(guò)過(guò)程間優(yōu)化,編譯器可以通過(guò)使用寄存器優(yōu)化函數(shù)調(diào)用、內(nèi)聯(lián)函數(shù)展開(kāi)、過(guò)程間常數(shù)傳遞、跨多文件優(yōu)化等方式進(jìn)一步優(yōu)化程序。
此外,Intel編譯器支持多文件的過(guò)程間優(yōu)化,而由于本程序只有一個(gè)文件,所以并不需要使用。
但是IPO優(yōu)化卻會(huì)對(duì)本程序的調(diào)試帶來(lái)極大的麻煩。所以本程序開(kāi)發(fā)時(shí)不使用IPO優(yōu)化,只有在最后的版本中才嘗試使用IPO優(yōu)化能否提高效率。
使用GPO
Intel編譯器支持GPO(Profile-Guided Optimization)。GPO由一下三步組成。
第一步:使用/Qprof-gen編譯程序,產(chǎn)生能記錄運(yùn)行細(xì)節(jié)的特殊程序。
第二步:運(yùn)行第一步產(chǎn)生的程序,生成動(dòng)態(tài)信息文件(.dyn)
第三步,使用/Qprof-use,結(jié)合動(dòng)態(tài)信息文件重新編譯程序,產(chǎn)生更優(yōu)化的程序。
通過(guò)使用GPO,Intel編譯器可以更詳細(xì)得了解程序的運(yùn)行情況,從而根據(jù)實(shí)際情況產(chǎn)生更優(yōu)化的代碼。比如優(yōu)化條件跳轉(zhuǎn),使得CPU分支預(yù)測(cè)的能力更準(zhǔn)確,又如決定哪些函數(shù)需要內(nèi)聯(lián),哪些不要內(nèi)聯(lián)等。
此外,基于GPO還有很多的工具方便用戶開(kāi)發(fā)程序。比如Code-Coverage Tool可以進(jìn)行代碼覆蓋測(cè)試。
由于GPO收集的信息和特定的程序有關(guān),而本程序一直在修改。所以本程序只在每個(gè)版本的最后部分使用GPO進(jìn)行優(yōu)化。
循環(huán)展開(kāi)
循環(huán)展開(kāi)(Loop Unrolling)通過(guò)在把循環(huán)語(yǔ)句中的內(nèi)容展開(kāi)從而使執(zhí)行的代碼速度更快。循環(huán)展開(kāi)可以提高代碼的并行程度,減少條件轉(zhuǎn)移次數(shù)從而提高速度。另外,對(duì)于Pentium 4處理器,其分支預(yù)測(cè)功能可以精確得預(yù)測(cè)出16次迭代以內(nèi)的循環(huán),所以,如果能把循環(huán)展開(kāi)到迭代次數(shù)在16次以內(nèi),對(duì)于特定的CPU可以提高分支預(yù)測(cè)準(zhǔn)確度。
但是循環(huán)展開(kāi)必須有一個(gè)度,并不是展開(kāi)層數(shù)越多越好,展開(kāi)層數(shù)多了,可能反而影響代碼的執(zhí)行速度。所以通常的做法是讓編譯器自己決定循環(huán)展開(kāi)的層數(shù)。
Intel編譯器對(duì)于循環(huán)展開(kāi)有如下選項(xiàng):
u       /Qunrolln:執(zhí)行循環(huán)展開(kāi)n層。
u       /Qunroll:讓Intel編譯器自己決定循環(huán)展開(kāi)的層數(shù)。
此外Intel編譯器還提供在了程序中使用編譯制導(dǎo)語(yǔ)句規(guī)定某個(gè)特定循環(huán)的展開(kāi)次數(shù)。如下例指示for循環(huán)展開(kāi)n層。

#pragma unroll(n)
for(i=0;i<10000;i++){……}

所以本程序使用/Qunroll參數(shù),讓Intel編譯器自己決定使用循環(huán)展開(kāi)的層數(shù)。但是在程序的最終優(yōu)化時(shí),如果發(fā)現(xiàn)Intel編譯器的循環(huán)展開(kāi)并不是最優(yōu)的,則通過(guò)在特定循環(huán)前加上編譯制導(dǎo)語(yǔ)句,使用最佳的循環(huán)展開(kāi)層數(shù)。
浮點(diǎn)計(jì)算優(yōu)化
Intel編譯器提供了很多基于浮點(diǎn)數(shù)的優(yōu)化參數(shù),有提供精度的,也有提高速度的。對(duì)于本程序,主要使用如下優(yōu)化參數(shù)。
u       /fp: fast或/fp: fast=1:這兩個(gè)參數(shù)的等價(jià)的,同時(shí)也是默認(rèn)的參數(shù)。他告訴編譯器進(jìn)行快速浮點(diǎn)計(jì)算優(yōu)化。
u       /fp: fast=2:這個(gè)參數(shù)比/fp: fast=1提供更高的優(yōu)化級(jí)別,同時(shí)也可能帶來(lái)更大的精度損失。
本程序使用/fp: fast=2優(yōu)化,但是如果發(fā)生精度問(wèn)題,可以考慮使用/fp: fast=1。
自動(dòng)并行化
Intel的編譯器支持自動(dòng)并行化(Auto-parallelization)。通過(guò)/Qparallel可以打開(kāi)編譯器的自動(dòng)并行化,編譯器會(huì)在分析了用戶的串行程序后,自動(dòng)選擇可以并行的部分進(jìn)行并行化。自動(dòng)并行化的有點(diǎn)是方便,不需要用戶懂得專業(yè)知識(shí),不需要更改原來(lái)的串行程序。但是缺點(diǎn)也是顯而易見(jiàn)的,由于編譯器并不知道用戶的程序邏輯,所以無(wú)法很好得進(jìn)行并行化。在對(duì)本程序試用/Qparallel后發(fā)現(xiàn),效果并不好。所以本程序不只用/Qparallel進(jìn)行自動(dòng)并行化。
使用OpenMP并行化
OpenMP是一種通用的并行程序設(shè)計(jì)語(yǔ)言,其通過(guò)在源代碼中添加編譯制導(dǎo)語(yǔ)句,提示編譯器如何進(jìn)行程序的并行化。OpenMP具有書寫方便,不需要改變?cè)创a結(jié)構(gòu)等多種優(yōu)點(diǎn)。Intel的編譯器支持OpenMP。本次程序并不打算使用OpenMP進(jìn)行并行化,而打算使用Windows Thread。但是由于本程序需要使用到Intel Math Kernel Library,而Intel Math Kernel Library中的代碼支持OpenMP并行化。所以有必要使用一些基本的OpenMP設(shè)置函數(shù)。
需要使用OpenMP,需要在編譯時(shí)加上/Qopenmp選項(xiàng)。并且在源代碼中包含” omp.h”文件。
OpenMP提供了函數(shù)omp_set_num_threads(nthreads)設(shè)置OpenMP使用的線程數(shù),由于其設(shè)置會(huì)影響到Intel Math Kernel Library,所以將其設(shè)置成1,禁止Intel Math Kernel Library的自動(dòng)并行化。
向量化
Intel的編譯器支持向量化(Vectorization)??梢园蜒h(huán)計(jì)算部分使用MMX,SSE,SSE2,SSE3,SSSE3等指令進(jìn)行向量化,從而大大提高計(jì)算速度。這也是本程序串行化時(shí)的主要優(yōu)化點(diǎn)。前面提到的針對(duì)處理器的/QaxT優(yōu)化選項(xiàng)已經(jīng)打開(kāi)了向量化。將代碼向量化還有許多需要注意的地方,具體的注意點(diǎn)和方法將在后面具體的代碼中說(shuō)明。這里先給出一些對(duì)向量化有用的編譯制導(dǎo)語(yǔ)句以及選項(xiàng)。
u       /Qrestrict選項(xiàng):當(dāng)Intel編譯器遇到循環(huán)中使用指針時(shí),由于多個(gè)指針可能指向同一個(gè)地址,所以其無(wú)法保證指針指向內(nèi)容的唯一性。故Intel編譯器無(wú)法確定循環(huán)內(nèi)數(shù)據(jù)是否存在依賴性。這是可以通過(guò)使用/Qrestrict選項(xiàng)與restrict關(guān)鍵字,指示某個(gè)指針指向內(nèi)容的唯一性。從而能解決數(shù)據(jù)依賴性不確定的問(wèn)題。
u       #pragma vector編譯制導(dǎo)語(yǔ)句:該編譯制導(dǎo)語(yǔ)句一共包含3個(gè)。#pragma vector always用于指示編譯器忽略其他因素,進(jìn)行向量化。#pragma vector aligned用于指示編譯器進(jìn)行向量化時(shí)使用對(duì)齊的數(shù)據(jù)讀寫方式。#pragma vector unaligned用于指示編譯器進(jìn)行向量化時(shí)使用不對(duì)齊的數(shù)據(jù)讀寫方式。由于在使用SSE類指令進(jìn)行向量化時(shí),需要同時(shí)處理多個(gè)數(shù)據(jù),所以每次讀寫的數(shù)據(jù)長(zhǎng)度很長(zhǎng),可以達(dá)到128bit。所以將要處理的數(shù)據(jù)按照128bit(16byte)對(duì)齊,使用對(duì)齊的讀寫指令是可以提高程序運(yùn)行速度的。但是需要注意的是對(duì)于實(shí)際沒(méi)有對(duì)齊的數(shù)據(jù)使用#pragma vector aligned會(huì)造成程序運(yùn)行錯(cuò)誤。
使用變量對(duì)齊指示
Intel編譯器提供了__declspec(align(n))用于在定義變量時(shí)指定其需要進(jìn)行n字節(jié)對(duì)齊。變量對(duì)齊對(duì)于向量化計(jì)算的讀取速度有很大關(guān)系。對(duì)于向量化計(jì)算一般使用__declspec(align(16))進(jìn)行對(duì)齊。另外也可以使用__declspec(align(64))指定變量對(duì)齊到Cache的行首。關(guān)于Cache的行對(duì)齊的詳細(xì)討論請(qǐng)見(jiàn)后文的分析。
數(shù)據(jù)預(yù)讀
通常數(shù)據(jù)是放在內(nèi)存中,當(dāng)要計(jì)算時(shí)才讀入CPU進(jìn)行計(jì)算。由于內(nèi)存到CPU的傳輸需要很長(zhǎng)時(shí)間,所以CPU中有多級(jí)Cache機(jī)制。Intel編譯器支持?jǐn)?shù)據(jù)預(yù)讀優(yōu)化選項(xiàng)。通過(guò)/Qprefetch打開(kāi)數(shù)據(jù)預(yù)讀優(yōu)化,編譯器會(huì)在使用數(shù)據(jù)前先插入預(yù)讀指令,讓CPU先把數(shù)據(jù)預(yù)讀到Cache中,從而加快數(shù)據(jù)的訪問(wèn)速度。該選項(xiàng)默認(rèn)情況下是打開(kāi)的。此外Intel還提供了數(shù)據(jù)預(yù)讀的編譯制導(dǎo)語(yǔ)句,通過(guò)使用#pragma prefetch語(yǔ)句,用戶可以人為得在程序中增加數(shù)據(jù)預(yù)讀指令。但是需要注意的是,數(shù)據(jù)預(yù)讀指令并不是越多越好的。不恰當(dāng)?shù)臄?shù)據(jù)預(yù)讀指令會(huì)占用內(nèi)存帶寬,把有用的數(shù)據(jù)從Cache中擠出去,反而影響速度。并且Core Microarchitecture體系結(jié)構(gòu)已經(jīng)支持給予硬件的數(shù)據(jù)預(yù)讀指令。所以本程序傾向于使用給予硬件的數(shù)據(jù)預(yù)讀機(jī)制。而由于/Qprefetch默認(rèn)的打開(kāi)的,也沒(méi)有必要特意關(guān)閉該選項(xiàng),Intel編譯器有能力判斷哪些地方可以通過(guò)合適的數(shù)據(jù)訪問(wèn)模式激活硬件數(shù)據(jù)預(yù)讀機(jī)制,哪些地方需要額外添加數(shù)據(jù)預(yù)讀指令。
產(chǎn)生調(diào)試信息
通過(guò)使用/Zi選項(xiàng)產(chǎn)生調(diào)試信息以幫助調(diào)試。默認(rèn)為關(guān)閉。在本程序的開(kāi)發(fā)階段,打開(kāi)此選項(xiàng)。在開(kāi)發(fā)完成后關(guān)閉此選項(xiàng)。
使用全局優(yōu)化
通過(guò)使用/Og選項(xiàng)打開(kāi)編譯器的全局優(yōu)化功能。改選項(xiàng)需要在本程序不同的開(kāi)發(fā)階段分別嘗試是否打開(kāi)以確定最優(yōu)優(yōu)化選項(xiàng)。
針對(duì)Windows程序優(yōu)化
通過(guò)使用/GA選項(xiàng)可以打開(kāi)Intel編譯器的針對(duì)Windows程序優(yōu)化的功能。其實(shí)通過(guò)打開(kāi)/GA選項(xiàng),Intel可以提高訪問(wèn)Windows下thread-local storage(TLS)變量的速度。TLS變量通過(guò)__declspec(thread)來(lái)定義。在本程序中,并不打算使用TLS變量。但還是打開(kāi)/GA選項(xiàng)。
 
內(nèi)聯(lián)函數(shù)擴(kuò)展
Intel編譯器可以通過(guò)/Obn來(lái)定義內(nèi)聯(lián)函數(shù)的擴(kuò)展級(jí)別。當(dāng)n為0禁止用戶定義的內(nèi)核函數(shù)的擴(kuò)展。當(dāng)n為1時(shí),根據(jù)用戶定義的inline關(guān)鍵字進(jìn)行擴(kuò)展。當(dāng)n為2時(shí),根據(jù)Intel編譯器的自動(dòng)判斷進(jìn)行擴(kuò)展。本次程序使用/Ob2選項(xiàng)。
FTZ與DAZ
在計(jì)算機(jī)內(nèi)浮點(diǎn)數(shù)是由尾數(shù)和指數(shù)組成的。尾數(shù)通常被規(guī)范化成[1,2)之間。但是當(dāng)數(shù)字接近0時(shí),由于其指數(shù)已經(jīng)無(wú)法將尾數(shù)規(guī)范成[1,2)之間,所以需要在尾數(shù)表示成0.0000xx的形式。這種表示形式稱為不規(guī)范的形式。其會(huì)影響CPU的浮點(diǎn)計(jì)算速度。并且由于這種數(shù)非常接近0,所有有時(shí)將其表示成0并不會(huì)影響計(jì)算的結(jié)果。所以CPU的浮點(diǎn)控制器有2個(gè)用于控制對(duì)于不規(guī)范數(shù)處理的選項(xiàng)。FTZ用于將計(jì)算結(jié)果中的不規(guī)范數(shù)表示成0,DAZ用于在讀入不規(guī)范數(shù)時(shí)將其表示成0。Intel編譯器提供了內(nèi)置的宏來(lái)方便用戶設(shè)置這兩個(gè)模式。這兩個(gè)宏分別是_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON)和_MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON)。用戶在程序中設(shè)置了這兩個(gè)模式將有助于提高浮點(diǎn)計(jì)算速度。但是實(shí)際上對(duì)于本程序,由于已經(jīng)使用了/O3以及SSE指令集優(yōu)化。所以Intel編譯器已經(jīng)設(shè)置好了FTZ模式,用戶不必另外設(shè)置FTZ。并且由于本程序中所有的數(shù)都是計(jì)算得來(lái)的,所以只要計(jì)算時(shí)使用了FTZ,那讀取數(shù)據(jù)時(shí)就不會(huì)碰到不規(guī)范的數(shù)據(jù),所以用戶也沒(méi)必要設(shè)置DAZ。
 
 
編譯器報(bào)告
編譯器報(bào)告雖然不能直接提供優(yōu)化,但是卻可以讓用戶了解編譯器處理程序的信息,給用戶更改源代碼提供了很多有用的信息。對(duì)于本程序,向量化是非常重要的一步,而編譯器報(bào)告可以指出某個(gè)地方是由于什么原因造成沒(méi)有向量化。所以本使用使用/Qvec-report3參數(shù)對(duì)向量?jī)?yōu)化進(jìn)行報(bào)告。
使用Intel編譯器函數(shù)進(jìn)行精確時(shí)間測(cè)量
Intel編譯器提供了許多特殊的函數(shù)。這類函數(shù)一般都對(duì)應(yīng)一條或者幾條匯編語(yǔ)言。其可以讓用戶以比匯編語(yǔ)言方便的方式寫出性能接近匯編語(yǔ)言的代碼。其中最主要的是對(duì)SIMD類指令的支持。當(dāng)然其中還有很多其他功能的函數(shù)。比如_rdtsc()函數(shù)。
需要注意的是要使用這些函數(shù)必需打開(kāi)/Oi選項(xiàng)。這個(gè)選項(xiàng)默認(rèn)是打開(kāi)的。
當(dāng)程序需要進(jìn)行精確時(shí)間測(cè)量,比如優(yōu)化后需要知道某段特定的代碼到底快了多少毫米時(shí),使用Windows的時(shí)間函數(shù)已經(jīng)無(wú)法滿足精度要求。這是用戶可以使用Intel VTune Analyzers進(jìn)行測(cè)量(具體使用方法將在后面介紹)。其實(shí)CPU已經(jīng)提供了一個(gè)特殊的機(jī)器指令rdtsc,使用這條指令可以讀出CPU自從啟動(dòng)以來(lái)的時(shí)鐘周期數(shù)。由于現(xiàn)在的CPU主頻已經(jīng)是上GHz了。所以,其計(jì)時(shí)精度可以達(dá)到納秒級(jí)。Intel提供的_rdtsc()函數(shù)使得用戶不必再使用匯編語(yǔ)言,可以像調(diào)用函數(shù)一樣得到CPU的時(shí)鐘周期數(shù)。例子代碼如下:
注:以下代碼摘自“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;
}

優(yōu)化結(jié)果
經(jīng)過(guò)以上編譯器選項(xiàng)的調(diào)整,程序的運(yùn)行速度已經(jīng)達(dá)到了2.25秒。
1.3 使用Intel VTune Analyzers進(jìn)行性能分析
1.3.1 Intel VTune Analyzers概述
Intel VTune Analyzers用于監(jiān)視程序或者系統(tǒng)的各種性能,從而為用戶優(yōu)化程序提供有價(jià)值的數(shù)據(jù)。同時(shí)Intel VTune Analyzers也能分析其收集的信息,給出用戶優(yōu)化程序的建議。Intel VTune Analyzers即支持本地的數(shù)據(jù)收集,也支持遠(yuǎn)程的數(shù)據(jù)收集。在本程序中,我們只需使用其本地?cái)?shù)據(jù)收集功能。Intel VTune Analyzers共支持3種數(shù)據(jù)收集機(jī)制。每種機(jī)制都有其自己的適用范圍,詳細(xì)介紹如下:
u       SAMPLING其通過(guò)使用CPU內(nèi)部的監(jiān)視功能來(lái)檢測(cè)系統(tǒng)底層的各種性能事件。使用這個(gè)功能無(wú)需在執(zhí)行代碼中插入特定的指令,因此其幾乎沒(méi)有探針效應(yīng)。其無(wú)法給出函數(shù)間的調(diào)用關(guān)系。但是可以把相應(yīng)的事件關(guān)聯(lián)到程序中某行源代碼或者匯編代碼上。該方法通常適用于對(duì)某段程序的微調(diào)或者針對(duì)特定性能事件的調(diào)整上。
u       CALL GRAPH其通過(guò)在程序中插入特殊的指令,來(lái)記錄每個(gè)函數(shù)執(zhí)行的時(shí)間。函數(shù)間的調(diào)用關(guān)系等。其有一定的探針效應(yīng)。該方法通常用于對(duì)于整個(gè)比較龐大的程序,進(jìn)行分析,找出其中具有性能瓶頸的函數(shù)。
u       COUNTER MONITOR其無(wú)需在程序內(nèi)部插入特殊的指令,因此其幾乎沒(méi)有探針效應(yīng)。該方法即無(wú)法顯示函數(shù)間的調(diào)用關(guān)系,也沒(méi)法把事件定位到具體的某行代碼中。該方式是用于測(cè)試整個(gè)系統(tǒng)的某些性能,比如CPU占用率,內(nèi)存帶寬等。通常用于系統(tǒng)級(jí)的調(diào)試。
對(duì)于本程序。由于程序結(jié)構(gòu)簡(jiǎn)單。無(wú)需進(jìn)行函數(shù)間調(diào)用的分析。而主要需要進(jìn)行基于特定代碼的分析。特別是后期需要針對(duì)CPU內(nèi)部的事件特性進(jìn)行源代碼級(jí)甚至是匯編級(jí)的調(diào)試。所以本次優(yōu)化主要采用SAMPLING方式。
1.3.2基于SAMPLING方式的分析
原理:Intel的CPU有一組性能檢測(cè)寄存器,由于記錄各種影響性能的事件。程序首先通過(guò)編程設(shè)定需要檢測(cè)的事件,并且設(shè)定觸發(fā)中斷的計(jì)數(shù)值。當(dāng)CPU中被檢測(cè)的事件達(dá)到預(yù)設(shè)的值后觸發(fā)相應(yīng)的中斷。Intel VTune Analyzers中的SAMPLING就是使用CPU的性能檢測(cè)功能幫助用戶分析程序的性能。其中有關(guān)于內(nèi)存訪問(wèn)的事件,分支預(yù)測(cè)的事件,指令執(zhí)行的事件等等。由于不同的CPU支持不同的性能事件,所以在不同的CPU上使用VTune時(shí),所能監(jiān)視的事件并不相同。
使用注意事項(xiàng):SAMPLING一共支持2種統(tǒng)計(jì)。一種是Event,其是直接測(cè)量得到的值。另外一種是Event Ratio,其是基于多個(gè)Event計(jì)算得到的,有時(shí)更有實(shí)際意義,更直觀。需要注意的是,每個(gè)Event都有一個(gè)預(yù)設(shè)的值,當(dāng)這個(gè)預(yù)設(shè)的值到了以后,CPU引起中斷,VTune進(jìn)行統(tǒng)計(jì)。而這個(gè)值的設(shè)置不能太大,否則統(tǒng)計(jì)到的事件不夠多,無(wú)法分析。也不能太小,否則頻繁引起中斷,會(huì)加大探針效應(yīng)。用戶可以在每個(gè)Event上手工設(shè)置合適的Sample After值,也可以通過(guò)選項(xiàng)卡上的選項(xiàng),讓VTune先運(yùn)行一遍程序,然后根據(jù)實(shí)際的事件數(shù)量來(lái)校準(zhǔn)觸發(fā)值。對(duì)于本程序,這點(diǎn)尤其需要引起注意。因?yàn)楸境绦騼?yōu)化到后面時(shí)間非常短,如果不校準(zhǔn)觸發(fā)值,分析的效果會(huì)不理想。需要注意的是Clockticks和Instructions Retired這兩個(gè)最基本的事件,默認(rèn)是不校準(zhǔn)觸發(fā)值的,我們需要把他們調(diào)整成自動(dòng)校準(zhǔn)。此外對(duì)于某個(gè)Event的發(fā)生,大部分的中斷點(diǎn)并不是精確的。即真正發(fā)生該事件的指令在所記錄事件指令的前幾條。但是有一部分屬于精確事件,引起這類事件的指令正好是發(fā)生中斷的前一條。
1.3.3對(duì)于本次程序的分析
本程序首先使用VTune最基本的3個(gè)事件(Clockticks、Instructions Retired和CPI)進(jìn)行程序耗時(shí)分析。其結(jié)果如圖:
說(shuō)明程序中耗時(shí)最長(zhǎng)的是computePot函數(shù)。
1.4 優(yōu)化computePot函數(shù)
在對(duì)computePot函數(shù)向量化前,我們可以注意到distx,disty,distz三個(gè)變量都是臨時(shí)變量。先將這3個(gè)變量去掉,從而可以使得Intel編譯器能夠更靈活得進(jìn)行中間結(jié)果優(yōu)化。另外最完成循環(huán)的i雖然是從0開(kāi)始的,但是實(shí)際0和1并不進(jìn)行計(jì)算,所以把外層循環(huán)的i設(shè)置層從2開(kāi)始。代碼如下:

   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;
      }
   }

此時(shí)編譯器顯示內(nèi)層循環(huán)已經(jīng)向量化了。但是這個(gè)絕非我們的目標(biāo)。為了提高計(jì)算開(kāi)根號(hào)倒數(shù)的速度,為了使用Intel Math Kernel Library,我們需要把開(kāi)根號(hào)倒數(shù)的計(jì)算先存在一組向量中,再一同計(jì)算。既將dist變量變成,dist數(shù)組,然后再對(duì)dist數(shù)組統(tǒng)一計(jì)算,再求和。代碼如下:

   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編譯器提示,內(nèi)部的3個(gè)循環(huán)都進(jìn)行了向量化。此時(shí)出現(xiàn)了令人驚喜的成績(jī)。程序的執(zhí)行時(shí)間突然降到了1.453秒。使用VTune進(jìn)行分析,發(fā)現(xiàn)Intel編譯器對(duì)于開(kāi)根號(hào)倒數(shù)的計(jì)算自動(dòng)調(diào)用了內(nèi)部的向量化代碼庫(kù)。注意此時(shí),還沒(méi)有使用Intel Math Kernel Library,所以這個(gè)向量代碼庫(kù)是Intel編譯器內(nèi)置的,雖然效率沒(méi)有使用Intel Math Kernel Library高,但是速度已經(jīng)提高了很多。調(diào)用Intel編譯器內(nèi)置的向量庫(kù)的結(jié)果如圖:
1.5 使用Intel Math Kernel Library
Intel Math Kernel Library中提供了一部分的向量函數(shù)(Vector Mathematical Functions)。這類函數(shù)提供了對(duì)于普通數(shù)學(xué)計(jì)算函數(shù)的快速的向量化計(jì)算。VML中有一個(gè)向量函數(shù)就是計(jì)算開(kāi)根號(hào)倒數(shù)的。
Intel的VML庫(kù)中提供了如下函數(shù)來(lái)計(jì)算整個(gè)向量中各個(gè)數(shù)的開(kāi)根號(hào)倒數(shù):
vdInvSqrt( n, a, y )
其中n表示計(jì)算的元素個(gè)數(shù)。a是指向輸入計(jì)算數(shù)據(jù)數(shù)組的頭指針。y是指向輸出計(jì)算數(shù)據(jù)數(shù)組的頭指針。其中a和y可以相同。
要使用該函數(shù),首先需要在頭文件中包含”mkl.h”,并且鏈接mkl_c.lib文件和libguide40.lib文件。
除了基本計(jì)算功能外,VML還提供了一個(gè)設(shè)置模式的函數(shù),用于設(shè)置特定的計(jì)算模式:
vmlSetMode ( mode )
其中的mode是一個(gè)預(yù)定義宏。在我們的程序中,需要設(shè)置如下模式:
VML_LAVML的所有向量函數(shù)都提供了2個(gè)精度的版本。精度低的版本計(jì)算速度也相對(duì)比較快。本程序只需要保留小數(shù)點(diǎn)后7位精度。低精度的版本符合要求,所以設(shè)定VML使用低精度的版本。
VML_DOUBLE_CONSISTENT該選項(xiàng)用于控制FPU的計(jì)算精度為double,其實(shí)由于我們這次使用的函數(shù)基本上是使用SSE2指令集進(jìn)行計(jì)算的,和FPU沒(méi)什么關(guān)系。但是也可能存在使用FPU的可能,所以設(shè)定VML使FPU的精度為double。
VML_ERRMODE_IGNORE該選項(xiàng)用于關(guān)閉VML的錯(cuò)誤處理功能,本程序不需要進(jìn)行錯(cuò)誤處理。
VML_NUM_THREADS_OMP_FIXEDVML函數(shù)都能使用OpenMP,根據(jù)特定的硬件環(huán)境進(jìn)行并行化。而我們并不需要其進(jìn)行并行化。所以使用該選項(xiàng)和前面提到的omp_set_num_threads(1)結(jié)合。關(guān)閉VML的自動(dòng)并行化功能。
具體的代碼如下:

   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];
      }
   }

優(yōu)化后出現(xiàn)了令人可惜可賀的成績(jī):0.796秒。
1.6 根據(jù)Cache大小優(yōu)化Intel Math Kernel Library調(diào)用
在上面的程序中對(duì)于MKL函數(shù)的調(diào)用是每次內(nèi)部循環(huán)都執(zhí)行一次調(diào)用,我們知道每次執(zhí)行函數(shù)的調(diào)用都是需要開(kāi)銷的,那是否有更優(yōu)化的調(diào)用MKL方法那?下面這段話摘自Intel Math Kernel Library的說(shuō)明文檔上:

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的網(wǎng)站上:
從這段文字和這副圖片中,我們了解到對(duì)于MKL函數(shù)的調(diào)用時(shí),所處理的向量不能太短,否則函數(shù)的建立時(shí)間開(kāi)銷將是非常大的,也不能太長(zhǎng),操作了L2 Cache,否則函數(shù)執(zhí)行時(shí)訪問(wèn)內(nèi)存的開(kāi)銷是很大的。并且通過(guò)圖片了解到不合適的長(zhǎng)度對(duì)于函數(shù)的性能將產(chǎn)生指數(shù)級(jí)影響。
根據(jù)理論計(jì)算:每次執(zhí)行computePot函數(shù),總共需要執(zhí)行的計(jì)算量為(1+998)*998/2=498501個(gè)。每個(gè)double類型占用8個(gè)字節(jié),所有總共需要占用的空間為498501*8=3988008byte=3894KB。而這次進(jìn)行競(jìng)賽的測(cè)試平臺(tái)的CPU的L2 Cache大小為2M,由于有2個(gè)線程同時(shí)計(jì)算,平均每個(gè)線程分到的L2 Cache為1M。由于L2 Cache可能還被其他數(shù)據(jù)占據(jù)。所以為了保證所計(jì)算的數(shù)據(jù)在L2 Cache中,最好每次計(jì)算的向量長(zhǎng)度在512KB左右。故把整個(gè)computePot函數(shù)的計(jì)算量分成8份。每份計(jì)算量的中間結(jié)果向量長(zhǎng)度為3894KB/8=486KB。
但是實(shí)際情況并非如此,進(jìn)行這種優(yōu)化后,程序的執(zhí)行速度反而降低了。通過(guò)分析發(fā)現(xiàn)原來(lái)CPU中的L1 Cache大小為32KB。數(shù)組r有3000個(gè)元素,如果每次迭代都進(jìn)行vdInvSqrt調(diào)用。那dist的長(zhǎng)度為1000個(gè)元素左右。加起來(lái)正好可以全部在L1 Cache中。而如果合并起來(lái)調(diào)用vdInvSqrt,則由于vdInvSqrt過(guò)長(zhǎng)。其L1 Cache中存放不下,需要存放在L2 Cache中,從而反而影響了速度。看來(lái),對(duì)于本程序,不應(yīng)該根據(jù)L2 Cache進(jìn)行優(yōu)化,而應(yīng)該根據(jù)L1 Cache進(jìn)行優(yōu)化。但是對(duì)于只有幾個(gè)或者幾十個(gè)數(shù)據(jù)就調(diào)用MKL函數(shù),其開(kāi)銷還是很大的。因此本程序使用了折中的方法,對(duì)于前面非常小的幾十個(gè)數(shù)據(jù),湊足1000個(gè)放在一起進(jìn)行計(jì)算,而后面的數(shù)據(jù)還是按照原來(lái)的方式計(jì)算。具體實(shí)現(xiàn)的代碼如下:

   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];
      }
   }

通過(guò)該不優(yōu)化,程序的性能略微有所提高,達(dá)到了0.781秒。
1.7 優(yōu)化updatePositions函數(shù)
雖然updatePositions函數(shù)執(zhí)行的時(shí)間非常短。但還是值得優(yōu)化的。
首先進(jìn)行的是基于數(shù)學(xué)的優(yōu)化。我們發(fā)現(xiàn)在updatePositions和initPositions中,都有加0.5的計(jì)算。但是從后面的computePot的相減計(jì)算中發(fā)現(xiàn),這個(gè)0.5是被抵消的,既不加0.5對(duì)結(jié)果沒(méi)有影響。故去掉該加0.5的計(jì)算。另外updatePositions和initPositions中都有除以RAND_MAX的計(jì)算。而通過(guò)提取公因子的變換發(fā)現(xiàn),如果此處不除以RAND_MAX而將最后的pot乘以RAND_MAX,則最后結(jié)果相同。故去掉該處的除以RAND_MAX的計(jì)算,而以在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函數(shù)中:
      pot = 0.0;
      computePot();
      pot*=(double)RAND_MAX;
      if (i%10 == 0) printf("%5d: Potential: %20.7f\n", i, pot);
 

其次需要進(jìn)行updatePositions內(nèi)rand函數(shù)的優(yōu)化。雖然rand函數(shù)本身的執(zhí)行時(shí)間非常短,但是其頻繁得進(jìn)行調(diào)用卻影響了性能。通過(guò)查找Microsoft Visual Studio .NET 2005中提供的源代碼。將其中的rand函數(shù)提取出來(lái),進(jìn)行必要的修改,并且加上inline屬性。從而加快程序的調(diào)用速度。具體代碼如下:

int holdrand=1;
inline int myrand (){
        return( ((holdrand = holdrand * 214013L+ 2531011L) >> 16) & 0x7fff );
}
 

經(jīng)過(guò)上述優(yōu)化,代碼的執(zhí)行速度已經(jīng)達(dá)到了0.765秒。
1.8 其他優(yōu)化以及性能分析
至此,該程序串行優(yōu)化部分已經(jīng)一本完成。但是還有一點(diǎn)細(xì)小的地方需要優(yōu)化。
變量對(duì)齊對(duì)于數(shù)據(jù)讀取速度是非常重要的。尤其是使用SIMD指令集進(jìn)行優(yōu)化后,對(duì)于對(duì)齊的變量,可以使用對(duì)齊的讀寫指令提高速度。一般對(duì)于SIMD指令需要進(jìn)行16字節(jié)對(duì)齊。但是對(duì)于本程序,由于后面要進(jìn)行多線程優(yōu)化,而多線程執(zhí)行時(shí)基于Cache Line的共享沖突會(huì)對(duì)讀寫造成很大的損失。故本程序使用64字節(jié)對(duì)齊。代碼如下:

__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函數(shù)的第一次迭代中。有一處進(jìn)行pot累加的地方,使用了k變量作為循環(huán)條件。但是其實(shí)該變量的確切值是可以計(jì)算出來(lái)的。通過(guò)計(jì)算出該變量的確切值,可以讓Intel編譯器在編譯時(shí)就知道循環(huán)的次數(shù),從而有助于優(yōu)化。具體代碼如下(注意1035這個(gè)值):

   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];
   }

此外再調(diào)整以下編譯器的某些優(yōu)化參數(shù),選擇合適的使用。比如使用哪個(gè)編譯級(jí)別,是否打開(kāi)全局優(yōu)化,使用IPO,使用GPO等。
至此本程序的串行優(yōu)化全部完成。使用Intel VTune Analyzers的分析結(jié)果為:
 
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
(注:此分析數(shù)據(jù)是在奔騰D 820上測(cè)得)
從以上數(shù)據(jù)上表明updatePositions函數(shù)說(shuō)執(zhí)行的事件非常短,低于1%,computePot函數(shù)的執(zhí)行時(shí)間在三分之一左右。mkl_vml_core_t7_vml_dInvSqrt_50的執(zhí)行時(shí)間在三分之二左右。這些數(shù)據(jù)對(duì)下面一步并行化采用的策略是非常重要的。

 
2.1 并行優(yōu)化概述
在進(jìn)行本程序的并行優(yōu)化前先談?wù)劜⑿袃?yōu)化需要注意的問(wèn)題。在并行優(yōu)化中經(jīng)常用到數(shù)據(jù)重復(fù)和計(jì)算重復(fù)的方法。所謂數(shù)據(jù)重復(fù),就是為了保證多個(gè)線程能同時(shí)進(jìn)行計(jì)算,就把數(shù)據(jù)復(fù)制多份來(lái)提高并行度。所謂計(jì)算重復(fù),就是有時(shí)使用計(jì)算換通信的方法,提高并行度。
在對(duì)本程序進(jìn)行優(yōu)化前需要注意的是。測(cè)試平臺(tái)使用的是基于Core Microarchitecture結(jié)構(gòu)的。這個(gè)結(jié)構(gòu)的雙核CPU是共享L2 Cache的。但是當(dāng)數(shù)據(jù)在一個(gè)核中進(jìn)行修改,另外一個(gè)核去讀他時(shí),需要消耗幾十個(gè)時(shí)鐘周期的延遲。其代價(jià)的非常高的。這里需要注意的是,數(shù)據(jù)在Cache中是按行進(jìn)行存放的,也就是說(shuō),CPU看待數(shù)據(jù)有沒(méi)有被修改過(guò)是根據(jù)Cache Line的。所以2個(gè)分別被不同的核修改的數(shù)據(jù)如果存在于同一行Cache中,訪問(wèn)時(shí)的效率就會(huì)非常低。也就是發(fā)生了共享沖突。所以在分配變量時(shí)要盡量把不同性質(zhì)的變量分配到不同的Cache Line中。我們的測(cè)試平臺(tái)的L1 Cache和L2 Cache都是每行64byte的。所以前一章中的變量對(duì)齊都使用了64byte對(duì)齊。同樣,在程序并行化時(shí)也需要考慮這種情況。
2.2 優(yōu)化方案一
此方案使用數(shù)據(jù)重復(fù)的方法。程序可以定義2個(gè)r數(shù)組。以及2個(gè)pot數(shù)組。通過(guò)定義2個(gè)r數(shù)組,使得主線程可以在從線程使用一個(gè)r數(shù)組計(jì)算時(shí)同時(shí)更新第二個(gè)r數(shù)組。即主線程先更新r數(shù)組,然后主線程和從線程同時(shí)開(kāi)始計(jì)算。但是從線程的計(jì)算量比主線程大一點(diǎn)。這樣當(dāng)主線程計(jì)算完后,可以繼續(xù)更新第二個(gè)r數(shù)組,而此時(shí)從線程還在計(jì)算原來(lái)r數(shù)組的內(nèi)容。當(dāng)主線程更新完第二個(gè)r數(shù)組時(shí),從線程正好完成前面的計(jì)算,并和主線程一同計(jì)算第二個(gè)r數(shù)組,依次類推。同時(shí)2個(gè)pot數(shù)組,一個(gè)給主線程計(jì)算每步的中間結(jié)果,另一個(gè)給從線程計(jì)算每步的中間結(jié)果。等計(jì)算結(jié)束后,再將其結(jié)果相加,打印。
優(yōu)點(diǎn):使用該方法的優(yōu)點(diǎn)是顯而易見(jiàn)的,理論上線程可以做到完全同步。
缺點(diǎn):使用該方法的缺點(diǎn)是,從線程每次計(jì)算需要從主線程計(jì)算好的r數(shù)組中讀取內(nèi)容,由于是2個(gè)核,所以其訪問(wèn)延遲非常大。此外使用2個(gè)數(shù)值,每次迭代都需要將指針指向使用的數(shù)組,增加了程序的設(shè)計(jì)難度。同時(shí)計(jì)算任務(wù)分配的調(diào)優(yōu)也是非常繁瑣的。
由于在前一章中,我們發(fā)現(xiàn)updatePositions函數(shù)所花費(fèi)的時(shí)間非常短。所以做到線程間的完全平衡意義并不大。
2.3 優(yōu)化方案二
在前一個(gè)方案中,我們提到了線程的完全平衡的算法。同時(shí)我們發(fā)現(xiàn)完全平衡的意義不大。因此我們?cè)O(shè)計(jì)適合本程序的更優(yōu)的方案。既然updatePositions函數(shù)所花費(fèi)的時(shí)間非常短。那2個(gè)線程同時(shí)執(zhí)行updatePositions造成的額外開(kāi)銷也是可以忽略的。本方案使用了數(shù)據(jù)重復(fù)和計(jì)算重復(fù)的方法。同樣使用2個(gè)r數(shù)組,但是2個(gè)線程同時(shí)進(jìn)行重復(fù)計(jì)算,并且2個(gè)線程分區(qū)完成不同的迭代步驟的computePot計(jì)算。即主線程完成整個(gè)r數(shù)組的更新,但是只計(jì)算其中的奇數(shù)次迭代。從線程同樣完成整個(gè)r數(shù)組的更新,但是只進(jìn)行偶數(shù)次迭代。并且同樣使用了一個(gè)pot數(shù)組,2個(gè)線程分別將自己的計(jì)算結(jié)果先存儲(chǔ)到pot數(shù)組中。等最后同步的時(shí)候再打印。
優(yōu)點(diǎn):使用該方案,程序的設(shè)計(jì)相對(duì)來(lái)說(shuō)比較簡(jiǎn)單,負(fù)載均衡的調(diào)整也很容易。程序只需要很少的同步操作(在本程序中,只使用了2次同步)。并且重要的是。由于2個(gè)線程都在各自的CPU上使用各自的數(shù)據(jù)進(jìn)行計(jì)算,所以最大化得避免了共享沖突的發(fā)生。同時(shí)也保留了前一章優(yōu)化中針對(duì)L1 Cache的命中率。
缺點(diǎn):該方案的缺點(diǎn)是存在重復(fù)計(jì)算。但是通過(guò)前面VTune的測(cè)試,已經(jīng)發(fā)現(xiàn)其重復(fù)計(jì)算量非常小,可以忽略。
2.4 并行實(shí)現(xiàn)
本程序使用方案二進(jìn)行并行化。首先將所有需要計(jì)算的數(shù)據(jù)和函數(shù)都復(fù)制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數(shù)組記錄每次迭代的計(jì)算結(jié)果,用于最后的數(shù)組。
在主函數(shù)的并行中。我們發(fā)現(xiàn)由于偶數(shù)次迭代比奇數(shù)次迭代需要多算一次。故本程序的偶數(shù)次迭代在進(jìn)行到快完成前先釋放一個(gè)同步鎖。使得主線程可以先輸出一部分?jǐn)?shù)據(jù)。而從線程在執(zhí)行完所有的偶數(shù)次迭代后再釋放一個(gè)同步鎖,使主線程輸出剩余的數(shù)據(jù)。由于輸出數(shù)據(jù)也有一點(diǎn)的耗時(shí)。所以使用這種方法可以提高一點(diǎn)并行度。另外在本代碼中使用了SetThreadAffinityMask分別設(shè)置不同的線程對(duì)應(yīng)各自的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 性能分析
并行化后的性能并不沒(méi)有像理論中這么高只有0.437秒。于是我們開(kāi)始查找原因。通過(guò)使用Intel Threading Checker我們發(fā)現(xiàn),VML庫(kù)中存在著訪問(wèn)沖突。圖片如下:
 
當(dāng)然這個(gè)錯(cuò)誤有可能是Intel Threading Checker的誤報(bào)。因?yàn)槌绦蛎看螆?zhí)行都沒(méi)有發(fā)現(xiàn)不正確的結(jié)果,并且VML函數(shù)的文檔上說(shuō)明是線程安全性的。
由于兼容性原因,本系統(tǒng)無(wú)法使用Intel VTune Analyzers進(jìn)行每個(gè)函數(shù)的耗時(shí)分析。于是使用Intel編譯器提供的內(nèi)置函數(shù)_rdtsc()記錄不同部分所花費(fèi)的CPU時(shí)鐘周期。結(jié)果發(fā)現(xiàn)VML函數(shù)的總執(zhí)行時(shí)間大概增加了0.088秒左右。說(shuō)明VML函數(shù)在用戶使用Windows Thread函數(shù)并行化訪問(wèn)時(shí),其同步開(kāi)銷可能有一定的影響。

 
3.1 優(yōu)化目標(biāo)
本程序主要的執(zhí)行時(shí)間在computePot函數(shù)與VML庫(kù)中。對(duì)于computePot函數(shù),通過(guò)查看Intel編譯器產(chǎn)生的匯編碼發(fā)現(xiàn)其已經(jīng)很優(yōu)了。而對(duì)于VML函數(shù)由于其需要滿足通用性,所以本程序應(yīng)該可以設(shè)計(jì)出最適合本程序的計(jì)算函數(shù)來(lái)。
3.2 數(shù)學(xué)理論
Intel的CPU支持的SSE2指令中,有2條是用于計(jì)算雙精度浮點(diǎn)的開(kāi)根號(hào)倒數(shù)的。sqrtpd指令可以同時(shí)計(jì)算2個(gè)double型的開(kāi)根號(hào),其吞吐率為28個(gè)時(shí)鐘周期。divpd指令用于計(jì)算2個(gè)數(shù)的除法,即用于計(jì)算倒數(shù),其吞出率為17個(gè)時(shí)鐘周期。由此可以計(jì)算出,如果當(dāng)當(dāng)使用這2條指令計(jì)算雙精度數(shù)的開(kāi)根號(hào)倒數(shù),那即使使用匯編語(yǔ)言,忽略其他開(kāi)銷。計(jì)算每個(gè)元素的時(shí)鐘周期也有(17+28)/2=22.5。而Intel的VML庫(kù)計(jì)算每個(gè)元素的只需要10多個(gè)時(shí)鐘周期,說(shuō)明其肯定是通過(guò)其他快速的數(shù)學(xué)計(jì)算方法得到的。所以要優(yōu)化vdInvSqrt函數(shù),關(guān)鍵是找到更快速的數(shù)學(xué)計(jì)算方法。在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》文章中)
在上面的代碼中最后一條是典型的牛頓迭代,可以根據(jù)精度要求進(jìn)行多次迭代。這段代碼神奇的地方在于初始值的估算上,只用了減法和移位2個(gè)簡(jiǎn)單的操作,達(dá)到了非常接近的估算值。我們稱0x5f3759df為幻數(shù)(magic number)。CHRIS LOMONT在他的《FAST INVERSE SQUARE ROOT》文章中給出了對(duì)于這個(gè)幻數(shù)的解釋和計(jì)算方法。并且計(jì)算出了理論上最優(yōu)的適用于double類型的幻數(shù)為0x5fe6ec85e7de30da。說(shuō)們我們的代碼中可以使用該方法進(jìn)行計(jì)算,示例代碼如下:

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;
}

但是不幸的是,根據(jù)調(diào)試,需要達(dá)到比賽要求的小數(shù)點(diǎn)后7位精度,必需進(jìn)行4此牛頓迭代也行。而4此牛頓迭代的計(jì)算量使得這個(gè)方法對(duì)于Intel的VML函數(shù)來(lái)說(shuō)毫無(wú)優(yōu)勢(shì)可言。那能否降低牛頓迭代的次數(shù)那?
我們發(fā)現(xiàn)如果以上代碼只進(jìn)行3次牛頓迭代,那誤差只有小數(shù)點(diǎn)最后的1,2位。CHRIS LOMONT在他的文中提到他說(shuō)計(jì)算出來(lái)的理論最優(yōu)值,而這個(gè)幻數(shù)只是在線性估計(jì)時(shí)是最優(yōu)的。在多次牛頓迭代中,這個(gè)值并不是最優(yōu)的。CHRIS LOMONT并沒(méi)有給出對(duì)于多次牛頓迭代最優(yōu)幻數(shù)的計(jì)算方法,他在文章中對(duì)于float類型的實(shí)際最優(yōu)值也是窮舉得到的。我們同樣在理論最優(yōu)值0x5fe6ec85e7de30da的基礎(chǔ)上進(jìn)行了一定的窮舉操作,發(fā)現(xiàn)的確有更優(yōu)的幻數(shù)。但是即使使用了更優(yōu)的幻數(shù),還是無(wú)法在3次牛頓迭代基礎(chǔ)上達(dá)到精度要求。但是我們發(fā)現(xiàn)所有的數(shù)值都偏小。于是我們可以在三次牛頓迭代后再乘一個(gè)比1大一點(diǎn)點(diǎn)的偏移量。從而能做到3次牛頓迭代就能達(dá)到精度要求。示例代碼如下:

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;
}

由于時(shí)間原因,這里并沒(méi)有對(duì)newmagicnum和offset進(jìn)行詳細(xì)的計(jì)算與統(tǒng)計(jì)。只給出一個(gè)對(duì)于本程序相對(duì)較優(yōu)的newmagicnum值0x5fe6d250b0000000。
在上面的代碼中只進(jìn)行了3次牛頓迭代。對(duì)于Intel的VML來(lái)說(shuō)也沒(méi)有什么優(yōu)勢(shì)可言。那能不能再減少一次牛頓迭代,只進(jìn)行2次迭代就達(dá)到精度要求那?
我們知道要進(jìn)行2次牛頓迭代就達(dá)到精度要求就必須對(duì)其初始值的估計(jì)更加準(zhǔn)確。而使用上面的方法估計(jì)的初始值已經(jīng)無(wú)法滿足該準(zhǔn)確性。這是通過(guò)查找《Intel 64 and IA-32 Architectures Optimization Reference Manual》,我們發(fā)現(xiàn)SSE指令集中有一條RSQRTPS的指令用于同時(shí)計(jì)算四個(gè)單精度浮點(diǎn)數(shù)的開(kāi)根號(hào)倒數(shù),而其在Core Microarchitecture上的延遲為3個(gè)周期,吞吐率為2個(gè)周期。也就是說(shuō)我們可以在極短的時(shí)間內(nèi)就算出單精度類型的開(kāi)根號(hào)倒數(shù)值(看來(lái)在現(xiàn)在的CPU上,當(dāng)初Quake 3那段具有傳奇色彩的代碼已經(jīng)沒(méi)有用了)。于是我們想到了先使用單精度類型精度初值估算,然后再使用牛頓迭代。實(shí)驗(yàn)結(jié)果表明該方法只需要進(jìn)行2次牛頓迭代就能滿足小數(shù)點(diǎn)后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;
}

不幸的是由于該代碼涉及到了復(fù)雜的算法以及類型轉(zhuǎn)換,Intel的編譯器并無(wú)法將其很好的并行化。所以只有依靠手工使用匯編語(yǔ)言將其優(yōu)化。
3.3 匯編碼實(shí)現(xiàn)
在實(shí)現(xiàn)匯編碼前先要將原來(lái)的代碼進(jìn)行優(yōu)化,將牛頓迭代中的減法變成加法,代碼如下:

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;
}

進(jìn)行這種轉(zhuǎn)變是一點(diǎn)都不影響計(jì)算結(jié)果的。但是確可以提高計(jì)算速度。這是因?yàn)椋绻麍?zhí)行的是減法,匯編語(yǔ)言的減法指令會(huì)將結(jié)果存在原來(lái)存放被減數(shù)(即1.5)的寄存器中。從而覆蓋掉了原來(lái)的常數(shù)1.5,使得每次計(jì)算必須重新讀入該參數(shù)。而優(yōu)化成加法后則沒(méi)有這個(gè)問(wèn)題。
下面列出了本次匯編語(yǔ)言優(yōu)化時(shí)使用的主要的匯編指令及其延遲,吞吐率和使用的計(jì)算部件。這些數(shù)據(jù)對(duì)優(yōu)化匯編代碼有幫助。
指令名
延遲
吞吐率
計(jì)算部件
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
(注:以上數(shù)據(jù)摘自《Intel 64 and IA-32 Architectures Optimization Reference Manual》)
在進(jìn)行優(yōu)化前,還有一點(diǎn)需要注意的是。rsqrtps函數(shù)是4個(gè)元素一算的,所以本程序使用4個(gè)元素作為一次計(jì)算單元來(lái)向量化。而用戶輸入的數(shù)據(jù)并不可能是正好4個(gè)元素。對(duì)于Intel編譯器以及VML函數(shù)庫(kù)來(lái)所,其使用的解決方法稱為” Strip-mining and Cleanup”。即先按照4個(gè)數(shù)據(jù)一組進(jìn)行計(jì)算。對(duì)于剩下的個(gè)別數(shù)據(jù)再進(jìn)行單獨(dú)計(jì)算。這對(duì)于通用化的程序來(lái)說(shuō)是必須的。但是在我們的程序中,多計(jì)算幾個(gè)并不會(huì)影響結(jié)果。而對(duì)于單獨(dú)幾個(gè)的數(shù)據(jù)如果另外處理不但會(huì)增加程序設(shè)計(jì)的復(fù)雜性,而且性能也可能會(huì)降低。所以本程序使用過(guò)渡計(jì)算的方法。即對(duì)于需要計(jì)算的數(shù)據(jù)中不足4個(gè)的,補(bǔ)滿4個(gè)將其后面的數(shù)據(jù)計(jì)算掉。但是此時(shí)需要注意,由于dist變量是全局變量,默認(rèn)的值為全0。如果過(guò)渡計(jì)算遇到0的值,速度可能會(huì)受到影響。所以本程序需要在一開(kāi)始,將會(huì)被過(guò)渡計(jì)算使用到,但是從來(lái)不會(huì)被初始化的存儲(chǔ)單元,初始化成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); //調(diào)用方法

對(duì)于本函數(shù)的調(diào)用方法為分別傳入其需要計(jì)算數(shù)據(jù)的頭指針和尾指針。
3.4 性能分析
使用匯編語(yǔ)言優(yōu)化后,程序跑出了驚人的0.312秒的好成績(jī)。并且所有的輸出數(shù)據(jù)全部都滿足小數(shù)點(diǎn)后7位的精度要求。在使用Intel Threading Checker和Intel Threading Profiler分析程序時(shí)也得到了相對(duì)比較好的結(jié)果。如下圖:
 
在Intel Threading Checker的檢測(cè)中,沒(méi)有發(fā)現(xiàn)程序有任何沖突。在使用Intel Threading Profiler的分析中,表現(xiàn)出了程序良好的并行性。
最后,在另外一臺(tái)Intel酷睿2 E6600的機(jī)器上測(cè)試時(shí),程序達(dá)到了0.25秒的好成績(jī),并且所有數(shù)據(jù)輸出精度都達(dá)到了小數(shù)點(diǎn)后7位。
3.5 總結(jié)
在本次優(yōu)化比賽中。我花了幾個(gè)星期仔細(xì)鉆研Intel的工具使用方法,并且結(jié)合Intel的CPU特性對(duì)源代碼進(jìn)行優(yōu)化。在經(jīng)過(guò)了漫長(zhǎng)的調(diào)優(yōu)后,終于在保留小數(shù)點(diǎn)后7位精度的七位精度的情況下達(dá)到了0.25秒的成績(jī)。這里需要說(shuō)明的是,在本程序的最后優(yōu)化時(shí)雖然沒(méi)有使用Intel的VML庫(kù),但并不是意味著VML庫(kù)不好。VML庫(kù)的通用化和高效率是有目共睹的。而是由于VML庫(kù)是通用庫(kù),其需要考慮很多情況,而針對(duì)本程序自己設(shè)計(jì)的計(jì)算函數(shù)卻不用考慮各自情況。所以設(shè)計(jì)有針對(duì)性的函數(shù)才能提高速度。當(dāng)然要設(shè)計(jì)這種函數(shù)對(duì)用戶的要求太高,需要了解數(shù)學(xué)理論,匯編語(yǔ)言,以及優(yōu)化的方法。所以對(duì)于一般的用戶還是使用VML庫(kù)比較好。
最后需要說(shuō)明的是,由于本次競(jìng)賽的時(shí)間有限。很多地方都沒(méi)有得到更好的優(yōu)化。比如那段匯編語(yǔ)言就能針對(duì)Intel CPU的指令延遲特性進(jìn)一步優(yōu)化。如果大賽能給出更多的時(shí)間,那有可能可以優(yōu)化得更好。
 
 
附錄A 目錄結(jié)構(gòu)和編譯方法
本報(bào)告壓縮文件的根目錄下分別有version1,version2,version3三個(gè)目錄,分別對(duì)應(yīng)第一,第二,第三章中的,串行優(yōu)化,并行優(yōu)化,匯編優(yōu)化三個(gè)不同階段的版本。其中version3是最終版。
在每個(gè)version目錄下,有Microsoft Visual Studio的項(xiàng)目文件,可以使用Microsoft Visual Studio直接打開(kāi)。在對(duì)應(yīng)的Release目錄下有已經(jīng)編譯好的可執(zhí)行文件。程序的優(yōu)化選項(xiàng)可以在Microsoft Visual Studio中看到,具體的解釋可以在第一章中查找。
此外,在壓縮文件的根目錄下有最終板的可執(zhí)行文件potential_serial(final).exe方便進(jìn)行測(cè)試。
 
附錄B 參考文獻(xiàn)
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
 

發(fā)表于 @ 2007年01月21日 13:58:00|評(píng)論(2 )|編輯

 | 

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多