C++ 我想使用AVX来提高这段代码的性能

C++ 我想使用AVX来提高这段代码的性能,c++,optimization,compiler-optimization,avx,avx2,C++,Optimization,Compiler Optimization,Avx,Avx2,我分析了我的代码,代码中最昂贵的部分是文章中包含的循环。我想用AVX提高这个循环的性能。我尝试过手动展开循环,虽然这确实提高了性能,但改进并不令人满意 int N = 100000000; int8_t* data = new int8_t[N]; for(int i = 0; i< N; i++) { data[i] = 1 ;} std::array<float, 10> f = {1,2,3,4,5,6,7,8,9,10}; std::vector<float&g

我分析了我的代码,代码中最昂贵的部分是文章中包含的循环。我想用AVX提高这个循环的性能。我尝试过手动展开循环,虽然这确实提高了性能,但改进并不令人满意

int N = 100000000;
int8_t* data = new int8_t[N];
for(int i = 0; i< N; i++) { data[i] = 1 ;}
std::array<float, 10> f  = {1,2,3,4,5,6,7,8,9,10};
std::vector<float> output(N, 0);
int k = 0;
for (int i = k; i < N; i = i + 2) {
    for (int j = 0; j < 10; j++, k = j + 1) {
        output[i] += f[j] * data[i - k];
        output[i + 1] += f[j] * data[i - k + 1];
    }
}
int N=100000000;
int8_t*数据=新的int8_t[N];
对于(inti=0;i

我可以就如何处理这个问题提供一些指导。

我假设
数据
是一个有符号字节的大输入数组,
f
是一个长度为10的小浮点数组,
输出
是一个大浮点输出数组。您的代码超出了
i
前10次迭代的范围,因此我将从10开始
i
。以下是原始代码的干净版本:

int s = 10;
for (int i = s; i < N; i += 2) {
    for (int j = 0; j < 10; j++) {
        output[i]   += f[j] * data[i-j-1];
        output[i+1] += f[j] * data[i-j];
    }
}
这个版本的代码(以及输入/输出数据的声明)应该出现在问题本身中,而不需要其他人清理/简化混乱


现在很明显,这段代码适用,这是信号处理中非常常见的事情。例如,可以使用函数在Python中计算它。内核的长度非常小,因此与bruteforce方法相比不会提供任何好处。鉴于这个问题是众所周知的,您可以阅读很多关于向量化小内核卷积的文章。我会跟着的

首先,让我们摆脱反向索引。显然,我们可以在运行主算法之前反转内核。然后,我们必须计算所谓的卷积,而不是卷积。简单地说,我们沿着输入数组移动内核数组,并为每个可能的偏移量计算它们之间的点积

std::reverse(f.data(), f.data() + 10);
for (int i = s; i < N; i++) {
    int b = i-10;
    float res = 0.0;
    for (int j = 0; j < 10; j++)
        res += f[j] * data[b+j];
    output[i] = res;
}
std::reverse(f.data(),f.data()+10);
对于(int i=s;i
为了矢量化它,让我们一次计算8个连续的点积。回想一下,我们可以将八个32位浮点数打包到一个256位AVX寄存器中。我们将通过i对外环进行矢量化,这意味着:

  • 我的循环将在每次迭代中前进8
  • 外部循环中的每个值都会变成一个8元素的包,这样包的第k个元素会在从标量版本开始的外部循环的(i+k)次迭代中保存该值
以下是生成的代码:

//reverse the kernel
__m256 revKernel[10];
for (size_t i = 0; i < 10; i++)
    revKernel[i] = _mm256_set1_ps(f[9-i]); //every component will have same value
//note: you have to compute the last 16 values separately!
for (size_t i = s; i + 16 <= N; i += 8) {
    int b = i-10;
    __m256 res = _mm256_setzero_ps();
    for (size_t j = 0; j < 10; j++) {
        //load: data[b+j], data[b+j+1], data[b+j+2], ..., data[b+j+15]
        __m128i bytes = _mm_loadu_si128((__m128i*)&data[b+j]);
        //convert first 8 bytes of loaded 16-byte pack into 8 floats
        __m256 floats = _mm256_cvtepi32_ps(_mm256_cvtepi8_epi32(bytes));
        //compute res = res + floats * revKernel[j] elementwise
        res = _mm256_fmadd_ps(revKernel[j], floats, res);
    }
    //store 8 values packed in res into: output[i], output[i+1], ..., output[i+7]
    _mm256_storeu_ps(&output[i], res);
}
//反转内核
__m256 revKernel[10];
对于(大小i=0;i<10;i++)
revKernel[i]=_mm256_set1_ps(f[9-i])//每个组件都有相同的值
//注意:您必须分别计算最后16个值!

对于(size_t i=s;i+16我假设
数据
是一个有符号字节的大输入数组,
f
是一个长度为10的小浮点数数组,
output
是浮点数的大输出数组。您的代码在
i
的前10次迭代中超出了界限,因此我将从10开始
i
。H以下是原始代码的干净版本:

int s = 10;
for (int i = s; i < N; i += 2) {
    for (int j = 0; j < 10; j++) {
        output[i]   += f[j] * data[i-j-1];
        output[i+1] += f[j] * data[i-j];
    }
}
这个版本的代码(以及输入/输出数据的声明)应该出现在问题本身中,而不需要其他人清理/简化混乱


现在很明显,这段代码适用,这是信号处理中非常常见的事情。例如,它可以在Python中使用函数进行计算。内核的长度非常小,因此与bruteforce方法相比不会提供任何好处。鉴于这个问题是众所周知的,您可以阅读很多关于将small-k矢量化的文章欧内尔,卷积,我跟着

首先,让我们摆脱反向索引。显然,我们可以在运行主算法之前反转内核。之后,我们必须计算所谓的卷积,而不是卷积。简单地说,我们沿着输入数组移动内核数组,并计算它们之间的点积,以获得每个可能的偏移量

std::reverse(f.data(), f.data() + 10);
for (int i = s; i < N; i++) {
    int b = i-10;
    float res = 0.0;
    for (int j = 0; j < 10; j++)
        res += f[j] * data[b+j];
    output[i] = res;
}
std::reverse(f.data(),f.data()+10);
对于(int i=s;i
为了对其进行矢量化,让我们一次计算8个连续的点积。回想一下,我们可以将8个32位浮点数打包到一个256位AVX寄存器中。我们将通过i对外部循环进行矢量化,这意味着:

  • 我的循环将在每次迭代中前进8
  • 外部循环中的每个值都会变成一个8元素的包,这样包的第k个元素会在从标量版本开始的外部循环的(i+k)次迭代中保存该值
以下是生成的代码:

//reverse the kernel
__m256 revKernel[10];
for (size_t i = 0; i < 10; i++)
    revKernel[i] = _mm256_set1_ps(f[9-i]); //every component will have same value
//note: you have to compute the last 16 values separately!
for (size_t i = s; i + 16 <= N; i += 8) {
    int b = i-10;
    __m256 res = _mm256_setzero_ps();
    for (size_t j = 0; j < 10; j++) {
        //load: data[b+j], data[b+j+1], data[b+j+2], ..., data[b+j+15]
        __m128i bytes = _mm_loadu_si128((__m128i*)&data[b+j]);
        //convert first 8 bytes of loaded 16-byte pack into 8 floats
        __m256 floats = _mm256_cvtepi32_ps(_mm256_cvtepi8_epi32(bytes));
        //compute res = res + floats * revKernel[j] elementwise
        res = _mm256_fmadd_ps(revKernel[j], floats, res);
    }
    //store 8 values packed in res into: output[i], output[i+1], ..., output[i+7]
    _mm256_storeu_ps(&output[i], res);
}
//反转内核
__m256 revKernel[10];
对于(大小i=0;i<10;i++)
revKernel[i]=\u mm256\u set1\u ps(f[9-i]);//每个组件将具有相同的值
//注意:您必须分别计算最后16个值!

对于(size_t i=s;i+16 Post a,以及您的基准测试。理想情况下,还要发布准确的编译器调用(您是否在编译期间启用了AVX优化?)以及生成的程序集,并澄清您期望加速的原因和数量。我正在添加-mavx2和-Ofast作为我的标志。您的代码在运行时应该崩溃。当I=0,j=9,k将为10时,
数据[I-k]
将在索引-10处读取,从堆中获取随机垃圾。为什么
int k=0;
在外循环之外声明(而不是内部),并用于初始化
i
,而实际上它只设置在最内部的循环中。(以一种奇怪的方式,在第一个循环之后的每个内部循环的第一次迭代中k=11?)简化了奇怪的索引,或者手动剥离第一个外部循环-