【问题标题】:Performance AVX/SSE assembly vs. intrinsics性能 AVX/SSE 程序集与内在函数
【发布时间】:2015-05-19 07:17:28
【问题描述】:

我只是想检查优化一些基本例程的最佳方法。在这种情况下,我尝试了将 2 个浮点向量相乘的非常简单的示例:

void Mul(float *src1, float *src2, float *dst)
{
    for (int i=0; i<cnt; i++) dst[i] = src1[i] * src2[i];
};

Plain C 实现非常慢。我使用 AVX 做了一些外部 ASM,也尝试使用内部函数。以下是测试结果(时间,越小越好):

ASM: 0.110
IPP: 0.125
Intrinsics: 0.18
Plain C++: 4.0

(使用MSVC 2013,SSE2编译,试用Intel Compiler,结果差不多)

正如您所见,我的 ASM 代码甚至击败了英特尔性能基元(可能是因为我做了很多分支以确保我可以使用 AVX 对齐指令)。但我个人更喜欢使用内在方法,它更容易管理,我认为编译器应该在优化所有分支和东西方面做得最好(我的 ASM 代码在这方面很糟糕,但它更快)。所以这里是使用内在函数的代码:

    int i;
    for (i=0; (MINTEGER)(dst + i) % 32 != 0 && i < cnt; i++) dst[i] = src1[i] * src2[i];

    if ((MINTEGER)(src1 + i) % 32 == 0)
    {
        if ((MINTEGER)(src2 + i) % 32 == 0)
        {
            for (; i<cnt-8; i+=8)
            {
                __m256 x = _mm256_load_ps( src1 + i); 
                __m256 y = _mm256_load_ps( src2 + i); 
                __m256 z = _mm256_mul_ps(x, y); 
                _mm256_store_ps(dst + i, z);
            };
        }
        else
        {
            for (; i<cnt-8; i+=8)
            {
                __m256 x = _mm256_load_ps( src1 + i); 
                __m256 y = _mm256_loadu_ps( src2 + i); 
                __m256 z = _mm256_mul_ps(x, y); 
                _mm256_store_ps(dst + i, z);
            };
        };
    }
    else
    {
        for (; i<cnt-8; i+=8)
        {
            __m256 x = _mm256_loadu_ps( src1 + i); 
            __m256 y = _mm256_loadu_ps( src2 + i); 
            __m256 z = _mm256_mul_ps(x, y); 
            _mm256_store_ps(dst + i, z);
        };
    };

    for (; i<cnt; i++) dst[i] = src1[i] * src2[i];

简单:首先到达 dst 与 32 字节对齐的地址,然后分支检查哪些源对齐。

一个问题是 C++ 实现在开头和结尾都没有使用 AVX,除非我在编译器中启用了 AVX,这是我不想要的,因为这应该只是 AVX 专业化,但软件应该可以在一个平台,其中 AVX 不可用。遗憾的是,vmovss 等指令似乎没有内在函数,因此将 AVX 代码与编译器使用的 SSE 混合可能会受到惩罚。但是,即使我在编译器中启用了 AVX,它仍然没有低于 0.14。

任何想法如何优化这一点以使 instrisics 达到 ASM 代码的速度?

【问题讨论】:

  • 您可以只编译启用了 avx 的专业化文件。
  • 我们为什么不直接将一个小块 memcpy 到一个已知对齐的位置,然后 memcpy 到真正的 dst?
  • 你的编译选项是什么?
  • 您可能不会将苹果与苹果进行比较(请参阅 Hurkyl 的回答)。看看组装。
  • 出于兴趣,您的数组大小是多少,典型的对齐用例是什么(如果有的话),以及您的时序数字(例如内在函数的 0.18)代表什么?

标签: c++ assembly sse intrinsics avx


【解决方案1】:

您使用内在函数的实现与您在直接 C 中的实现不同:例如如果使用参数Mul(p, p, p+1) 调用您的函数怎么办?你会得到不同的结果。纯 C 版本的速度很慢,因为编译器会确保代码完全按照您所说的进行。

如果您希望编译器基于三个数组不重叠的假设进行优化,您需要明确说明:

void Mul(float *src1, float *src2, float *__restrict__ dst)

甚至更好

void Mul(const float *src1, const float *src2, float *__restrict__ dst)

(我认为将__restrict__ 放在输出指针上就足够了,尽管将它也添加到输入指针上也无妨)

【讨论】:

  • 好点。但是my experience with __restrict__ in MSVC是被忽略了。
  • @Z:我对高性能的 MSVC 的经验很少,但英特尔的编译器应该没问题。
  • 是的,我希望 ICC 会很好,但 I had other problems with ICC in the past。但不可否认,我对 ICC 的经验很少,而且那是很久以前的事了。
  • @Zboson 我不了解矢量化,但 MSVC 上的 restrict 确实适用于其他事情。我从中得到了显着的加速。 (在 MSVC 上是)
  • @Mysticial,好的,很高兴知道。我只在那个问题上试过。我不久前放弃了使用 MSVC。顺便说一句,你为什么还要使用 MSVC?
【解决方案2】:

在带有 AVX 的 CPU 上,使用未对齐的负载几乎不会受到惩罚 - 我建议将这个小惩罚与您用于检查对齐等的所有额外逻辑进行权衡,并且只需一个循环 + 标量代码来处理任何剩余元素:

   for (i = 0; i <= cnt - 8; i += 8)
   {
        __m256 x = _mm256_loadu_ps(src1 + i); 
        __m256 y = _mm256_loadu_ps(src2 + i); 
        __m256 z = _mm256_mul_ps(x, y); 
        _mm256_storeu_ps(dst + i, z);
   }
   for ( ; i < cnt; i++)
   {
       dst[i] = src1[i] * src2[i];
   }

更好的是,首先确保您的缓冲区都是 32 字节对齐的,然后只使用对齐的加载/存储。

请注意,在这样的循环中执行单个算术运算通常是 SIMD 的一种不好的方法 - 执行时间将主要由加载和存储主导 - 您应该尝试将此乘法与其他 SIMD 操作结合起来以减轻负载/商店费用。

【讨论】:

  • 嗯,从数字中可以看出,我在 ASM 中的实现实际上要快得多,比 IPP 快 10%,不同之处在于我完成了所有的对齐工作,而我的分支很可能非常坏的 :)。因此,差异可能会更大。然而确实如此,我刚刚用 Haswell 检查了一个最新的 CPU,那里的差异要小得多,但还是有一些。由于这个例程(和类似的例程)使用了很多,我真的很想获得最好的性能。
  • 我刚刚在 Haswell i7 上尝试了您的代码,它实际上比我的 ASM 实现慢 2 倍。我坚信这种对齐方式确实相关。
  • 谢谢 - 知道我的预感是错误的很有用。不过请注意我的最后一段 - I/O 是您的主要瓶颈,因此您应该尝试将更多操作与您目前拥有的单乘法结合起来。
  • 我刚刚尝试过使用英特尔编译器和同样的东西。根据我的经验,英特尔的代码在性能方面几乎相同,只是更大。关于其他加载/存储缓解 - 我理解,但在许多情况下,这就是我需要做的 - 乘以 2 个数组。
猜你喜欢
  • 2023-03-11
  • 2015-08-15
  • 2013-10-21
  • 2016-10-29
  • 2013-08-21
  • 1970-01-01
  • 2012-02-14
  • 2015-03-11
  • 1970-01-01
相关资源
最近更新 更多