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