一两句话更好的性能。自动矢量化的优点是简单。你需要设置一些关键字而不是发生魔法,编译器会为你制作快速代码。如果你想这样试试这个manual。
这种方法的缺点是没有简单的方法来理解编译器如何工作。在矢量化报告中,您会看到“LOOP WAS VECTORIZED”或“LOOP WAS NOT VECTORIZED”。但是如果你想真正理解你的代码是如何工作的,唯一的方法就是看你的程序集。这不是组装的问题。你需要用-fcode-asm
来编译程序。但我认为如果你需要阅读程序集来检查“简单的自动插件”方法是如何工作的,那么它并不那么简单。
替代自动矢量化的是内在函数(实际上,这不是单一的替代方法)。考虑像使用C函数包装的组件一样的内部函数。许多内部函数内部包装单个装配命令。
我推荐使用这个intrinsics guide。
所以我简单的方法步骤:
- 让单线程参考实现。您将使用它来检查内部版本的正确性。
- 实施SSE内在版本。 SSE内部函数非常简单,可以在Xeon上进行测试。
- 实现Xeon Phi的AVX-512版本。
- 衡量你的速度。
让我们来做你的程序。 与您的程序有很多不同之处:
- 我使用float而不是double。
- 我使用_mm_malloc代替posix_memalign。
- 我想n被16除以余数(AVX-512向量寄存器中的16个浮点数)。在这个例子中,我不使用循环剥皮。
- 我使用本地模式,而不是卸载模式。 KNL是可引导的,因此不再需要使用卸载模式。
- 另外我认为你的程序是不正确的,因为它在一段时间内从多个线程修改c数组。但让我们认为这不重要,我们只需要一些计算工作。
我的代码工作时间:
的Intel Xeon 5680
- 参考计算时间:97.677505秒
- 内在函数计算时间:6。189296秒
的Intel Xeon披(KNC)SE10X
- 参考计算时间:199.0秒
- 内在函数计算时间:2.78秒
代码:
#include <stdio.h>
#include <omp.h>
#include <math.h>
#include "immintrin.h"
#include <assert.h>
#define F_E_Q(X,Y,N) (round((X) * pow(10, N)-(Y) * pow(10, N)) == 0)
void reference(float* a, float* b, float* c, int n, int nPadded);
void intrinsics(float* a, float* b, float* c, int n, int nPadded);
char *test(){
int n=4800;
int nPadded = n;
assert(n%16 == 0);
float* a = (float*) _mm_malloc(sizeof(float)*n*nPadded, 64);
float* b = (float*) _mm_malloc(sizeof(float)*n*nPadded, 64);
float* cRef = (float*) _mm_malloc(sizeof(float)*n*nPadded, 64);
float* c = (float*) _mm_malloc(sizeof(float)*n*nPadded, 64);
assert(a != NULL);
assert(b != NULL);
assert(cRef != NULL);
assert(c != NULL);
for(int i=0, max = n*nPadded; i<max; i++){
a[i] = (int) rand()/1804289408.0;
b[i] = (int) rand()/1804289408.0;
cRef[i] = 0.0;
c[i] = 0.0;
}
debug_arr("a", "%f", a, 0, 9, 1);
debug_arr("b", "%f", b, 0, 9, 1);
debug_arr("cRef", "%f", cRef, 0, 9, 1);
debug_arr("c", "%f", c, 0, 9, 1);
double t1 = omp_get_wtime();
reference(a, b, cRef, n, nPadded);
double t2 = omp_get_wtime();
debug("reference calc time: %f", t2-t1);
t1 = omp_get_wtime();
intrinsics(a, b, c, n, nPadded);
t2 = omp_get_wtime();
debug("Intrinsics calc time: %f", t2-t1);
debug_arr("cRef", "%f", cRef, 0, 9, 1);
debug_arr("c", "%f", c, 0, 9, 1);
for(int i=0, max = n*nPadded; i<max; i++){
assert(F_E_Q(cRef[i], c[i], 2));
}
_mm_free(a);
_mm_free(b);
_mm_free(cRef);
_mm_free(c);
return NULL;
}
void reference(float* a, float* b, float* c, int n, int nPadded){
for(int i = 0; i < n; i++)
for(int k = 0; k < n; k++)
for(int j = 0; j < n; j++)
c[i*nPadded+j] = c[i*nPadded+j] + a[i*nPadded+k]*b[k*nPadded+j];
}
#if __MIC__
void intrinsics(float* a, float* b, float* c, int n, int nPadded){
#pragma omp parallel for
for(int i = 0; i < n; i++)
for(int k = 0; k < n; k++)
for(int j = 0; j < n; j+=16){
__m512 aPart = _mm512_extload_ps(a + i*nPadded+k, _MM_UPCONV_PS_NONE, _MM_BROADCAST_1X16, _MM_HINT_NONE);
__m512 bPart = _mm512_load_ps(b + k*nPadded+j);
__m512 cPart = _mm512_load_ps(c + i*nPadded+j);
cPart = _mm512_add_ps(cPart, _mm512_mul_ps(aPart, bPart));
_mm512_store_ps(c + i*nPadded+j, cPart);
}
}
#else
void intrinsics(float* a, float* b, float* c, int n, int nPadded){
#pragma omp parallel for
for(int i = 0; i < n; i++)
for(int k = 0; k < n; k++)
for(int j = 0; j < n; j+=4){
__m128 aPart = _mm_load_ps1(a + i*nPadded+k);
__m128 bPart = _mm_load_ps(b + k*nPadded+j);
__m128 cPart = _mm_load_ps(c + i*nPadded+j);
cPart = _mm_add_ps(cPart, _mm_mul_ps(aPart, bPart));
_mm_store_ps(c + i*nPadded+j, cPart);
}
}
#endif
尝试产生更多的线程。例如,编写'#pragma omp parallel for num_threads(200)'。 –
'n = 100'是一个* tiny *数组,难怪你没有从并行化中获得更多的加速。如果'卸载'意味着你在一个普通的Haswell或Skylake CPU上启动程序,并且它必须与Xeon-Phi内核进行通信,那么对于一个微型阵列来说这完全是不值得的。 –