C++ 我想使用AVX来提高这段代码的性能
我分析了我的代码,代码中最昂贵的部分是文章中包含的循环。我想用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
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?)简化了奇怪的索引,或者手动剥离第一个外部循环-