jibinghu

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

第三次作业


alt text

提交内容一:

源代码在-O3编译优化下执行结果:
alt text

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;
}

执行结果:
alt text

在这里因为使用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;
}

执行结果:
alt text

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;
}

执行结果:
alt text

源代码 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的作用,就是通过自身主动升频、降频,能够在保证能耗限制的同时,尽可能对程序进行优化。

alt text
alt text

提交内容二:

alt text

分析:
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

posted on 2024-04-09 16:53  zombie_black  阅读(40)  评论(0编辑  收藏  举报