gcc - _mm256_fmadd_ps 比 _mm256_mul_ps + _mm256_add_ps 慢?

标签 gcc sse simd avx micro-optimization

我有一个用 AVX 计算 a*b+c*d 的图像处理算法。伪代码如下:

float *a=new float[N];
float *b=new float[N];
float *c=new float[N];
float *d=new float[N];

//assign values to a, b, c and d
__m256 sum;
double start=cv::getTickCount();
for (int i = 0; i < n; i += 8) // assume that n is a multiple of 8
{
    __m256 am=_mm256_loadu_ps(a+i);
    __m256 bm=_mm256_loadu_ps(b+i);
    __m256 cm=_mm256_loadu_ps(c+i);
    __m256 dm=_mm256_loadu_ps(d+i);

    __m256 abm=_mm256_mul_ps(am, bm);
    __m256 cdm=_mm256_mul_ps(cm, dm);
    __m256 abcdm=_mm256_add_ps(abm, cdm);
    sum=_mm256_add_ps(sum, abcdm);
}
double time1=(cv::getTickCount()-start)/cv::getTickFrequency();

我把上面的_mm256_mul_ps和_mm256_add_ps改为_mm256_fmadd_ps如下:

float *a=new float[N];
float *b=new float[N];
float *c=new float[N];
float *d=new float[N];

//assign values to a, b, c and d
__m256 sum;
double start=cv::getTickCount();
for (int i = 0; i < n; i += 8) // assume that n is a multiple of 8
{
    __m256 am=_mm256_loadu_ps(a+i);
    __m256 bm=_mm256_loadu_ps(b+i);
    __m256 cm=_mm256_loadu_ps(c+i);
    __m256 dm=_mm256_loadu_ps(d+i);

    sum=_mm256_fmadd_ps(am, bm, sum);
    sum=_mm256_fmadd_ps(cm, dm, sum);
}
double time2=(cv::getTickCount()-start)/cv::getTickFrequency();

但是下面的代码比上面的慢!上面代码执行时间1为50ms,下面代码执行时间2为90ms。 _mm256_fmadd_ps 比 _mm256_mul_ps + _mm256_add_ps 慢 ???

我使用 Ubuntu 16.04,GCC 7.5.0,编译器标志:-fopenmp -march=native -O3

最佳答案

您的缩减循环既是延迟瓶颈,又不是吞吐量瓶颈,因为您只使用一个 FP 向量累加器。 FMA 速度较慢,因为您使关键路径更长(每个循环迭代有 2 条指令链,而不是只有 1 条)。

add的情况下,sum的循环携带的依赖链只有sum=_mm256_add_ps(sum, abcdm);。其他指令对于每次迭代都是独立的,并且可以在前一个 vaddps 准备好本次迭代的 sum 之前准备好 abcdm 输入。

fma 的情况下,循环携带的 dep 链经过两个 _mm256_fmadd_ps 操作,都进入 sum,所以是的,你d 预计它会慢两倍左右。

展开更多累加器以隐藏 FP 延迟(就像点积的正常情况一样)。参见 Why does mulss take only 3 cycles on Haswell, different from Agner's instruction tables? (Unrolling FP loops with multiple accumulators)有关更多详细信息以及 OoO exec 的工作原理。

另见 Improving performance of floating-point dot-product of an array with SIMD 2 个累加器的更简单的初学者友好示例。

(将那些单独的 __m256 sum0、sum1、sum2 等 变量相加应该在循环之后完成。您还可以使用 __m256 sum[4] 来节省输入。您甚至可以对该数组使用内部循环;大多数编译器将完全展开小的固定计数循环,因此您可以在单独的 YMM 寄存器中使用每个 __m256 获得所需的展开 asm。)

或者让 clang 自动向量化它;它通常会为您展开多个累加器。

或者,如果您出于某种原因不想展开,您可以使用 FMA,同时通过 sum += fma(a, b, c*d); 保持低循环延迟(一个 mul,一个 FMA,一个 add)。当然,如果您使用 -ffast-math 编译,假设您的编译器没有“收缩”您的 mul 并为您添加到 FMA 中;默认情况下,GCC 会积极地跨语句执行此操作,而 clang 不会。

一旦你这样做,你的吞吐量将在每个时钟 2 个负载上成为瓶颈(最好的情况是对齐阵列没有缓存行拆分,new 不会给你),所以除了减少前端瓶颈外,使用 FMA 几乎没有帮助。 (与需要每次加载运行 1 个 FP 运算才能跟上的多累加器 mul/add 版本相比;使用多个累加器将使您比任何一个原始循环都更快。就像每 2 个周期一次迭代(4 次加载),而不是 1每 3 个周期,有 vaddps 延迟瓶颈)。


在 Skylake 和更高版本上,FMA/add/mul 都具有相同的延迟:4 个周期。在 Haswell/Broadwell 上,vaddps 延迟为 3 个周期(一个专用的 FP 添加单元),而 FMA 延迟为 5 个。

Zen2 有 3 个周期的 vaddps,5 个周期的 vfma....ps ( https://uops.info/ )。 (两者的 2/clock 吞吐量,以及在不同的执行端口上,因此理论上您可以在 Zen2 上每个时钟运行 2 个 FMA 2 个 vaddps。)

由于您的较长延迟 FMA 循环的速度不到原来的两倍,我猜您可能使用的是 Skylake 派生的 CPU。也许 mul/add 版本在前端或资源冲突或其他方面存在一些瓶颈,并且没有完全达到预期的每 3 个时钟 1 次迭代延迟限制速度。

一般情况下,参见 https://uops.info/用于延迟和 uops/端口故障。 (还有 https://agner.org/optimize/ )。

关于gcc - _mm256_fmadd_ps 比 _mm256_mul_ps + _mm256_add_ps 慢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66260651/

相关文章:

linux - 在 fedora 上运行 gcc 获取 AVR

vectorization - Xeon Phi 上的 loaddup_pd/unpacklo_pd

c++ - 高效的 SSE NxN 矩阵乘法

c++ - 同时使用多个 SIMD 指令集的好处

c++ - Gold 所描述的 C++ "Key Function"是什么?

gcc,不同架构上 long int 的宽度

x86 - 使用 SSE 加载非连续 float

c++ - SSE微优化指令顺序

sse - 检查比较结果的多个向量中的每个向量中至少有1个元素为真-水平或然后与

c - 内联汇编中的 "matching constraint"是什么意思?