我正在使用AVX一次计算八个点积。在我的当前代码中,我做的类似于以下操作(未展开之前):
Ivy-Bridge/Sandy-Bridge
__m256 areg0 = _mm256_set1_ps(a[m]);
for(int i=0; i<n; i++) {
__m256 breg0 = _mm256_load_ps(&b[8*i]);
tmp0 = _mm256_add_ps(_mm256_mul_ps(arge0,breg0), tmp0);
}
哈斯韦尔
__m256 areg0 = _mm256_set1_ps(a[m]);
for(int i=0; i<n; i++) {
__m256 breg0 = _mm256_load_ps(&b[8*i]);
tmp0 = _mm256_fmadd_ps(arge0, breg0, tmp0);
}
每种情况下需要展开循环多少次才能保证最大吞吐量?
对于使用FMA3的Haswell,我认为答案在这里 FLOPS per cycle for sandy-bridge and haswell SSE2/AVX/AVX2。 我需要展开循环10次。
对于Ivy Bridge,我认为是8。 这是我的逻辑。 AVX加法的延迟为3,乘法的延迟为5。 Ivy Bridge可以同时使用不同的端口进行一个AVX乘法和一个AVX加法。 使用符号m表示乘法,a表示加法,x表示未操作以及数字来表示局部和(例如,m5表示与第5个局部和相乘),我可以写成:
port0: m1 m2 m3 m4 m5 m6 m7 m8 m1 m2 m3 m4 m5 ...
port1: x x x x x a1 a2 a3 a4 a5 a6 a7 a8 ...
通过在九个时钟周期后使用8个部分和(四个来自加载和五个来自乘法),我可以每个时钟周期提交一个AVX加载、一个AVX加法和一个AVX乘法。
我想这意味着在Ivy Bridge和Haswell的32位模式中无法实现此任务的最大吞吐量,因为32位模式只有八个AVX寄存器?
编辑:关于悬赏问题。 我主要的问题仍然存在。 我想获得上面提到的Ivy Bridge或Haswell函数的最大吞吐量,n可以是大于或等于64的任何值。 我认为这只能通过展开(Ivy Bridge展开8次,Haswell展开10次)来完成。 如果您认为可以用另一种方法来解决,请让我们看看。 在某种程度上,这是如何实现每个周期的理论最大4 FLOP的变体?。 但是,我不仅要求乘法和加法,还要求每个时钟周期具有一个256位加载(或两个128位加载)、一个AVX乘法和一个AVX加法,或者使用Haswell每个时钟周期具有两个256位加载和两个FMA3指令。
我还想知道需要多少寄存器。 对于Ivy Bridge,我认为是10个。 一个用于广播,一个用于加载(由于寄存器重命名只有一个),八个用于八个部分和。 因此,我认为这不能在32位模式下完成(实际上,当我在32位模式下运行时,性能会显著下降)。
我应该指出,编译器可能会给出误导性的结果高度优化的矩阵乘法代码的MSVC和GCC之间性能差异
我目前在Ivy Bridge上使用的函数如下。 这基本上将一个64x64矩阵a的一行与所有64x64矩阵b相乘(我在a的每一行上运行此函数64次,以获取矩阵c中的完整矩阵乘积)。
#include <immintrin.h>
extern "C" void row_m64x64(const float *a, const float *b, float *c) {
const int vec_size = 8;
const int n = 64;
__m256 tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7;
tmp0 = _mm256_loadu_ps(&c[0*vec_size]);
tmp1 = _mm256_loadu_ps(&c[1*vec_size]);
tmp2 = _mm256_loadu_ps(&c[2*vec_size]);
tmp3 = _mm256_loadu_ps(&c[3*vec_size]);
tmp4 = _mm256_loadu_ps(&c[4*vec_size]);
tmp5 = _mm256_loadu_ps(&c[5*vec_size]);
tmp6 = _mm256_loadu_ps(&c[6*vec_size]);
tmp7 = _mm256_loadu_ps(&c[7*vec_size]);
for(int i=0; i<n; i++) {
__m256 areg0 = _mm256_set1_ps(a[i]);
__m256 breg0 = _mm256_loadu_ps(&b[vec_size*(8*i + 0)]);
tmp0 = _mm256_add_ps(_mm256_mul_ps(areg0,breg0), tmp0);
__m256 breg1 = _mm256_loadu_ps(&b[vec_size*(8*i + 1)]);
tmp1 = _mm256_add_ps(_mm256_mul_ps(areg0,breg1), tmp1);
__m256 breg2 = _mm256_loadu_ps(&b[vec_size*(8*i + 2)]);
tmp2 = _mm256_add_ps(_mm256_mul_ps(areg0,breg2), tmp2);
__m256 breg3 = _mm256_loadu_ps(&b[vec_size*(8*i + 3)]);
tmp3 = _mm256_add_ps(_mm256_mul_ps(areg0,breg3), tmp3);
__m256 breg4 = _mm256_loadu_ps(&b[vec_size*(8*i + 4)]);
tmp4 = _mm256_add_ps(_mm256_mul_ps(areg0,breg4), tmp4);
__m256 breg5 = _mm256_loadu_ps(&b[vec_size*(8*i + 5)]);
tmp5 = _mm256_add_ps(_mm256_mul_ps(areg0,breg5), tmp5);
__m256 breg6 = _mm256_loadu_ps(&b[vec_size*(8*i + 6)]);
tmp6 = _mm256_add_ps(_mm256_mul_ps(areg0,breg6), tmp6);
__m256 breg7 = _mm256_loadu_ps(&b[vec_size*(8*i + 7)]);
tmp7 = _mm256_add_ps(_mm256_mul_ps(areg0,breg7), tmp7);
}
_mm256_storeu_ps(&c[0*vec_size], tmp0);
_mm256_storeu_ps(&c[1*vec_size], tmp1);
_mm256_storeu_ps(&c[2*vec_size], tmp2);
_mm256_storeu_ps(&c[3*vec_size], tmp3);
_mm256_storeu_ps(&c[4*vec_size], tmp4);
_mm256_storeu_ps(&c[5*vec_size], tmp5);
_mm256_storeu_ps(&c[6*vec_size], tmp6);
_mm256_storeu_ps(&c[7*vec_size], tmp7);
}
tmp0
依赖。 - Leeor