为什么GCC不能将这条直线代码向量化?

6
我有下面的代码,看起来很适合使用SLP:
struct vector {
  double x, y, z;
} __attribute__((aligned(16)));

int
slp_test(struct vector *x0, struct vector *n)
{
  double t = -x0->z/n->z;
  double u = x0->x + t*n->x;
  double v = x0->y + t*n->y;
  return t >= 0.0 && u >= 0.0 && v >= 0.0 && u + v <= 1.0;
}

uv的计算似乎很容易向量化,x0n也应该足够对齐。但是在x86-64上,使用-O3编译选项,gcc 4.9.0生成了以下代码:

    movsd   .LC0(%rip), %xmm1
    movsd   16(%rdi), %xmm0
    movsd   (%rdi), %xmm2
    xorpd   %xmm1, %xmm0
    movsd   (%rsi), %xmm1
    pxor    %xmm3, %xmm3
    divsd   16(%rsi), %xmm0 ; t = x0->z/n->z
    mulsd   %xmm0, %xmm1    ; t*n->x
    addsd   %xmm1, %xmm2    ; u = x0->x + t*n->x
    movsd   8(%rsi), %xmm1
    mulsd   %xmm0, %xmm1    ; t*n->y
    ucomisd %xmm3, %xmm2
    addsd   8(%rdi), %xmm1  ; v = x0->y + t*n->y
    setae   %dl
    ucomisd %xmm3, %xmm1
    setae   %al
    testb   %al, %dl
    je      .L3
    ucomisd %xmm3, %xmm0
    jb      .L3
    addsd   %xmm2, %xmm1
    movsd   .LC2(%rip), %xmm0
    xorl    %eax, %eax
    ucomisd %xmm1, %xmm0
    setae   %al
    ret
.L3:
    xorl    %eax, %eax
    ret

为什么gcc没有使用mulpdaddpd来替代两个mulsdaddsd呢?我使用-fopt-info-all-vec尝试查看原因,但它抱怨了对齐问题(完整输出):

slp-test.c:8:17: note: === vect_analyze_data_refs_alignment ===
slp-test.c:8:17: note: vect_compute_data_ref_alignment:
slp-test.c:8:17: note: misalign = 0 bytes of ref x0_3(D)->z
slp-test.c:8:17: note: vect_compute_data_ref_alignment:
slp-test.c:8:17: note: misalign = 0 bytes of ref n_6(D)->z
slp-test.c:8:17: note: vect_compute_data_ref_alignment:
slp-test.c:8:17: note: misalign = 0 bytes of ref x0_3(D)->x
slp-test.c:8:17: note: vect_compute_data_ref_alignment:
slp-test.c:8:17: note: misalign = 0 bytes of ref n_6(D)->x
slp-test.c:8:17: note: vect_compute_data_ref_alignment:
slp-test.c:8:17: note: misalign = 8 bytes of ref x0_3(D)->y
slp-test.c:8:17: note: vect_compute_data_ref_alignment:
slp-test.c:8:17: note: misalign = 8 bytes of ref n_6(D)->y
slp-test.c:8:17: note: === vect_analyze_slp ===
slp-test.c:8:17: note: Failed to SLP the basic block.
slp-test.c:8:17: note: not vectorized: failed to find SLP opportunities in basic block.

除非我对__attribute__((aligned(16)))的理解有误,否则它应该能够强制对这些访问进行对齐。你有什么想法吗?

哼... 我得到了 test.c:1:16: 警告:属性'aligned'被忽略,将它放在"struct"之后以将属性应用于类型声明[-Wignored-attributes] - Joachim Isaksson
那是clang的警告吗?我的gcc似乎不支持它。无论如何,将__attribute__((aligned(16)))移动到正确的位置似乎并没有改变任何东西。 - Tavian Barnes
1
没有向量化的好处。在那之后需要单独访问uv。为此需要进行解包的开销超过了向量化单个算术操作的好处。 - Mysticial
@Mysticial:“u >= 0.0 && v >= 0.0”比较也可以向量化,因此只需要对“u + v <= 1.0”比较进行拆包,而这并不总是需要执行。 - Tavian Barnes
@Mysticial 从 "更好" 的意义上讲,它允许您保存一个乘法。您是正确的,实际上它可能不会更快,我将尝试进行基准测试。 - Tavian Barnes
显示剩余7条评论
1个回答

2

这段代码不会从向量化中获得什么好处,需要记住的是CPU能够在单个周期内执行多个指令。

例如,在Nehalem上,乘法/加法的延迟为4,倒数吞吐量为1,因此在理想情况下应该能够在四个周期内计算出4个这样的指令。至少应该可以完成2个。

即使矢量寄存器已经完美填充,使用打包指令也无济于事。

编辑:我没有意识到数据可以一次加载,因此设置成本可能是微不足道的

为了填充它们,您可能已经需要高低mov指令,这将花费比一些打包指令更多的代价,最终却无法带来明显提升。(在Nehalem上,mov[hl]pd的延迟约为5,而movsd的延迟为2)

比较操作也不能获得盈利性的向量化,因为您需要将打包的比较解包回普通寄存器中,这是一个非常昂贵的操作。此外,编译器无法知道分支的概率,必须假定第一个比较将始终短路其余部分,因此并行执行将有害。

编辑:但是,使用SSE4的ptest可能会获得盈利性

瓶颈在这里也很可能是无法向量化的除法。您可能更好地尝试同时对2个结构执行操作,而不是尝试在一个结构中向量化操作。


如果你在进行可向量化的算术运算时玩得更多,它仍然无法向量化。 (例如,向量乘以t,然后向量加x0->(x,y),然后向量乘以t,然后向量加n->(x,y)。)Clang可以将其向量化。 - tmyklebu
请查看我的最新评论,向量化确实有助于这里的性能。它们可以使用movapd进行填充,并且可以使用cmppdptest或类似方法进行比较。当然,gcc没有意识到这一点是合理的。 - Tavian Barnes
有趣的是,我忽略了你可以做一次加载并忘记了ptest,所以它可能可以为您节省周期。我有点惊讶,您可以节省25%。值得一提的是,ICC也不会将其向量化,即使允许使用sse4,clang也不会使用ptest。 - jtaylor
经过更仔细的基准测试,我发现我错了;代码的每个版本(GCC的、Clang的、显式向量化的版本和我手动编写的向量化汇编)在“true”情况下都需要确切的13个周期,在“false”情况下大约需要9个周期。此外,如果“true”和“false”情况混合使用,gcc的版本明显比其他所有版本都要快。 - Tavian Barnes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接