【问题标题】:Puzzling GCC behaviour with respect to vectorization and loop size关于矢量化和循环大小的令人费解的 GCC 行为
【发布时间】:2016-11-18 11:14:58
【问题描述】:

最初调查#pragma omp simd 指令的效果时,我遇到了一个我无法解释的行为,它与简单for 循环的向量化有关。下面的代码示例可以在这个很棒的compiler explorer 上进行测试,前提是应用了 -O3 指令并且我们使用的是 x86 架构。

谁能解释一下以下观察背后的逻辑?

#include <stdint.h> 

void test(uint8_t* out, uint8_t const* in, uint32_t length)
{
    unsigned const l1 = (length * 32)/32;  // This is vectorized
    unsigned const l2 = (length / 32)*32;  // This is not vectorized

    unsigned const l3 = (length << 5)>>5;  // This is vectorized
    unsigned const l4 = (length >> 5)<<5;  // This is not vectorized

    unsigned const l5 = length -length%32; // This is not vectorized
    unsigned const l6 = length & ~(32 -1); // This is not vectorized

    for (unsigned i = 0; i<l1 /*pick your choice*/; ++i)
    {
      out[i] = in[i*2];
    }
}

令我困惑的是 l1 和 l3 都生成向量化代码,尽管不能保证是 32 的倍数。所有其他长度生成向量化代码,但应该是 32 的倍数32. 这背后有什么原因吗?

顺便说一句,使用 #pragma omp simd 指令实际上并没有改变任何东西。

编辑:经过进一步调查,当索引类型为 size_t 时,行为差异消失了(甚至不需要边界操作),这意味着这会生成矢量化代码:

#include <stdint.h> 
#include <string>

void test(uint8_t* out, uint8_t const* in, size_t length)
{
    for (size_t i = 0; i<length; ++i)
    {
        out[i] = in[i*2];
    }
}

如果有人知道为什么循环向量化如此依赖于索引类型,我很想知道更多!

Edit2,感谢 Mark Lakata,实际上需要 O3

【问题讨论】:

  • 在这个问题的扩展中,Clang 可以看到完全相同的行为,所以我猜它有一些逻辑。
  • 似乎编译器害怕索引可能会因为它而换行并放弃:-(
  • 已经向我解释了类型依赖性,与溢出风险有关(这会阻止矢量化)。允许无符号溢出,而不允许有符号溢出,这就解释了最后一点。使用无符号并丢弃第一位(有效消除溢出风险)允许矢量化,GCC 非常聪明:godbolt.org/g/SsVZ2r
  • @BenjaminLefaudeux 示例中没有签名类型。 i*2 始终未签名。 i&lt;length 也是如此。
  • @BenjaminLefaudeux 请考虑接受明确的答案,我认为到 2501 年。

标签: c++ c gcc vector auto-vectorization


【解决方案1】:

问题是数组索引中从unsignedsize_t 的明显转换1in[i*2];

如果您使用l1l3,则i*2 的计算将始终适合size_t 类型。这意味着unsigned 类型的行为实际上就像是size_t

但是当您使用其他选项时,i*2 的计算结果可能不适合 size_t,因为该值可能会换行并且必须进行转换。

如果你举第一个例子,不选择选项 l1 或 l3,然后做演员:

out[i] = in[( size_t )i*2];

如果你转换整个表达式,编译器会优化:

out[i] = in[( size_t )(i*2)];

没有。


1标准实际上并没有指定索引中的类型必须是size_t,但从编译器的角度来看这是一个合乎逻辑的步骤。

【讨论】:

  • 我不确定在取消引用指针时索引是否会转换为size_t,尽管您谈论的溢出的可能性仍然是相关的
  • @GuyGreer 根据他们没有的标准,请参阅更新。
  • 我仍然不同意从unsignedsize_t 的任何形式的转换问题(在 32 位机器上是无操作的)。在处理环绕以及编译器何时可以向自己证明它不会发生时,您的回答对我来说更有意义。
  • @GuyGreer 就 C 而言没有转换。我已经澄清了措辞。
  • @GuyGreer 我也相信这是一个包装问题:在 l1/l3 的情况下,左边的位被杀死,所以编译器知道 *2 不会溢出(我有不知道他们这么聪明..),所以可以启用矢量化。我认为这就是原因,至少它连接了我这边的所有点(也是类型依赖)
【解决方案2】:

我相信您将优化与矢量化混淆了。我使用了您的compiler explorer 并将 -O2 设置为 x86,并且没有一个示例是“矢量化”的。

这里是l1

test(unsigned char*, unsigned char const*, unsigned int):
        xorl    %eax, %eax
        andl    $134217727, %edx
        je      .L1
.L5:
        movzbl  (%rsi,%rax,2), %ecx
        movb    %cl, (%rdi,%rax)
        addq    $1, %rax
        cmpl    %eax, %edx
        ja      .L5
.L1:
        rep ret

这里是l2

test(unsigned char*, unsigned char const*, unsigned int):
        andl    $-32, %edx
        je      .L1
        leal    -1(%rdx), %eax
        leaq    1(%rdi,%rax), %rcx
        xorl    %eax, %eax
.L4:
        movl    %eax, %edx
        addq    $1, %rdi
        addl    $2, %eax
        movzbl  (%rsi,%rdx), %edx
        movb    %dl, -1(%rdi)
        cmpq    %rcx, %rdi
        jne     .L4
.L1:
        rep ret

这并不奇怪,因为您所做的本质上是一个“收集”加载操作,其中加载索引与存储索引不同。 x86 中不支持收集/分散。仅在AVX2和AVX512中引入,未选中。

稍长的代码处理有符号/无符号问题,但没有进行矢量化。

【讨论】:

  • 感谢您澄清矢量化。您能否详细说明已签名/未签名的内容。 C 源代码不包含有符号类型,那么为什么要在汇编中使用它们?
  • 好吧,我真的不知道,但我猜这与间接负载的限制有关 movzbl(%rsi, %rax, 2), %ecx 并且 %rax 必须小于 32 位,否则规模2会溢出。但是我在谷歌上找不到答案...
  • fwiw,您的代码中有一个带符号的值。常数 2 是有符号的……但这在本次讨论中并不重要。
  • 我的错,-O3 矢量化,我会纠正这个问题。编译器可以通过增加字长和丢弃奇数子部分来优化负载,这是一个有效的“矢量化”,虽然不如原生交错负载好,但已经出现在 x86 for some time 上。就我而言,处理溢出风险的解释说明了一切
猜你喜欢
  • 1970-01-01
  • 2016-06-03
  • 2021-03-04
  • 2021-03-01
  • 1970-01-01
  • 2012-06-11
  • 2011-12-29
  • 2020-10-06
  • 2019-04-12
相关资源
最近更新 更多