性能AVX / SSE组装与内在函数

时间:2015-03-17 00:23:41

标签: c++ assembly sse intrinsics avx

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

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

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

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

(使用MSVC 2013编译,SSE2,尝试过英特尔编译器,结果几乎相同)

你可以看到我的ASM代码甚至打败了Intel Performance Primitives(可能是因为我做了很多分支以确保我可以使用AVX对齐指令)。但我个人喜欢利用内在的方法,它更容易管理,我认为编译器应该做最好的工作优化所有的分支和东西(我的ASM代码很糟糕imho,但它更快)。所以这里是使用内在函数的代码:

    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代码的速度?

2 个答案:

答案 0 :(得分:3)

使用内在函数实现与直接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__就足够了,尽管将它添加到输入指针也没什么坏处。)

答案 1 :(得分:1)

在使用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操作相结合以减轻负载/商店成本。