第三次作业
提交内容一:
源代码在-O3编译优化下执行结果:
AVX指令集优化:
// conv_avx.cpp
bool Convolve1D_Ks5_F64_AVX(double* __restrict__ y, const double* __restrict__ x, const double* __restrict__ kernel, int64_t num_pts) {
constexpr int64_t kernel_size = 5;
constexpr int64_t ks2 = kernel_size / 2;
if (num_pts < kernel_size) {
return false;
}
// 初始化卷积核
__m256d k0 = _mm256_set1_pd(kernel[0]);
__m256d k1 = _mm256_set1_pd(kernel[1]);
__m256d k2 = _mm256_set1_pd(kernel[2]);
__m256d k3 = _mm256_set1_pd(kernel[3]);
__m256d k4 = _mm256_set1_pd(kernel[4]);
// AVX指令集下卷积操作
for (int64_t i = ks2; i <= num_pts - kernel_size; i += 4) {
// std::cout << i << '\n';
__m256d x0 = _mm256_loadu_pd(&x[i + 2]);
__m256d x1 = _mm256_loadu_pd(&x[i + 1]);
__m256d x2 = _mm256_loadu_pd(&x[i]);
__m256d x3 = _mm256_loadu_pd(&x[i - 1]);
__m256d x4 = _mm256_loadu_pd(&x[i - 2]);
// 对每个卷积操作进行乘加
__m256d y_val = _mm256_add_pd(
_mm256_add_pd(
_mm256_mul_pd(x0, k0),
_mm256_mul_pd(x1, k1)),
_mm256_add_pd(
_mm256_mul_pd(x2, k2),
_mm256_add_pd(
_mm256_mul_pd(x3, k3),
_mm256_mul_pd(x4, k4))));
// 回存
_mm256_storeu_pd(&y[i], y_val);
}
return true;
}
执行结果:
在这里因为使用double类型,使用_mm256_load_pd和_mm256_store_pd会出现内存地址不对齐,发生段错误;所以使用_mm256_loadu_pd和_mm256_storeu_pd进行存取。另外在conv程序中可以对每次卷积操作进行fma优化。
AVX2相对AVX引入了向量整数运算的支持,引入了gather指令,允许从非连续内存位置加载数据到一个寄存器中,增加了对向量位操作的支持,且对fma操作进行了加强。
fma优化:
# conv_avx_fma.cpp
bool Convolve1D_Ks5_F64_AVX(double* __restrict__ y, const double* __restrict__ x, const double* __restrict__ kernel, int64_t num_pts) {
constexpr int64_t kernel_size = 5;
constexpr int64_t ks2 = kernel_size / 2;
if (num_pts < kernel_size) {
return false;
}
// 初始化卷积核
__m256d k0 = _mm256_set1_pd(kernel[0]);
__m256d k1 = _mm256_set1_pd(kernel[1]);
__m256d k2 = _mm256_set1_pd(kernel[2]);
__m256d k3 = _mm256_set1_pd(kernel[3]);
__m256d k4 = _mm256_set1_pd(kernel[4]);
// AVX指令集下卷积操作
for (int64_t i = ks2; i <= num_pts - kernel_size; i += 4) {
// std::cout << i << '\n';
__m256d x0 = _mm256_loadu_pd(&x[i + 2]);
__m256d x1 = _mm256_loadu_pd(&x[i + 1]);
__m256d x2 = _mm256_loadu_pd(&x[i]);
__m256d x3 = _mm256_loadu_pd(&x[i - 1]);
__m256d x4 = _mm256_loadu_pd(&x[i - 2]);
// 对每个卷积操作进行乘加fma乘加
__m256d y_val = _mm256_setzero_pd();
y_val = _mm256_fmadd_pd(x0,k0,y_val);
y_val = _mm256_fmadd_pd(x1,k1,y_val);
y_val = _mm256_fmadd_pd(x2,k2,y_val);
y_val = _mm256_fmadd_pd(x3,k3,y_val);
y_val = _mm256_fmadd_pd(x4,k4,y_val);
// 回存
_mm256_storeu_pd(&y[i], y_val);
}
return true;
}
执行结果:
AVX512优化:
bool Convolve1D_Ks5_F64_AVX512(double* __restrict__ y, const double* __restrict__ x, const double* __restrict__ kernel, int64_t num_pts) {
constexpr int64_t kernel_size = 5;
constexpr int64_t ks2 = kernel_size / 2;
if (num_pts < kernel_size) {
return false;
}
// 初始化卷积核
__m512d k0 = _mm512_set1_pd(kernel[0]);
__m512d k1 = _mm512_set1_pd(kernel[1]);
__m512d k2 = _mm512_set1_pd(kernel[2]);
__m512d k3 = _mm512_set1_pd(kernel[3]);
__m512d k4 = _mm512_set1_pd(kernel[4]);
// AVX指令集下卷积操作
for (int64_t i = ks2; i <= num_pts - kernel_size; i += 8) { // Note: Change stride to 8 for AVX512
__m512d x0 = _mm512_loadu_pd(&x[i + 2]);
__m512d x1 = _mm512_loadu_pd(&x[i + 1]);
__m512d x2 = _mm512_loadu_pd(&x[i]);
__m512d x3 = _mm512_loadu_pd(&x[i - 1]);
__m512d x4 = _mm512_loadu_pd(&x[i - 2]);
// 对每个卷积操作进行乘加fma乘加
__m512d y_val = _mm512_setzero_pd();
y_val = _mm512_fmadd_pd(x0, k0, y_val);
y_val = _mm512_fmadd_pd(x1, k1, y_val);
y_val = _mm512_fmadd_pd(x2, k2, y_val);
y_val = _mm512_fmadd_pd(x3, k3, y_val);
y_val = _mm512_fmadd_pd(x4, k4, y_val);
_mm512_storeu_pd(&y[i], y_val);
}
return true;
}
执行结果:
源代码 | avx2 | avx2_fma | avx512 |
---|---|---|---|
85643us | 45320us | 44870us | 46879us |
Summary:
通过对一维卷积进行avx优化,可以观察出一些现象:源代码通过手动循环展开已经得到了不错的优化,但通过avx指令集对源代码进行初次优化加速比可以达到1.89接近两倍的加速比(另外可以通过对卷积操作进行并行乘小幅度提高效率,空间换时间);而通过对卷积操作进行fma优化可以进一步小幅度提升执行效率;但进一步使用avx512时执行速度却反而有了退步,通过查阅一些资料和一些重复猜想是cpu执行avx512指令时导致的主频的降频。
https://zhuanlan.zhihu.com/p/430223278?utm_id=0
想要知道答案,我们必须从CPU自身设计入手。通过查阅Intel的wiki,我们可以大致得到结论,就是Intel CPU依赖复杂的机制基于可用headroom进行dynamic frequency scaling。dynamic frequency scaling,顾名思义,其实就是在运行时,根据不同的负载,CPU会主动升频、降频。
而在CPU进行dynamic frequency scaling中不得不提的一点是,在CPU发展过程中,虽然晶体管的数量仍然能勉强维持摩尔定律[4],并且单核越来越复杂,但是芯片的能耗限制却始终没有解除。所以,当进行某些更加复杂、耗能的计算时(例如AVX-512中的FMA计算),CPU必须要保证自身热能与电能的能耗不超过限制,这也是dynamic frequency scaling的作用,就是通过自身主动升频、降频,能够在保证能耗限制的同时,尽可能对程序进行优化。
提交内容二:
分析:
gather函数是avx2引入的用于从非连续内存位置加载数据到一个寄存器中的操作;gather.cpp中通过index[i]*4到对应字节取值。
__m256d _mm256_i64gather_pd(double const* base_addr, __m256i vindex, const int scale);
permute函数是avx2引入的用于对向量进行排列(Permute)的操作;permute.cpp通过控制位imm8进行对aa的置换操作。
参考资料:
AVX512降频:https://zhuanlan.zhihu.com/p/430223278?utm_id=0
AVX操作:https://blog.csdn.net/qq_17075011/article/details/130555559
AVX2 基本命令:https://blog.csdn.net/weixin_44885334/article/details/129157542