当前位置: 首页 > 知识库问答 >
问题:

为什么向量长度SIMD代码比普通C慢

平学
2023-03-14

为什么我的SIMD向量4长度函数比原始向量长度方法慢3倍?

SIMD矢量4长度函数:

__extern_always_inline float vec4_len(const float *v) {
    __m128 vec1 = _mm_load_ps(v);
    __m128 xmm1 = _mm_mul_ps(vec1, vec1);
    __m128 xmm2 = _mm_hadd_ps(xmm1, xmm1);
    __m128 xmm3 = _mm_hadd_ps(xmm2, xmm2);
    return sqrtf(_mm_cvtss_f32(xmm3));
}

幼稚的实现:

sqrtf(V[0] * V[0] + V[1] * V[1] + V[2] * V[2] + V[3] * V[3])
#include <math.h>
#include <time.h>
#include <stdint.h>
#include <stdio.h>
#include <x86intrin.h>

static float vec4_len(const float *v) {
    __m128 vec1 = _mm_load_ps(v);
    __m128 xmm1 = _mm_mul_ps(vec1, vec1);
    __m128 xmm2 = _mm_hadd_ps(xmm1, xmm1);
    __m128 xmm3 = _mm_hadd_ps(xmm2, xmm2);
    return sqrtf(_mm_cvtss_f32(xmm3));
}

int main() {
    float A[4] __attribute__((aligned(16))) = {3, 4, 0, 0};

    struct timespec t0 = {};
    clock_gettime(CLOCK_MONOTONIC, &t0);

    double sum_len = 0;
    for (uint64_t k = 0; k < 1000000000; ++k) {
        A[3] = k;
        sum_len += vec4_len(A);
//        sum_len += sqrtf(A[0] * A[0] + A[1] * A[1] + A[2] * A[2] + A[3] * A[3]);
    }
    struct timespec t1 = {};
    clock_gettime(CLOCK_MONOTONIC, &t1);

    fprintf(stdout, "%f\n", sum_len);

    fprintf(stdout, "%ldms\n", (((t1.tv_sec - t0.tv_sec) * 1000000000) + (t1.tv_nsec - t0.tv_nsec)) / 1000000);

    return 0;
}

我用GCC(Ubuntu7.4.0-1Ubuntu1~18.04.1)7.4.0:

gcc -Wall -Wextra -O3 -msse -msse3 sse.c -lm && ./a.out

SSE版本输出:

499999999500000128.000000
13458ms

纯C版本输出:

499999999500000128.000000
4441ms

共有1个答案

倪棋
2023-03-14

最明显的问题是使用效率低下的点积(使用haddps,其成本为2倍shuffle uops+1x add uop),而不是shuffle+add。关于在_mm_mul_ps之后要做什么,请参见在x86上做水平浮点向量和的最快方法,这并不太费劲。但这仍然不是x86能够非常有效地完成的事情。

但无论如何,真正的问题是您的基准循环。

A[3]=K;然后使用_mm_load_ps(A)创建一个存储转发延迟,如果它是简单编译而不是向量洗牌的话。如果加载只从单个存储指令加载数据,而没有其他数据,则存储+重新加载可以以大约5个延迟周期有效地转发。否则,它必须对整个存储缓冲区进行较慢的扫描来组装字节。这会给存储转发增加大约10个延迟周期。

参见Agner Fog的microarch指南和/或优化手册。

  • Intel开发人员手册中的“存储缓冲区转发”是什么意思?
  • 在内存访问不对齐的情况下,存储区如何进行加载转发?
  • 现代x86实现是否可以从一个以上的先前存储进行存储转发?
  • 编译器为什么要生成这个程序集?引用英特尔的优化手册RE:存储转发。(在这个问题中,旧的gcc版本分别存储了一个8字节结构的两个dword半部分,然后用qword加载/存储。Super braindead复制该结构。)

另外,通过让编译器将v[0]*v[0]+v[1]*v[1]+v[2]*v[2]的计算从循环中移出,您对SSE的影响更大。

GCC9.1来自Kamil的Godbolt链接的内部循环看起来很糟糕,似乎包括了一个循环携带的存储/重新加载,以将新的a[3]合并到8字节的a[2..3]对中,进一步限制了CPU重叠多个迭代的能力。

我不知道为什么海合会认为这是个好主意。对于将向量加载分成8字节的CPU(如Pentium M或Bobcat)可能会有所帮助,以避免存储转发停滞。但对于“通用”的现代x86-64 CPU来说,这不是一个合理的调优

.L18:
        pxor    xmm4, xmm4
        mov     rdx, QWORD PTR [rsp+8]     ; reload A[2..3]
        cvtsi2ss        xmm4, rbx
        mov     edx, edx                   ; truncate RDX to 32-bit
        movd    eax, xmm4                  ; float bit-pattern of (float)k
        sal     rax, 32
        or      rdx, rax                   ; merge the float bit-pattern into A[3]
        mov     QWORD PTR [rsp+8], rdx     ; store A[2..3] again

        movaps  xmm0, XMMWORD PTR [rsp]    ; vector load: store-forwarding stall
        mulps   xmm0, xmm0
        haddps  xmm0, xmm0
        haddps  xmm0, xmm0
        ucomiss xmm3, xmm0
        movaps  xmm1, xmm0
        sqrtss  xmm1, xmm1
        ja      .L21             ; call sqrtf to set errno if needed; flags set by ucomiss.
.L17:

        add     rbx, 1
        cvtss2sd        xmm1, xmm1
        addsd   xmm2, xmm1            ; total += (double)sqrtf
        cmp     rbx, 1000000000
        jne     .L18                ; }while(k<1000000000);

这种疯狂在标量版本中是不存在的。

不管怎样,gcc都避免了完整的UINT64_T->float转换的低效(在AVX512之前,x86在硬件中没有这种转换)。它可以证明使用有符号的64位->浮点转换总是有效的,因为不能设置高位。

脚注1:但是sqrtps与scalar具有相同的每3个循环1的吞吐量,因此通过一次水平执行1个向量,而不是并行执行4个向量的4个长度,您只能获得CPU sqrt吞吐量能力的1/4。

 类似资料:
  • 我最近用Java写了一个计算密集型算法,然后把它翻译成C++。令我吃惊的是,C++的执行速度要慢得多。我现在已经编写了一个更短的Java测试程序,以及一个相应的C++程序-参见下面。我的原始代码具有大量的数组访问功能,测试代码也是如此。C++的执行时间要长5.5倍(请参阅每个程序末尾的注释)。 以下1st21条评论后的结论... null null Java代码: C++代码:

  • 问题内容: 以下是C 中的一个简单循环。计时器正在使用QueryPerformanceCounter(),并且非常准确。我发现Java占用了C 60%的时间,这不是吗?我在这里做错了什么?即使是严格的别名(此处未包含在代码中)也完全没有帮助… 此C ++的运行时间约为9.5秒。我正在将Intel Compiler 12.1用于主机处理器优化(专门针对我的处理器),并将所有功能都最大化。所以这是最好

  • 问题内容: 我想迭代地构建稀疏矩阵,并注意到根据SciPy文档,有两种合适的选择: LiL矩阵: 类scipy.sparse.lil_matrix(arg1,shape = None,dtype = None,copy = False)[源]基于行的链表稀疏矩阵 这是用于增量构造稀疏矩阵的有效结构。 DoK矩阵: 类scipy.sparse.dok_matrix(arg1,shape = None

  • 问题内容: 考虑下面的go代码: : 我不明白的是,为什么taste_fruits的容量为3,直觉上我希望为2,因为这是切片的长度? 而且,如果tasty_fruits的容量为3,那么为什么: 造成: 问题答案: 这行: 创建一个 数组 ,而不是一个切片。即使您仅提供了3个元素,它也有4个元素。输出: 切片: 结果是: 长度:明显2.容量? 的 容量 是…的片的长度和超过所述切片中的[基本]阵列的

  • 我一直在研究一个深度学习库,自己写作。在矩阵运算中,获得最佳性能对我来说是一个关键。我一直在研究编程语言及其对数字运算的性能。过了一段时间,我发现C#SIMD具有与C++SIMD非常相似的性能。所以,我决定用C#编写这个库。 首先,我测试了C#SIMD(我测试了很多东西,但是这里不写了)。我注意到,当使用较小的数组时,它的工作效果要好得多。当使用较大的数组时,效率不高。我觉得很可笑。通常情况下,当

  • 本文向大家介绍唯一索引比普通索引快吗, 为什么?相关面试题,主要包含被问及唯一索引比普通索引快吗, 为什么?时的应答技巧和注意事项,需要的朋友参考一下 唯一索引不一定比普通索引快, 还可能慢. 查询时, 在未使用limit 1的情况下, 在匹配到一条数据后, 唯一索引即返回, 普通索引会继续匹配下一条数据, 发现不匹配后返回. 如此看来唯一索引少了一次匹配, 但实际上这个消耗微乎其微. 更新时,