提问人:Stef1611 提问时间:9/7/2023 更新时间:9/7/2023 访问量:117
如何有效地链接 avx2 内部函数以执行算术运算链?
How to chain avx2 intrinsics efficiently to perform chain of arithmetic operations?
问:
我写了一个大型程序来模拟分子系统。我在处理器是 Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz 的台式计算机上运行它。大多数时间 (75%) 用于计算 4 个邻居的 Lennard Jones 电位。
为了优化它,我正在学习如何使用,我编写了一个简化版本,仅执行 LJ 计算:avx2 intrinsics
(d_eq/d_cur)**12 - 2.0*(d_eq/d_cur)**6
这是没有内部函数的代码:
#include <stdio.h>
#include <stdlib.h>
#include <immintrin.h>
double lj_pot(double* d_cur) {
const double d_eq = 1.0;
double ratio, vpot1, eie;
eie =0.0;
for (int i=0; i<4; i++) {
ratio = d_eq/d_cur[i] ;
vpot1 = ratio*ratio*ratio ;
vpot1 = vpot1*vpot1 ;
eie += vpot1*(vpot1-2.0);
}
return eie;
}
int main()
{
double d_cur[4];
double eee;
eee=0.0;
for (int i=0; i<10000000; i++) {
for (int j=0; j<4; j++) {
d_cur[j] = 0.5+(double)(rand()) / (double)(RAND_MAX); // between 0.5 , 1.5
}
eee += lj_pot(d_cur);
}
printf("%f\n",eee);
return 0;
}
这是 AVX2 内部函数的代码
#include <stdio.h>
#include <stdlib.h>
#include <immintrin.h>
static inline double hsum_double_avx(__m256d v) {
// From https://stackoverflow.com/a/49943540/7462275
__m128d vlow = _mm256_castpd256_pd128(v);
__m128d vhigh = _mm256_extractf128_pd(v, 1); // high 128
vlow = _mm_add_pd(vlow, vhigh); // reduce down to 128
__m128d high64 = _mm_unpackhi_pd(vlow, vlow);
return _mm_cvtsd_f64(_mm_add_sd(vlow, high64)); // reduce to scalar
}
double lj_pot(double* d_cur) {
const __m256d d_eq = _mm256_set1_pd(1.0);
const __m256d two_dbl = _mm256_set1_pd(2.0);
__m256d vec1, vec2;
vec1 = _mm256_loadu_pd(d_cur);
vec1 = _mm256_div_pd(d_eq,vec1);
vec2 = _mm256_mul_pd(vec1,vec1); // (d_eq/d_cur)**2
vec1 = _mm256_mul_pd(vec2,vec2); // (d_eq/d_cur)**4
vec1 = _mm256_mul_pd(vec1,vec2); // (d_eq/d_cur)**6
vec2 = _mm256_sub_pd(vec1,two_dbl); // (d_eq/d_cur)**6 - 2.0
vec1 = _mm256_mul_pd(vec1,vec2); // (d_eq/d_cur)**12 -2.0*(d_eq/d_cur)**6
return hsum_double_avx(vec1);
}
int main()
{
double d_cur[4];
double eee;
eee=0.0;
for (int i=0; i<10000000; i++) {
for (int j=0; j<4; j++) {
d_cur[j] = 0.5+(double)(rand()) / (double)(RAND_MAX); // between 0.5 , 1.5
}
eee += lj_pot(d_cur);
}
printf("%f\n",eee);
return 0;
}
此代码输出的结果与第一个代码相同。但它的速度较慢。
( 用于编译这两个程序。gcc -O3 -mavx -o lj_pot lj_pot.c
当我检查汇编代码时,我想(也许这不是很好的理由)我没有正确使用 AVX2 内部函数来执行算术运算链。我必须更改什么才能更快地拥有程序?
谢谢你的回答
答:
GCC 会自动将 SIMD 用于原始代码。你的手动优化只是抑制了编译器的优化。使用以下命令查看生成的程序集:-O3 -march=skylake -funsafe-math-optimizations
lj_pot(double*):
vbroadcastsd ymm1, QWORD PTR .LC1[rip]
vdivpd ymm1, ymm1, YMMWORD PTR [rdi]
vmulpd ymm0, ymm1, ymm1
vmulpd ymm0, ymm0, ymm1
vbroadcastsd ymm1, QWORD PTR .LC3[rip]
vmulpd ymm0, ymm0, ymm0
vaddpd ymm1, ymm0, ymm1
vmulpd ymm0, ymm1, ymm0
vextractf128 xmm1, ymm0, 0x1
vaddpd xmm1, xmm1, xmm0
vunpckhpd xmm0, xmm1, xmm1
vaddpd xmm0, xmm0, xmm1
vzeroupper
ret
如果你想优化这一点,首先要看看编译器在做什么。这很聪明。
评论
_mm256_fmadd_ps
mul
vpot1*(vpot1-2.0)
vpot1*vpot1
vpot1*vpot1 -2
eie +=...
重要的是要注意,我认为(至少我编译源代码的方式),独立副本是自动矢量化的,而不是内联副本,后者是实际运行的副本。独立副本不是从 调用的。因此,自动矢量化在这里并没有真正为您做任何事情,除非您获得的汇编代码与我编译您的程序时发生的情况有质的不同。lj_pot
main
关于矢量化代码,我注意到了两件不理想的事情:
- 随机数生成仍然使用标量完成,标量本身已经是一个值得注意的项目(尽管它本身不是回归),并且还意味着向量必须由标量组装。计算一些标量,然后将它们组合成一个向量通常是要避免的。将标量存储到内存中,然后执行向量加载(这是在代码中发生的情况)具有显着的延迟损失(这在这里可能不应该很重要,因为该延迟不在循环携带的依赖链中),并且使用shuffles/inserts/etc在不通过内存的情况下构造向量(这通常是由于滥用具有非常量参数的宏所致)会花费大量指令。
_mm(256)_set_***
- 水平加法在该循环中执行 10000000 次,而不是将向量相加并在循环后执行一次水平加法。
随着这两个项目的改进,因此在主循环之后生成矢量化随机数和水平求和,导致循环中基本上只有向量算术(好吧,并增加循环计数器),我希望代码应该变得比原来更快。
评论
(a large one). In this program, 75% of the time is used to calculate a LJ potential on 4 neigbourgs. I used
评论