小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

深入代碼優(yōu)化 (二) 使用SIMD優(yōu)化程序

 lichwoo 2025-01-08 發(fā)布于中國(guó)香港

在現(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ù)字的加法。

  1. for (i = 0; i < n; i ++)
  2. c[i] = a[i] + b[i];

在 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)化。

  1. [root@wuhan ~]# cat vector.c
  2. #include <stdio.h>
  3. #define N 100000
  4. #define M 1024
  5. int a[M], b[M], c[M];
  6. int main()
  7. {
  8. int i, j;
  9. for (j = 0; j < N; j ++)
  10. for (i = 0; i < M; i ++)
  11. c[i] = a[i] + b[i];
  12. return 0;
  13. }

對(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)行。

  1. [root@wuhan ~]# gcc vector.c -O1 -o no_vector
  2. [root@wuhan ~]# time ./no_vector
  3. real 0m0.110s
  4. user 0m0.108s
  5. sys 0m0.001s
  6. [root@wuhan ~]# gcc vector.c -O1 -o with_vector -ftree-vectorize
  7. [root@wuhan ~]# time ./with_vector
  8. real 0m0.029s
  9. user 0m0.028s
  10. sys 0m0.000s

從運(yùn)行耗時(shí)可以看出,矢量化編譯 (-ftree-vectorize) 之后性能大大提升。再將這段程序的匯編碼打印出來,可以看出矢量化編譯之后,匯編碼里面使用了 XMM 寄存器。前面介紹過了,XMM 寄存器可以同時(shí)裝載4個(gè) int 類型的數(shù)據(jù),并對(duì)其進(jìn)行相同的操作,這也就是性能提升的關(guān)鍵。

[root@wuhan ~]# gcc vector.c -O1 -S             
[root@wuhan ~]# cat vector.s 
        .file   "vector.c"
        .text
        .globl  main
        .type   main, @function
main:
.LFB11:
        .cfi_startproc
        movl    $100000, %ecx
        jmp     .L2
.L6:
        subl    $1, %ecx
        je      .L4
.L2:
        movl    $0, %eax
.L3:
        movl    b(%rax), %edx
        addl    a(%rax), %edx
        movl    %edx, c(%rax)
        addq    $4, %rax
        cmpq    $4096, %rax
        jne     .L3
        jmp     .L6
.L4:
        movl    $0, %eax
        ret
        .cfi_endproc
.LFE11:
        .size   main, .-main
        .comm   c,4096,32
        .comm   b,4096,32
        .comm   a,4096,32
        .ident  "GCC: (GNU) 8.4.1 20200928 (Red Hat 8.4.1-1)"
        .section        .note.GNU-stack,"",@progbits

[root@wuhan ~]# gcc vector.c -O1 -ftree-vectorize -S
[root@wuhan ~]# cat vector.s 
        .file   "vector.c"
        .text
        .globl  main
        .type   main, @function
main:
.LFB11:
        .cfi_startproc
        movl    $100000, %edx
.L2:
        movl    $0, %eax
.L3:
       
movdqa  a(%rax), %xmm0
        paddd   b(%rax), %xmm0
        movaps  %xmm0, c(%rax)
        addq    $16, %rax
        cmpq    $4096, %rax
        jne     .L3
        subl    $1, %edx
        jne     .L2
        movl    $0, %eax
        ret
        .cfi_endproc
.LFE11:
        .size   main, .-main
        .comm   c,4096,32
        .comm   b,4096,32
        .comm   a,4096,32
        .ident  "GCC: (GNU) 8.4.1 20200928 (Red Hat 8.4.1-1)"
        .section        .note.GNU-stack,"",@progbits

上面的代碼很容易自動(dòng)矢量化,我們?cè)賮韺?duì)比一下這兩段代碼:

#include <stdio.h>

#define N 100000
#define M 1024
int a[M+16], b[M], c[M];

int main()
{
    int i, j;
    for (j = 0; j < N; j ++)
        for (i = 0; i < M; i ++)
        {
            a[i+1] = a[i];
        }
    return 0;
}

#include <stdio.h>

#define N 100000
#define M 1024
int a[M+16], b[M], c[M];

int main()
{
    int i, j;
    for (j = 0; j < N; j ++)
        for (i = 0; i < M; i ++)
        {
            a[i+4] = a[i];
        }
    return 0;
}

區(qū)別之處已加粗顯示,分別將他們編成匯編碼,再進(jìn)行對(duì)比:

[root@wuhan ~]# gcc vector2.c -O1 -ftree-vectorize -S

[root@wuhan ~]# cat vector2.s 
        .file   "vector2.c"
        .text
        .globl  main
        .type   main, @function
main:
.LFB11:
        .cfi_startproc
        movl    $100000, %esi
        movl    $a+4096, %ecx
        jmp     .L2
.L6:
        subl    $1, %esi
        je      .L4
.L2:
        movl    $a, %eax
.L3:
        movl    (%rax), %edx
        movl    %edx, 4(%rax)
        addq    $4, %rax
        cmpq    %rax, %rcx
        jne     .L3
        jmp     .L6
.L4:
        movl    $0, %eax
        ret
        .cfi_endproc
.LFE11:
        .size   main, .-main
        .comm   c,4096,32
        .comm   b,4096,32
        .comm   a,4160,32
        .ident  "GCC: (GNU) 8.4.1 20200928 (Red Hat 8.4.1-1)"
        .section        .note.GNU-stack,"",@progbits

[root@wuhan ~]# gcc vector2.c -O1 -ftree-vectorize -S

[root@wuhan ~]# cat vector2.s 
        .file   "vector2.c"
        .text
        .globl  main
        .type   main, @function
main:
.LFB11:
        .cfi_startproc
        movl    $100000, %ecx
        movl    $a+4096, %edx
.L2:
        movl    $a, %eax
.L3:
       
movdqa  (%rax), %xmm0
        movaps  %xmm0, 16(%rax)
        addq    $16, %rax
        cmpq    %rax, %rdx
        jne     .L3
        subl    $1, %ecx
        jne     .L2
        movl    $0, %eax
        ret
        .cfi_endproc
.LFE11:
        .size   main, .-main
        .comm   c,4096,32
        .comm   b,4096,32
        .comm   a,4160,32
        .ident  "GCC: (GNU) 8.4.1 20200928 (Red Hat 8.4.1-1)"
        .section        .note.GNU-stack,"",@progbits

顯然右側(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é)果。

a[1] = a[0];

a[2] = a[1];

a[3] = a[2];

a[4] = a[3];

a[4] = a[0];

a[5] = a[1];

a[6] = a[2];

a[7] = a[3];

自動(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ā)生變化。

a[i] = a[i+1];  /* 不同于a[i+1] = a[i], a[i+1] = a[i]不能矢量化 */

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] 。

  1. [root@wuhan ~]# cat vector_sse2.c
  2. #include <stdio.h>
  3. #include <emmintrin.h>
  4. #define N 100000
  5. #define M 1024
  6. int a[M], b[M], c[M];
  7. int add_sse2(int size, int *a, int *b, int *c) {
  8. int i = 0;
  9. for (; i + 4 <= size; i += 4) {
  10. /* 加載 a, b 數(shù)組的 128 位塊 */
  11. __m128i ma = _mm_loadu_si128((__m128i*) &a[i]);
  12. __m128i mb = _mm_loadu_si128((__m128i*) &b[i]);
  13. /* 128 位塊矢量相加 */
  14. ma = _mm_add_epi32(ma, mb);
  15. /* 將相加結(jié)果存儲(chǔ)到 c 數(shù)組的 128 位塊 */
  16. _mm_storeu_si128((__m128i*) &c[i], ma);
  17. }
  18. }
  19. int main()
  20. {
  21. for (int i = 0; i < N; i ++)
  22. add_sse2(M, a, b, c);
  23. return 0;
  24. }

 將文件編成匯編碼,匯編碼包含了 paddd   %xmm1, %xmm0

  1. [root@wuhan ~]# gcc vector_sse2.c -O1 -S
  2. [root@wuhan ~]# cat vector_sse2.s
  3. .file "vector_sse2.c"
  4. .text
  5. .comm a,4160,32
  6. .comm b,4096,32
  7. .comm c,4096,32
  8. .globl add_sse2
  9. .type add_sse2, @function
  10. add_sse2:
  11. .LFB503:
  12. .cfi_startproc
  13. pushq %rbp
  14. .cfi_def_cfa_offset 16
  15. .cfi_offset 6, -16
  16. movq %rsp, %rbp
  17. .cfi_def_cfa_register 6
  18. subq $40, %rsp
  19. movl %edi, -132(%rbp)
  20. movq %rsi, -144(%rbp)
  21. movq %rdx, -152(%rbp)
  22. movq %rcx, -160(%rbp)
  23. movl $0, -4(%rbp)
  24. jmp .L2
  25. .L6:
  26. movl -4(%rbp), %eax
  27. cltq
  28. leaq 0(,%rax,4), %rdx
  29. movq -144(%rbp), %rax
  30. addq %rdx, %rax
  31. movq %rax, -128(%rbp)
  32. movq -128(%rbp), %rax
  33. movdqu (%rax), %xmm0
  34. movaps %xmm0, -32(%rbp)
  35. movl -4(%rbp), %eax
  36. cltq
  37. leaq 0(,%rax,4), %rdx
  38. movq -152(%rbp), %rax
  39. addq %rdx, %rax
  40. movq %rax, -120(%rbp)
  41. movq -120(%rbp), %rax
  42. movdqu (%rax), %xmm0
  43. movaps %xmm0, -48(%rbp)
  44. movdqa -32(%rbp), %xmm0
  45. movaps %xmm0, -96(%rbp)
  46. movdqa -48(%rbp), %xmm0
  47. movaps %xmm0, -112(%rbp)
  48. movdqa -96(%rbp), %xmm1
  49. movdqa -112(%rbp), %xmm0
  50. paddd %xmm1, %xmm0
  51. movaps %xmm0, -32(%rbp)
  52. movl -4(%rbp), %eax
  53. cltq
  54. leaq 0(,%rax,4), %rdx
  55. movq -160(%rbp), %rax
  56. addq %rdx, %rax
  57. movq %rax, -56(%rbp)
  58. movdqa -32(%rbp), %xmm0
  59. movaps %xmm0, -80(%rbp)
  60. movdqa -80(%rbp), %xmm0
  61. movq -56(%rbp), %rax
  62. movups %xmm0, (%rax)
  63. addl $4, -4(%rbp)
  64. .L2:
  65. movl -4(%rbp), %eax
  66. addl $3, %eax
  67. cmpl %eax, -132(%rbp)
  68. jg .L6
  69. nop
  70. leave
  71. .cfi_def_cfa 7, 8
  72. ret
  73. .cfi_endproc
  74. .LFE503:
  75. .size add_sse2, .-add_sse2
  76. .globl main
  77. .type main, @function
  78. main:
  79. .LFB504:
  80. .cfi_startproc
  81. pushq %rbp
  82. .cfi_def_cfa_offset 16
  83. .cfi_offset 6, -16
  84. movq %rsp, %rbp
  85. .cfi_def_cfa_register 6
  86. subq $16, %rsp
  87. movl $0, -4(%rbp)
  88. jmp .L8
  89. .L9:
  90. movl $c, %ecx
  91. movl $b, %edx
  92. movl $a, %esi
  93. movl $1024, %edi
  94. call add_sse2
  95. addl $1, -4(%rbp)
  96. .L8:
  97. cmpl $99999, -4(%rbp)
  98. jle .L9
  99. movl $0, %eax
  100. leave
  101. .cfi_def_cfa 7, 8
  102. ret
  103. .cfi_endproc
  104. .LFE504:
  105. .size main, .-main
  106. .ident "GCC: (GNU) 8.4.1 20200928 (Red Hat 8.4.1-1)"
  107. .section .note.GNU-stack,"",@progbits

測(cè)試一下這個(gè)程序的性能, 比前面介紹的直接使用矢量化優(yōu)化的程序性能還要好一些。

[root@wuhan ~]# gcc vector.c -O1 -o with_vector -ftree-vectorize
[root@wuhan ~]# time ./with_vector 

real    0m0.029s
user    0m0.028s
sys     0m0.000s

[root@wuhan ~]# gcc vector_sse2.c -o vector_sse2 -O1
[root@wuhan ~]# time ./vector_sse2 

real    0m0.020s
user    0m0.020s
sys     0m0.000s

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 文件

  1. tee > /tmp/oneAPI.repo << EOF
  2. [oneAPI]
  3. name=Intel? oneAPI repository
  4. baseurl=https://yum.repos.intel.com/oneapi
  5. enabled=1
  6. gpgcheck=1
  7. repo_gpgcheck=1
  8. gpgkey=https://yum.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB
  9. EOF

(2)將新建的 repo 文件移到 /etc/yum.repos.d

sudo mv /tmp/oneAPI.repo /etc/yum.repos.d

(3)使用 yum 命令安裝 Intel? oneAPI Base Toolkit

sudo yum install intel-basekit

(4)在使用編譯器之前運(yùn)行setvars.sh設(shè)置環(huán)境變量

source /opt/intel/oneapi/setvars.sh intel64

(5)安裝結(jié)束,查看編譯器版本

  1. [root@wuhan ~]# icpx --version
  2. Intel(R) oneAPI DPC++/C++ Compiler 2021.4.0 (2021.4.0.20210924)
  3. Target: x86_64-unknown-linux-gnu
  4. Thread model: posix
  5. InstalledDir: /opt/intel/oneapi/compiler/2021.4.0/linux/bin

使用 C++ 類進(jìn)行 SIMD 操作允許在單個(gè)操作中對(duì)數(shù)組或數(shù)據(jù)向量進(jìn)行操作。同樣是計(jì)算 c[i] = a[i] + b[i],用傳統(tǒng)數(shù)組的方法表示如下:

  1. int a[4], b[4], c[4];
  2. for (i=0; i<4; i++) /* 需要4次迭代 */
  3. c[i] = a[i] + b[i]; /* 分別計(jì)算 c[0], c[1], c[2], c[3] */

在 ICC 編譯器中可以使用 Ivec 類來表示 (需要添加頭文件 dvec.h)

  1. Is32vec4 ivecA, ivecB, ivecC; /* 只需要一次迭代 */
  2. ivecC = ivecA + ivecB; /* 計(jì)算 ivecC0, ivecC1, ivecC2, ivecC3 */

所以可以把前面示例的程序改造如下:

  1. [root@wuhan ~]# cat vector_class.cpp
  2. #include <iostream>
  3. #include <dvec.h>
  4. using namespace std;
  5. #define N 100000
  6. #define M 1024
  7. Is32vec4 a[M/4], b[M/4], c[M/4];
  8. int main()
  9. {
  10. for (int i = 0; i < N; i ++)
  11. for (int j = 0; j < M/4; j ++)
  12. c[j] = a[j] + b[j];
  13. return 0;
  14. }

把程序變成匯編碼,檢查一下是否用了 XMM 寄存器,C++ 匯編碼編譯出來太長(zhǎng)了,就不貼全部源碼。果然 paddd 指令和 XMM 寄存器也都使用了。

  1. [root@wuhan ~]# icpx vector_class.cpp -O1 -S
  2. [root@wuhan ~]# cat vector_class.s | grep -i xmm
  3. movaps %xmm0, c(%rbx)
  4. movdqa %xmm0, (%rsp) # 16-byte Spill
  5. paddd (%rsp), %xmm0 # 16-byte Folded Reload
  6. movaps 16(%rsp), %xmm0
  7. movaps (%rdi), %xmm0
  8. movaps %xmm0, (%rdi)

總結(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://www.intel.com/content/www/us/en/develop/documentation/oneapi-dpcpp-cpp-compiler-dev-guide-and-reference/top/compiler-reference/libraries/intel-c-class-libraries/c-classes-and-simd-operations.html

https://en./wiki/Streaming_SIMD_Extensions

Intel? Intrinsics Guide

CS3330: A quick guide to SSE/SIMD

https://www.eidos.ic.i./~tau/lecture/parallel_distributed/2018/slides/pdf/simd2.pdf

Basics of SIMD Programming

Improving performance with SIMD intrinsics in three use cases - Stack Overflow Blog

https://en./wiki/Automatic_vectorization

SIMD指令學(xué)習(xí)筆記 - 濁酒戀紅塵 - 博客園

    本站是提供個(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)論公約

    類似文章 更多