用C/gcc内置函数交换NEON向量的两半部分:没有VSWP内置函数?

3

我希望能够使用 NEON 向量指令来完成相对简单的任务:

给定一个 uint64x2_t,我想要交换其中两个 64 位成员的位置。

也就是说,如果这是一段简单的普通代码:

typedef struct {
    U64 u[2];
} u64x2;


u64x2 swap(u64x2 in)
{
    u64x2 out;
    out.u[0] = in.u[1];
    out.u[1] = in.u[0];
    return out;
}

令人惊讶的是,我找不到相关的内置函数。显然有一个汇编指令 (VSWP),但没有相应的内置函数。

这很奇怪。这是一个非常微不足道的操作,所以一定是可以的。问题是:如何实现?

编辑:参考@Jake的答案,使用godbolt的结果为: https://godbolt.org/z/ueJ6nB。 没有vswp,但vext效果很好。


你应该提及你的编译器。它是GCC还是ARMCC? - Eugene Sh.
目前是 gcc - Cyan
这是适用于armcc的引用:“此指令作为内部函数没有任何好处,因为内部函数使用变量来封装寄存器分配和访问。因此,可以使用简单的C风格变量赋值来执行变量交换。” 我猜GCC也是一样的。 - Eugene Sh.
2
相关链接:https://stackoverflow.com/questions/39514952/intrinsics-neon-swap-elements-in-vector - Eugene Sh.
1
@Cyan:你只是使用了普通的结构体,其ABI通过整数寄存器传递,而不是向量。我添加了一个类型转换和GNU C本地向量尝试(https://godbolt.org/z/EPeMFo),这两个仍然编译成vec->int->vec。所以还是不太好。 - Peter Cordes
显示剩余5条评论
2个回答

5

您是正确的,NEON内置函数不支持VSWP指令。

不过,您可以使用内置函数中也可用的VEXT指令代替它。

out = vextq_u64(in, in, 1);


或者,您可以使用vcombine(并祈祷编译器不会出错):

out = vcombine_U64(vget_high_u64(in), vget_low_u64(in));

但是要注意,编译器在看到vcombinevget时候往往会生成错误的机器代码。

我建议您使用前一种方法。


@Cyan:我也很好奇。vext是两个寄存器连接的字节粒度滑动窗口,只有一个q输出寄存器,而不是两个d寄存器,这可能会有所帮助。但是,我不知道是否有任何像Agner Fog为x86那样收集ARM指令时序的好来源。如果vext的吞吐量有限但延迟相同,则潜在瓶颈将取决于周围代码以及它们在哪些“端口”/“管道”/“执行单元”上出现瓶颈(如果有的话),至少对于乱序CPU来说。 - Peter Cordes
1
@Cyan 不用担心。VEXTVSWP 甚至更快,VEXT 被认为是一条置换指令,因此在 Cortex-A8 上会在不同的流水线上执行,这使得它有可能更快。 - Jake 'Alquimista' LEE
1
@PeterCordes 话虽如此,我们真正需要知道的是ARM代表的顺序核心的行为方式。在Cortex-A53上运行的代码在任何自定义的乱序核心上都不会变差。它们只是不应该这样。始终记住,在ARM平台上更重要的是功率效率而不是纯性能。 - Jake 'Alquimista' LEE
1
@PeterCordes 另一个例子:Cortex-A15具有“代码缓冲区”; 如果循环包括少于16条指令(包括末尾的分支指令),它们将落入代码缓冲区,在执行期间省略了获取和解码阶段,从而节省了大量功率。如果它运行得更快,我不会感到惊讶。自那时以来,事情变得更容易了;我不再那么激进地展开循环,并倾向于使用较短的代码。由于功率效率是主要关注点,因此ARM上的周期计数之外还有许多其他因素。 - Jake 'Alquimista' LEE
1
对于一些更现代的内核(例如Cortex-A76-https://developer.arm.com/docs/swog307215/latest/arm-cortex-a76-software-optimization-guide),您肯定希望使用VEXT(2个周期延迟)而不是VSWP(4个周期延迟)。 Cortex-A75更喜欢VSWP而不是VEXT(https://static.docs.arm.com/101398/0200/arm_cortex_a75_software_optimization_guide_v2.pdf) - James Greenhalgh
显示剩余11条评论

5
另一种表达这个洗牌的方法是使用GNU C native vector内置函数,它们提供了执行给定操作的与目标无关的方式。编译时常量洗牌掩码可以根据目标支持情况优化为即时洗牌。但是,运行时变量洗牌可能会因目标ISA支持而效率低下。
#include <arm_neon.h>

#ifndef __clang__
uint64x2_t swap_GNU_shuffle(uint64x2_t in)
{
    uint64x2_t mask = {1,0};
    uint64x2_t out = __builtin_shuffle (in, mask);
    return out;
}
#endif

在Godbolt上使用AArch64 gcc8.2编译实际上会生成与Jake建议的相同的洗牌操作,而不是SWP操作:

swap_GNU_shuffle:
        ext     v0.16b, v0.16b, v0.16b, #8
        ret

Clang也会将我们大部分纯C尝试优化为一个ext指令,包括使用memcpy进行类型转换到普通结构体和返回。与GCC不同,它没有很好的洗牌优化器。(在Godbolt上,使用下拉菜单中带有-O3 -target arm64的任何clang。clang通常默认构建支持多个目标ISA,而GCC不支持。)因此,这些编译器要么都错过了针对tune=generic和-mcpu=cortex-a53a57a75的优化,要么ext实际上是一个很好的选择,也许比swp更好,后者必须写入2个输出寄存器,而不是逻辑上写入一个全宽寄存器。但通常对于ARM来说这不是问题;相当多的指令可以做到这一点,并且它们通常使其高效。

ARM Cortex-A8的定时信息显示vextvswp具有相同的数字(从QnQ输出延迟为1个周期,但从QmQ输出延迟为2个周期)。我还没有检查过更新的核心(或任何64位核心)。


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