SSE 内在函数优化

Posted

技术标签:

【中文标题】SSE 内在函数优化【英文标题】:SSE intrinsics optimisation 【发布时间】:2016-05-14 06:37:35 【问题描述】:

我是 SSE 内在函数的新手,并尝试通过它优化我的代码。这是我关于计算等于给定值的数组元素的程序。

我将代码更改为 SSE 版本,但速度几乎没有变化。我想知道我是否以错误的方式使用 SSE...

此代码用于不允许我们启用编译器优化选项的赋值。

没有 SSE 版本:

int get_freq(const float* matrix, float value) 

    int freq = 0;

    for (ssize_t i = start; i < end; i++) 
        if (fabsf(matrix[i] - value) <= FLT_EPSILON) 
            freq++;
        
    

    return freq;

上交所版本:

#include <immintrin.h>
#include <math.h>
#include <float.h>

#define GETLOAD(n) __m128 load##n = _mm_load_ps(&matrix[i + 4 * n])
#define GETEQU(n) __m128 check##n = _mm_and_ps(_mm_cmpeq_ps(load##n, value), and_value)
#define GETCOUNT(n) count = _mm_add_ps(count, check##n)

    int get_freq(const float* matrix, float givenValue, ssize_t g_elements) 

        int freq = 0;
        int i;

        __m128 value = _mm_set1_ps(givenValue);
        __m128 count = _mm_setzero_ps();
        __m128 and_value = _mm_set1_ps(0x00000001);


        for (i = 0; i + 15 < g_elements; i += 16) 
            GETLOAD(0); GETLOAD(1); GETLOAD(2); GETLOAD(3);
            GETEQU(0);  GETEQU(1);  GETEQU(2);  GETEQU(3);
            GETCOUNT(0);GETCOUNT(1);GETCOUNT(2);GETCOUNT(3);
        

        __m128 shuffle_a = _mm_shuffle_ps(count, count, _MM_SHUFFLE(1, 0, 3, 2));
        count = _mm_add_ps(count, shuffle_a);
        __m128 shuffle_b = _mm_shuffle_ps(count, count, _MM_SHUFFLE(2, 3, 0, 1));
        count = _mm_add_ps(count, shuffle_b);
        freq = _mm_cvtss_si32(count);


        for (; i < g_elements; i++) 
            if (fabsf(matrix[i] - givenValue) <= FLT_EPSILON) 
                freq++;
            
        

        return freq;
    

【问题讨论】:

为什么要关心性能,向量代码甚至还没有正确 你能告诉我错误是什么吗?我会改正的。 你不比较fabs(x) &lt;= FLT_EPSILON,你比较x != 0.0f,完全不等同于非矢量化版本。 比较x != 0.0f 还是x == 0.0f 不是重点。两者都不等同于fabs(x) &lt;= FLT_EPSILON。不,SSE 不会神奇地使浮点数学变得无限精确。我会说你的测试很糟糕,不要练习FLT_EPSILON 相关的情况。当然,无论如何,该测试的价值值得怀疑,但我不是就该主题进行演讲的合适人选。 好吧,我还建议您研究一下 FLT_EPSILON 的实际 含义,以及它与浮点不准确性的关系。提示:一般不适合作为两个值之差的绝对误差。 【参考方案1】:

如果您需要使用-O0 进行编译,则尽可能在单个语句中执行。在普通代码中,int a=foo(); bar(a); 将编译为与 bar(foo()) 相同的 asm,但在 -O0 代码中,第二个版本可能会更快,因为它不会将结果存储到内存然后重新加载它以供下一个声明。

-O0 旨在提供最可预测的调试结果,这就是为什么在每条语句之后所有内容都存储到内存中的原因。这显然对性能很不利。

我写a big answer a while ago 是为了一个与其他人不同的问题,因为像你这样的愚蠢任务要求他们针对-O0 进行优化。其中一些可能会有所帮助。


不要努力完成这项任务。您发现的大多数使用-O0 使您的代码运行得更快的“技巧”可能只对-O0 有用,但在启用优化时没有任何区别。

在现实生活中,代码通常至少使用 clang 或 gcc -O2 编译,有时使用 -O3 -march=haswell 或任何自动矢量化的东西。 (一旦调试完毕,您就可以进行优化了。)


回复:您的更新:

现在它编译了,可以看到来自 SSE 版本的可怕 asm。我把它放在on godbolt along with a version of the scalar code that actually compiles, too。

标量版本的结果要好得多。它的源代码在一个表达式中完成所有操作,因此临时变量保留在寄存器中。但是,循环计数器仍在内存中,例如,在 Haswell 上,它的瓶颈最多是每 6 个周期迭代一次。 (有关优化资源,请参阅x86 标签 wiki。)


顺便说一句,矢量化fabsf() 很简单,请参阅Fastest way to compute absolute value using SSE。那和 SSE 比较小于应该可以为您提供与标量代码相同的语义。 (但更难让-O0 不烂)。

您最好手动展开标量版本一到两次,因为-O0 太糟糕了。

【讨论】:

【参考方案2】:

一些编译器非常擅长对向量进行优化。您是否检查了两个版本的优化构建的生成程序集? “天真的”版本不是实际上使用了 SIMD 或其他优化技术吗?

【讨论】:

是的...但是由于我们的作业要求,编译器中的所有优化标志都已关闭。 @JenniferQ: /facepalm。 __m128 变量在-O0 编译器输出中的语句之间被存储/重新加载。为-O0 优化代码与使用-O2-O3 实际优化代码以编译为良好的asm 有显着差异。我打算亲自查看 asm on godbolt,但您问题中的代码甚至无法编译。例如变量名称冲突,请参阅该链接中的错误消息。 这是我的错。我从整个程序中截取了代码,但忘记更改某些内容......现在编译应该没问题。

以上是关于SSE 内在函数优化的主要内容,如果未能解决你的问题,请参考以下文章

使用 Intel 内在函数的位反向重新排序优化

SIMD/SSE:短点积和短最大值

Delphi中的SSE2优化?

用于比较 (_mm_cmpeq_ps) 和赋值操作的 SSE 内在函数

用 sse 执行内在函数

SSE 内在函数检查零标志