【问题标题】:What's the difference between GCC builtin vectorization types and C arrays?GCC 内置向量化类型和 C 数组有什么区别?
【发布时间】:2013-05-13 05:01:38
【问题描述】:

我有三个函数 a()b()c() 应该做同样的事情:

typedef float Builtin __attribute__ ((vector_size (16)));

typedef struct {
        float values[4];
} Struct;

typedef union {
        Builtin b;
        Struct s;
} Union;

extern void printv(Builtin);
extern void printv(Union);
extern void printv(Struct);

int a() {
        Builtin m = { 1.0, 2.0, 3.0, 4.0 };
        printv(m);
}

int b() {
        Union m = { 1.0, 2.0, 3.0, 4.0 };
        printv(m);
}

int c() {
        Struct m = { 1.0, 2.0, 3.0, 4.0 };
        printv(m);
}

当我编译这段代码时,我观察到以下行为:

  • a() 中调用printv() 时,所有4 个浮点数都被%xmm0 传递。不会写入内存。
  • 当在b() 中调用printv() 时,%xmm0 传递了两个浮点数,%xmm1 传递了另外两个浮点数。为了完成这 4 个浮点数被加载 (.LC0) 到 %xmm2 并从那里加载到内存中。之后,从内存中的同一位置将 2 个浮点数读取到 %xmm0,并将其他 2 个浮点数 (.LC1) 加载到 %xmm1
  • 我对 c() 的实际作用有点迷茫。

为什么a()b()c() 不同?

这是 a() 的汇编输出:

        vmovaps .LC0(%rip), %xmm0
        call    _Z6printvU8__vectorf

b() 的汇编输出:

        vmovaps .LC0(%rip), %xmm2
        vmovaps %xmm2, (%rsp)
        vmovq   .LC1(%rip), %xmm1
        vmovq   (%rsp), %xmm0
        call    _Z6printv5Union

以及 c() 的汇编输出:

         andq    $-32, %rsp
         subq    $32, %rsp
         vmovaps .LC0(%rip), %xmm0
         vmovaps %xmm0, (%rsp)
         vmovq   .LC2(%rip), %xmm0
         vmovq   8(%rsp), %xmm1
         call    _Z6printv6Struct

数据:

        .section        .rodata.cst16,"aM",@progbits,16
        .align 16
.LC0:
        .long   1065353216
        .long   1073741824
        .long   1077936128
        .long   1082130432
        .section        .rodata.cst8,"aM",@progbits,8
        .align 8
.LC1:
        .quad   4647714816524288000
        .align 8
.LC2:
        .quad   4611686019492741120

四元组4647714816524288000 似乎只不过是相邻长字中的浮点数3.04.0

【问题讨论】:

  • 看起来这是调用约定的问题。 __m128 可以直接通过寄存器传入。但是Packed 必须在传递xmm0xmm1 的参数的情况下传递。简而言之,调用约定可能会阻止编译器进行这样的优化。
  • 自从您发表评论后,我对问题进行了相当多的编辑。我用在头文件中找到的 GCC 内置类型替换了所有与 AVX 相关的内容,以使问题更容易理解。但我认为您对调用约定可能是正确的。但为什么呢?

标签: gcc assembly sse vectorization


【解决方案1】:

很好的问题,我不得不挖掘一下,因为我自己从未使用过SSE(在本例中为 SSE2)。本质上,向量指令用于对存储在 one 寄存器(即 XMM(0-7) 寄存器)中的 多个 值进行操作。在 C 中,数据类型 float 使用 IEEE 754,因此其长度为 32 位。使用四个浮点数将产生一个长度为 128 位的向量,这正是 XMM(0-7) 寄存器的长度。现在SSE提供的寄存器是这样的:

SSE (avx-128):                         |----------------|name: XMM0; size: 128bit
SSE (avx-256):        |----------------|----------------|name: YMM0; size: 256bit

在您的第一种情况a() 中,您使用 SIMD 矢量化与

typedef float Builtin __attribute__ ((vector_size (16)));

它允许您将整个向量一次性移入 XMM0 寄存器。现在在您的第二种情况下b() 您使用联合。但是因为您没有将 .LC0 加载到与 Union m.b = { 1.0, 2.0, 3.0, 4.0 }; 的联合中,所以数据不会被识别为矢量化。这会导致以下行为:

.LC0 中的数据通过以下方式加载到 XMM2:

 vmovaps .LC0(%rip), %xmm2

但由于您的数据可以被解释为 结构 矢量化,因此必须将数据分成两个 64 位块仍然必须在 XMM(0-7) 寄存器中,因为它可以被视为矢量化,但它必须最长为 64 位长才能传输到寄存器(只有 64 位宽,如果 128 位是转移给它;数据丢失),因为数据也可以被视为一个结构。这是在下面完成的。

XMM2 中的向量化被加载到内存中

    vmovaps %xmm2, (%rsp)

现在矢量化的高 64 位(位 64-127),即浮点 3.04.0 被移动(vmovq 移动四字,即 64 位)到 XMM1

    vmovq   .LC1(%rip), %xmm1

最后是矢量化的低 64 位(0-63 位),即浮点 1.02.0 从内存移动到 XMM0

    vmovq   (%rsp), %xmm0

现在您在不同的 XMM(0-7) 寄存器中拥有了 128 位向量的上半部分和下半部分。

现在以防万一c() 我也不太确定,但它就是这样。首先 %rsp 与 32 位地址对齐,然后减去 32 字节以将数据存储在堆栈上(这将再次与 32 位地址对齐)这是通过

     andq    $-32, %rsp
     subq    $32, %rsp

现在这个向量化被加载到 XMM0 中,然后放在堆栈中

     vmovaps .LC0(%rip), %xmm0
     vmovaps %xmm0, (%rsp)

最后向量化的高64位存储在XMM0中,低64位存储在XMM1寄存器中

     vmovq   .LC2(%rip), %xmm0
     vmovq   8(%rsp), %xmm1

在所有三种情况下,向量化的处理方式都不同。希望这会有所帮助。

【讨论】:

  • 这与我观察到的差不多。我真的很想知道为什么 GCC 会以不同的方式处理这三种情况。我真的看不出有什么区别,至少在语义上是这样。
  • 您指出“它必须最长为 64 位,才能传输到寄存器”。这是为什么呢?
  • 您的处理器是 64 位,即寄存器是 64 位宽,只有 XMM0-7 更宽。我认为案例(b)很好地说明了这一点。数据的存储方式与情况 (a) 不同,因为数据由于联合,可以解释为向量化结构
猜你喜欢
  • 2016-03-03
  • 2011-08-16
  • 1970-01-01
  • 2020-04-17
  • 1970-01-01
  • 1970-01-01
  • 2012-01-25
  • 2010-10-14
相关资源
最近更新 更多