向量相加其二(C串行、OpenMP、OpenMP AVX2實現)

摘要

向量相加一中比較了純python和numpy實現向量相加的速度情況
在本文中我們使用C語言來討論向量相加的加速,因爲C語言是公認的執行效率較高的高級語言
一般我們個人學習生活中編程思想都是在單個CPU邏輯核上的,現在大家的個人筆記本基本都是4核8核10核甚至80核128核,超算的核數更是成千上萬,這裏的核就是指的邏輯核
如果我們將單核上的任務分給多核,將會得到顯著的性能提升
本文實現的加速方法:

  1. 普通C串行程序
  2. 使用OpenMP並行化向量計算
  3. 使用OpenMP和AVX2指令集並行化計算

測試機配置

由於上一臺測試機處理器不支持AVX2指令集,因此換用我的遊戲本

(a)	CPU
廠家:Intel
型號:Intel®Core™ i7-7700HQ CPU
核數:8
頻率:2.8GHz
指令集:支持AVX2,不支持AVX512

(b)	GPU0
廠家:Intel
型號:Intel®UHD Graphics 630
顯存容量:N/A
顯存頻率:350MHz
顯存帶寬:19.2GB/s

GPU1
廠家:NVIDIA
型號:NVIDIA GeForce GTX 1050Ti
顯存容量:4GB
顯存頻率:1752MHz
顯存帶寬:112.1GB/s

源代碼

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <omp.h>
#include <immintrin.h>

int main(int argc, char **argv)
{
    int n = atoi(argv[1]);
    int repeat = 10;

    // create vectors
    int *a = (int *)malloc(sizeof(int) * n);
    int *b = (int *)malloc(sizeof(int) * n);
    int *c = (int *)malloc(sizeof(int) * n);

    for (int i = 0; i < n; i++)
    {
        a[i] = 1;
        b[i] = 2;
    }

    struct timeval t1, t2;

    // serial version
    gettimeofday(&t1, NULL);
    for (int ri = 0; ri < repeat; ri++)
    {
        for (int i = 0; i < n; i++)
        {
            c[i] = a[i] + b[i];
        }
    }
    gettimeofday(&t2, NULL);
    double time_serial = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0;
    time_serial /= repeat;
    time_serial /= 1000.0;
    printf("C serial takes %f sec\n", time_serial);

    // OpenMP parallel version
    gettimeofday(&t1, NULL);
    for (int ri = 0; ri < repeat; ri++)
    {
        #pragma omp parallel for
        for (int i = 0; i < n; i++)
        {
            c[i] = a[i] + b[i];
        }
    }
    gettimeofday(&t2, NULL);
    time_serial = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0;
    time_serial /= repeat;
    time_serial /= 1000.0;
    printf("C OpenMP takes %f sec\n", time_serial);

    // OpenMP AVX2 parallel version
    gettimeofday(&t1, NULL);
    for (int ri = 0; ri < repeat; ri++)
    {
        int loop = n / 8;
        for (int tid = 0; tid < loop; tid++)
        {
            __m256i aavx2 = _mm256_loadu_si256((__m256i*)(&a[tid * 8]));
            __m256i bavx2 = _mm256_loadu_si256((__m256i*)(&b[tid * 8]));
            __m256i cavx2 = _mm256_add_epi32(aavx2, bavx2);
            _mm256_storeu_si256((__m256i*)(&c[tid * 8]), cavx2);
        }
        #pragma omp parallel for
        for (int i = loop * 8; i < n; i++)
        {
            c[i] = a[i] + b[i];
        }
    }
    gettimeofday(&t2, NULL);
    time_serial = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0;
    time_serial /= repeat;
    time_serial /= 1000.0;
    printf("C OpenMP AVX2 takes %f sec\n", time_serial);

    free(a);
    free(b);
    free(c);
}

說明

1.C串行版和OpenMP版可以用idle環境編譯,AVX2普通idle可能識別不出

C語言並行計算的加速基本都是對內存直接進行操作,還是放棄idle直接將gcc編譯器設置成環境變量然後用cmd編譯吧,越早習慣不用idle越好

打開cmd輸入gcc -v出現如下字樣就說明設置環境變量成功

C:\Users\admin>gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=d:/dev-cpp/mingw32/bin/../libexec/gcc/mingw32/4.7.2/lto-wrapper.exe
Target: mingw32
Configured with: ../gcc-4.7.2/configure --enable-languages=c,c++,ada,fortran,objc,obj-c++ --disable-sjlj-exceptions --with-dwarf2 --enable-shared --enable-libgomp --disable-win32-registry --enable-libstdcxx-debug --disable-build-poststage1-with-cxx --enable-version-specific-runtime-libs --build=mingw32 --prefix=/mingw
Thread model: win32
gcc version 4.7.2 (GCC)

2.sys/time.h這個頭文件idle環境也可能識別不了

sys/time.h原本是Linux系統下定義的獲取時間等等一系列,Windows下普通idle可以識別time.h,不過沒關係,直接打開cmd用gcc編譯器編譯windows下也能識別和運行。非要用time.h的話計時方法也要更改。

運行結果及分析

C:\Users\admin>cd C:\Users\admin\Desktop\

C:\Users\admin\Desktop>gcc -O3 -fopenmp -mavx2 0.c -o 0 -std=c99

C:\Users\admin\Desktop>0.exe 1000000
C serial takes 0.000999 sec
C OpenMP takes 0.001000 sec
C OpenMP AVX2 takes 0.001000 sec

C:\Users\admin\Desktop>0.exe 10000000
C serial takes 0.007998 sec
C OpenMP takes 0.007000 sec
C OpenMP AVX2 takes 0.007000 sec

C:\Users\admin\Desktop>0.exe 100000000
C serial takes 0.083790 sec
C OpenMP takes 0.062064 sec
C OpenMP AVX2 takes 0.069361 sec



C:\Users\admin\Desktop>gcc -fopenmp -mavx2 0.c -o 0 -std=c99

C:\Users\admin\Desktop>0.exe 1000000
C serial takes 0.003001 sec
C OpenMP takes 0.000998 sec
C OpenMP AVX2 takes 0.001002 sec

C:\Users\admin\Desktop>0.exe 10000000
C serial takes 0.027000 sec
C OpenMP takes 0.007997 sec
C OpenMP AVX2 takes 0.009002 sec

C:\Users\admin\Desktop>0.exe 100000000
C serial takes 0.276776 sec
C OpenMP takes 0.068133 sec
C OpenMP AVX2 takes 0.084478 sec

-O3是編譯器3級優化參數,-On這個n越大優化級別越高,不加就是對代碼不進行編譯器優化
-fopenmp對應omp.h頭文件,加這些纔可以用OpenMP
-mavx2對應immintrin.h頭文件,加這些纔可以用AVX2指令

我們可以看到C語言的速度遠勝python,而加了O3優化的C串行程序在10的6、7、8次方數量級的向量相加情況下緊咬OpenMP和OPenMP AVX2加速的程序,這就可以看出編譯器在我們寫代碼生活中做出的巨大貢獻。
一旦將編譯優化參數去掉,後兩個版本的代碼優勢十分明顯。

AVX2指令優化類似彙編語言,上面的程序中我們使用AVX2C語言指令從數組a,b的內存中一次分別各取出8個數,相加存到另一個_m256i變量中,再存回到數組c當中,再使用OpenMP並行將爲8的倍數個計算好的結果合併,得到最終結果。

AVX2指令集一次可以處理32個二進制位的數據,也就是8個int型或者float型,或者4個double型數據。

我們可以發現,向量計算也好、矩陣計算也好,並行化之後計算之所以快,就是得益於這種分塊(block)的思想。但是上面結果OpenMP AVX2版的並不比OpenMP的快多少,這是由於存存取取的操作佔用了時間,AVX2無法直接處理C語言中的變量。這和GPU加速是一樣的。AVX2加速和GPU加速都要付出這樣的時間代價。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章