|
在現(xiàn)代 CPU 中,并行性操作大致分為三種類型: (1)指令級(jí)并行,主要由 cpu 流水線技術(shù),亂序執(zhí)行技術(shù)等技術(shù)完成。 (2)線程級(jí)并行,主要依靠多核多線程技術(shù)實(shí)現(xiàn)。 (3)數(shù)據(jù)級(jí)并行,主要依靠 SIMD (單指令多數(shù)據(jù)) 來實(shí)現(xiàn)。 指令級(jí)并行和線程級(jí)并行這兩種技術(shù)不在本文進(jìn)行討論,本文將詳細(xì)介紹 SIMD 及其使用方法。 SIMD 介紹SIMD 是 CPU 硬件設(shè)計(jì)的一部分,并且是可以通過指令集架構(gòu) (ISA) 直接訪問。SIMD 描述了具有多個(gè)處理元件的計(jì)算機(jī)同時(shí)對(duì)多個(gè)數(shù)據(jù)執(zhí)行相同操作的過程。 以加法指令為例,單指令單數(shù)據(jù) (SISD) 的 CPU 對(duì)加法指令譯碼后,執(zhí)行部件先訪問內(nèi)存,取得第一個(gè)操作數(shù),之后再一次訪問內(nèi)存,取得第二個(gè)操作數(shù),隨后才能進(jìn)行求和運(yùn)算。而在支持 SIMD 的 CPU 中,指令譯碼后幾個(gè)執(zhí)行部件同時(shí)訪問內(nèi)存,一次性獲得所有操作數(shù)進(jìn)行運(yùn)算。這個(gè)特點(diǎn)使 SIMD 特別適合于多媒體應(yīng)用等數(shù)據(jù)密集型運(yùn)算。
英特爾的第一個(gè) IA-32 SIMD 指令集是 MMX 指令集。它沿用了 x87 時(shí)代的浮點(diǎn)寄存器,使 CPU 無法對(duì)浮點(diǎn)數(shù)進(jìn)行 SIMD 操作,只能處理整數(shù)。SIMD 流指令擴(kuò)展 (SSE) 是 x86 架構(gòu)的 SIMD 指令集擴(kuò)展,最早是由 Intel 設(shè)計(jì)并在1999年推出的奔騰3系列 CPU 中引入。SSE 浮點(diǎn)指令在新的獨(dú)立寄存器 XMM 上運(yùn)行,并擴(kuò)展了一些在 MMX 寄存器上運(yùn)行的整數(shù)指令。SSE 包含了70條新指令,其中大部分都適用于單精度浮點(diǎn)數(shù)據(jù)類型 (float) 。 SSE最初添加了8個(gè)新的128位寄存器,XMM0 - XMM7,在 AMD64 拓展里面,又額外添加了8個(gè)寄存器,XMM8 - XMM15,這個(gè)拓展也被引入到了 Intel64 位處理器 (IA-64) 架構(gòu)中。不過寄存器XMM8 - XMM15 只能用來處理 64bit 的操作數(shù)。SSE2 進(jìn)一步支持雙精度浮點(diǎn)數(shù),由于寄存器長(zhǎng)度沒有變長(zhǎng),所以只能支持2個(gè)雙精度浮點(diǎn)計(jì)算或是4個(gè)單精度浮點(diǎn)計(jì)算。另外,它在這組寄存器上實(shí)現(xiàn)了整型計(jì)算,從而代替了 MMX。 在 Intel 的 AVX 指令集中,將 SSE 的128位數(shù)據(jù)通道拓寬到256位,并由此產(chǎn)生的寄存器稱之為YMM。并且 AVX 全面兼容 SSE/SSE2/SSE3/SSE4,也就是 YMM 寄存器的低128位就是 XMM 寄存器。
現(xiàn)代編譯器有三種方式來支持 SIMD: (1)編譯器能夠在沒有用戶干預(yù)的情況下生成 SIMD 代碼,稱之為自動(dòng)矢量化。 (2)用戶可以插入 Intrinsics 函數(shù)實(shí)現(xiàn) SIMD。 (3)用戶可以使用矢量 C++ 類 (僅限ICC編譯器) 來實(shí)現(xiàn) SIMD。 1. 自動(dòng)矢量化在程序編寫過程中,可能會(huì)經(jīng)常遇到以下循環(huán)的方式,一次執(zhí)行許多數(shù)字的加法。
在 gcc 編譯器中,如果添加了 -ftree-vectorize 編譯選項(xiàng) (O2已包含此優(yōu)化選項(xiàng)),那么編譯器可以自動(dòng)將這類循環(huán)轉(zhuǎn)換成矢量操作序列。 下面以 vector.c 做實(shí)驗(yàn),看看編譯器怎么實(shí)現(xiàn)適量自動(dòng)化。
對(duì)其進(jìn)行編譯運(yùn)行,第一次編譯不帶 -ftree-vectorize,即期望編譯器不對(duì)這段代碼進(jìn)行自動(dòng)矢量化。第二次帶上 -ftree-vectorize,期望編譯器能對(duì)其進(jìn)行自動(dòng)矢量化。由于 -ftree-vectorize 只在 -O1 優(yōu)化下才生效,所以兩次編譯也都帶了 -O1 進(jìn)行。
從運(yùn)行耗時(shí)可以看出,矢量化編譯 (-ftree-vectorize) 之后性能大大提升。再將這段程序的匯編碼打印出來,可以看出矢量化編譯之后,匯編碼里面使用了 XMM 寄存器。前面介紹過了,XMM 寄存器可以同時(shí)裝載4個(gè) int 類型的數(shù)據(jù),并對(duì)其進(jìn)行相同的操作,這也就是性能提升的關(guān)鍵。
上面的代碼很容易自動(dòng)矢量化,我們?cè)賮韺?duì)比一下這兩段代碼:
區(qū)別之處已加粗顯示,分別將他們編成匯編碼,再進(jìn)行對(duì)比:
顯然右側(cè)程序的匯編碼使用了 XMM 寄存器,而左邊的程序卻沒有。 假如他們都會(huì)進(jìn)行矢量化,那么以下4條操作是要同時(shí)進(jìn)行的,假如 a[0] = 0, a[1] = 1, a[2] = 2...,那么左邊的程序運(yùn)行完之后,得到的結(jié)果 a[1] = 0, a[2] = 1, a[3] = 2...。但實(shí)際上,左邊程序運(yùn)行完之后應(yīng)該得到的結(jié)果是 a[1] = 0, a[2] = 0, a[3] = 0...。所以,如果左邊的程序也矢量化,那么程序的結(jié)果就是錯(cuò)誤的。而右邊的程序卻不受影響,雖然右邊程序 a[i+4] 的值也依賴于 a[i],但是他們地址相差128位,而XMM寄存器剛好是128位寬,矢量化運(yùn)行之后也不影響本來的結(jié)果。
自動(dòng)矢量化,就像任何循環(huán)優(yōu)化或其他編譯優(yōu)化一樣,必須準(zhǔn)確地保留程序行為。在執(zhí)行期間必須遵守所有的依賴項(xiàng),以防止出現(xiàn)錯(cuò)誤結(jié)果。如果出現(xiàn)處理不了的循環(huán)依賴,那么循環(huán)依賴必須獨(dú)立于矢量化指令執(zhí)行。 要矢量化一個(gè)程序,編譯器必須首先了解語句之間的依賴關(guān)系,并在必要時(shí)重新對(duì)齊它們。 一旦映射了依賴關(guān)系,編譯器必須正確安排實(shí)現(xiàn)指令,將適當(dāng)?shù)暮蜻x者更改為矢量指令,并用這些指令對(duì)多個(gè)數(shù)據(jù)項(xiàng)進(jìn)行操作。 編譯器進(jìn)行矢量自動(dòng)化優(yōu)化通常會(huì)經(jīng)歷一下三個(gè)步驟: (1)建立依賴圖。識(shí)別哪些語句依賴于哪些其他語句。這包括檢查每個(gè)語句并識(shí)別語句訪問的每個(gè)數(shù)據(jù)項(xiàng),將數(shù)組訪問修飾符映射到函數(shù)以及檢查每個(gè)訪問對(duì)所有語句中其他訪問的依賴關(guān)系。依賴圖包含了距離不大于矢量大小的所有局部依賴。 如果矢量寄存器為 128 位,數(shù)組類型為 32 位,則矢量大小為 128/32 = 4。所有其他非循環(huán)依賴項(xiàng)都不應(yīng)使其矢量化無效,因?yàn)樗麄儾粫?huì)調(diào)用相同的矢量指令。 (2)聚類。使用依賴圖,優(yōu)化器可以對(duì)強(qiáng)連接組件 (SCC) 進(jìn)行聚類,并將可矢量化語句與其余語句分開。例如,一個(gè)程序的循環(huán)內(nèi)包含三個(gè)語句組:(SCC1+SCC2)、SCC3 和 SCC4,其中只有第二組 (SCC3) 可以矢量化。那么最終的程序?qū)齻€(gè)循環(huán),每個(gè)循環(huán)一個(gè)語句組,只有中間的循環(huán)被矢量化。 優(yōu)化器不能在不違反語句執(zhí)行順序的情況下將第一個(gè)與最后一個(gè)連接起來,因?yàn)檫@很可能會(huì)保證不了數(shù)據(jù)有效性。 (3)監(jiān)測(cè)慣用語法,一些不明顯的依賴可以根據(jù)特定的習(xí)慣用法進(jìn)一步優(yōu)化。例如,下面的數(shù)據(jù)依賴項(xiàng)可以進(jìn)行矢量化,因?yàn)榭梢垣@取右側(cè)值然后將其存儲(chǔ)在左側(cè)值上,因此數(shù)據(jù)不會(huì)在賦值中發(fā)生變化。
2. 插入 Intrinsics 函數(shù) (內(nèi)在函數(shù)) 實(shí)現(xiàn) SIMD對(duì)于程序員來說,intrinsics 看起來就像普通的庫函數(shù)。只要包含了相關(guān)的頭文件,就可以使用內(nèi)在函數(shù)。如果要將4個(gè)整數(shù)和另外4個(gè)整數(shù)相加,可以使用 _mm_add_epi32 內(nèi)在函數(shù)。這個(gè)函數(shù)的聲明包含在 <emmintrin.h> 頭文件中。
intrinsics 與庫函數(shù)不同的是,intrinsics 是直接在編譯器中實(shí)現(xiàn)的。上面的 _mm_add_epi32 SSE2內(nèi)在函數(shù)通常編譯成一條指令 paddd。__m128i 內(nèi)置數(shù)據(jù)類型是四個(gè)整數(shù)型的矢量,每個(gè) 32 位,總共 128 位。編譯器將發(fā)出兩條指令:第一條將參數(shù)從內(nèi)存加載到寄存器中,第二條將四個(gè)值相加。通常來說,CPU 調(diào)用一個(gè)庫函數(shù)所花費(fèi)的時(shí)間,可能是調(diào)用 intrinsics 的數(shù)倍。 包含足夠數(shù)量的矢量 intrinsics 或嵌入等效匯編源代碼的過程稱為手動(dòng)矢量化?,F(xiàn)代編譯器和庫已經(jīng)使用內(nèi)在函數(shù)、匯編或兩者的組合實(shí)現(xiàn)了很多東西。例如,memset,memcpy 或 memmove 標(biāo)準(zhǔn) C 庫函數(shù)的實(shí)現(xiàn)就使用 SSE2 指令以獲得更好的性能。然而,在高性能計(jì)算、游戲開發(fā)或編譯器開發(fā)等細(xì)分領(lǐng)域之外,即使是非常有經(jīng)驗(yàn)的 C 和 C++ 程序員在很大程度上也不熟悉 SIMD 內(nèi)在函數(shù)。 下面函數(shù)是使用 intrinsics 函數(shù)來實(shí)現(xiàn) c[i] = a[i] + b[i] 。
將文件編成匯編碼,匯編碼包含了 paddd %xmm1, %xmm0
測(cè)試一下這個(gè)程序的性能, 比前面介紹的直接使用矢量化優(yōu)化的程序性能還要好一些。
3. 利用 C++ 類 (ICC編譯器專用)使用該方法依賴于環(huán)境里面安裝了 Intel ICC 編譯器。ICC 編譯器集成在 Intel oneAPI 開發(fā)套件里面,下載鏈接:Download the Intel? oneAPI Base Toolkit 直接在 linux 上用 yum 安裝: (1)在 /temp 文件夾底下創(chuàng)建 YUM 的 repo 文件
(2)將新建的 repo 文件移到 /etc/yum.repos.d
(3)使用 yum 命令安裝 Intel? oneAPI Base Toolkit
(4)在使用編譯器之前運(yùn)行setvars.sh設(shè)置環(huán)境變量
(5)安裝結(jié)束,查看編譯器版本
使用 C++ 類進(jìn)行 SIMD 操作允許在單個(gè)操作中對(duì)數(shù)組或數(shù)據(jù)向量進(jìn)行操作。同樣是計(jì)算 c[i] = a[i] + b[i],用傳統(tǒng)數(shù)組的方法表示如下:
在 ICC 編譯器中可以使用 Ivec 類來表示 (需要添加頭文件 dvec.h)
所以可以把前面示例的程序改造如下:
把程序變成匯編碼,檢查一下是否用了 XMM 寄存器,C++ 匯編碼編譯出來太長(zhǎng)了,就不貼全部源碼。果然 paddd 指令和 XMM 寄存器也都使用了。
總結(jié)SIMD 的使用不是那么簡(jiǎn)單,一般程序員也不太會(huì)使用 Intrinsics 函數(shù)或者 Ivec 類來優(yōu)化 SIMD,基本上都是靠編譯器幫我們進(jìn)行自動(dòng)矢量化。 想要代碼能盡量的自動(dòng)矢量化,以下幾點(diǎn)其實(shí)是我們可以做到的: - 避免使用全局指針和全局變量以幫助編譯器生成 SIMD 代碼。 - 使用盡可能小的 SIMD 數(shù)據(jù)類型,通過使用更長(zhǎng)的 SIMD 矢量長(zhǎng)度來實(shí)現(xiàn)更多的并行性。 - 合理安排循環(huán)的嵌套,以便最內(nèi)層的嵌套沒有迭代間的依賴關(guān)系。尤其要避免在較早的迭代中存儲(chǔ)數(shù)據(jù),而在往后的迭代中加載該數(shù)據(jù)。 - 避免在循環(huán)內(nèi)使用條件分支。 - 保持循環(huán)變量表達(dá)式簡(jiǎn)單。 參考文獻(xiàn): 《64-ia-32-architectures-optimization-manual》 https://en./wiki/Streaming_SIMD_Extensions CS3330: A quick guide to SSE/SIMD https://www.eidos.ic.i./~tau/lecture/parallel_distributed/2018/slides/pdf/simd2.pdf Improving performance with SIMD intrinsics in three use cases - Stack Overflow Blog |
|
|