【问题标题】:Faster AVX/2 matrix-vector multiply?更快的 AVX/2 矩阵向量乘法?
【发布时间】:2021-12-15 14:05:49
【问题描述】:

我正在研究神经网络的矩阵向量乘法+累加函数,我最终决定手动对整个事物进行向量化,而不是依赖自动向量化。

我想出了这个功能:

#include <immintrin.h>
#define re *restrict //just simplification
// computes a2[n2]=w[n2][n1]*a1[n1]+b[n2]
void l_frw(const int n2,const int n1,float re a2,const float re a1,const float w[restrict][n1],const float re b)
{
    __m256 x,y,z;
    __m256 one=_mm256_set1_ps(1.0f);
    for(int i=0; i<n2; i++)
    {
        a2[i]=b[i];
        z=_mm256_setzero_ps();
        for(int j=0; j<n1; j+=8)
        {
            x=_mm256_loadu_ps(&a1[j]);
            y=_mm256_loadu_ps(&w[i][j]);
            z=_mm256_fmadd_ps(x,y,z); //accumulates dot product of each row into z
        }
        z=_mm256_dp_ps(z,one,0b11111111);
        a2[i]+=z[0]+z[4];
    }
}

(是的,它只适用于 8 个大小的向量的倍数)。

它比简单的自动矢量化版本快约 20%,这非常简洁,但我仍在寻找改进。 有关如何加快速度的任何建议?

【问题讨论】:

  • 您的 memcpy 调用有问题 - 它需要以字节为单位的大小。好像也是多余的?
  • 我认为@chtz 只是意味着通过流水线和乱序执行进行并行化。如果您连续编写多个不依赖于彼此结果的指令,机器可以同时执行它们。因此,展开i 上的循环并从各行交错fmadd 操作。
  • 通过使用 stackoverflow.com/a/13222410/15671081 而不是 dp_ps 获得了额外 2% 的加速。
  • 请参阅Why does mulss take only 3 cycles on Haswell, different from Agner's instruction tables? (Unrolling FP loops with multiple accumulators) re:@NateEldredge 的建议,以及我的答案顶部的一些实际示例的链接,以及向量点积的延迟与吞吐量瓶颈讨论在那个答案中。使用多个累加器至关重要:您要尽量减少的不是循环开销,而是通过一个累加器的 FMA 操作链的延迟。
  • 我不希望展开会减慢迭代速度,除非只是做错了......主要的一点是只加载一次x;第二点是洗牌矩阵w,这样你就可以线性地阅读它。第三点是检查n1是否足够小,可以提前加载整个向量x

标签: c matrix-multiplication simd avx


【解决方案1】:

我最近发现 _mm256_set1_ps/pd/si... 是一个非常低效的内在/指令(根据变量类型,并不总是评估为 1 条指令)

见:https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#text=_mm256_set1_ps&ig_expand=6147

所以你可以使用:

 const __m256 _one = {1.0f, 1.0f, 1.0f, 1.0f}; //(I think you need C11 or newer)

或者如果是 c++:

 constexpr inline __m256 _one = {1.0f, 1.0f, 1.0f, 1.0f};

这将适用于 __m256 和 __m256d,但是我相信当与 __m256i 一起使用时这是未定义的行为,因为整数向量类型可以根据 int 的宽度具有可变大小。

ie: __m256i of chars 可以容纳 32 个字符,但只能容纳 4 个 unsigned long longs。

这还有一个好处是实际上不调用任何 AVX 指令,它只是将 4 个浮点数的数组初始化为 1.0f,以供以后引用和使用,因此这种初始化方式并不明确要求 AVX 的硬件支持。因此,您可以将一些常量向量存储在跨平台的 .dll 文件中,而无需担心在实际调用 AVX 指令之前检查 AVX 支持。

【讨论】:

  • 需要引用。哪个编译器在针对_mm256_set1_ps( 1.0f ) 的优化构建中发出多条指令?有些在调试版本中浪费了更多的指令,但那又怎样?反优化构建中的性能几乎无关紧要,并且已经是内在的灾难。你是对的,使用const __m256 one = _mm256_set1_ps(1.0f) 会更糟(即使进行了优化),因为大多数编译器都在这方面很糟糕,并且不会常量传播到静态初始化程序中。
  • @PeterCordes 查看英特尔自己的网站:intel.com/content/www/us/en/docs/intrinsics-guide/… 将鼠标悬停在“序列”上说“此内在函数生成指令序列,其性能可能比本机指令差。考虑此内在函数的性能影响。 "我一直在疯狂地在我的所有代码中用上面的内容替换这个内在函数,自己测试一下,有性能差异。
  • 说的是非常量参数。我确实为自己重新测试了它,commented 在你的另一个答案下重复了这一说法:如果你以理智的方式使用它们,两种方式都会编译成字面上相同的 asm,而且你的编译器并不是完全垃圾。您是否在使用旧版本的 MSVC,在内联函数后无法将向量常量设置提升到循环之外?如果是其他问题,请显示 minimal reproducible example 的 asm diff。
  • 当我专业地使用内在函数时,我会查看编译器的 asm 输出以确保它编译有效。多年来使用内在函数做事,_mm_set1_ps( constant ) 在使用它的函数中“即时”使用它总是使用 GCC 和 clang 高效编译。我通常不太关心 MSVC,尤其是众所周知的旧版 MSVC,它有严重的优化缺失。
  • 当我在 Godbolt 上查看最近的 MSVC 时,它也很好,就像我在我的 cmets 的链接中显示的关于你的另一个问题一样,就像我说的,在具有优化的非糟糕编译器上启用,像这样更改代码没有任何好处,asm 的结果是一样的。 (但除了样式之外也没有什么缺点,除了有时会重复常量)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-28
  • 1970-01-01
  • 2020-03-16
  • 2020-10-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多