_mm256_fmadd_ps 比 _mm256_mul_ps + _mm256_add_ps 慢?

2024-05-02

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

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 条指令)。

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

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

使用更多累加器展开以隐藏 FP 延迟(就像点积的正常情况一样). See 为什么mulss在Haswell上只需要3个周期,与Agner的指令表不同? (使用多个累加器展开 FP 循环) https://stackoverflow.com/questions/45113527/why-does-mulss-take-only-3-cycles-on-haswell-different-from-agners-instruction有关此内容以及 OoO exec 工作原理的更多详细信息。

另请参阅使用 SIMD 提高数组浮点点积的性能 https://stackoverflow.com/questions/65818232/improving-performance-of-floating-point-dot-product-of-an-array-with-simd/65827668#65827668这是一个更简单、适合初学者的 2 个累加器示例。

(将这些单独的__m256 sum0, sum1, sum2, etcvars 应该在循环之后完成。您还可以使用__m256 sum[4]以节省打字。您甚至可以对该数组使用内部循环;大多数编译器将完全展开小型固定计数循环,因此您可以在每个循环中获得所需的展开汇编__m256在单独的 YMM 寄存器中。)

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

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

一旦你这样做了,你的吞吐量将在每个时钟 2 个负载上成为瓶颈(最好的情况是即使使用对齐阵列,也没有缓存行分割,这new won't给你),所以使用 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/ https://uops.info/)。 (两者的 2/时钟吞吐量,并且在不同的执行端口上,因此理论上您可以运行 2 个 FMAandZen2 上每个时钟 2 个 vaddp。)

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

一般来说,请参阅https://uops.info/ https://uops.info/用于延迟和微指令/端口故障。 (还https://agner.org/optimize/ https://agner.org/optimize/).

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

_mm256_fmadd_ps 比 _mm256_mul_ps + _mm256_add_ps 慢? 的相关文章

随机推荐