GCC/Clang提供的向量扩展是一种方便的方法,可以在多种架构(如webassembly,arm 64,x64)上启用SIMD向量化。
与
using v8x16u = uint8_t __attribute__((vector_size(16)));
v8x16u add(v8x16u a, v8x16u b) { return a + b; }
add v0.16b, v1.16b, v0.16b
ret
它很容易做一些SIMD编程(即使在某些情况下性能不太高)。
我无法从任何文档中找到的是 * 从内存中初始化这些向量的首选/规范方法是什么 *。
v8x16u load(uint8_t const *p) {
v8x16u a;
for (int i = 0; i < 16; i++) a[i] = p[i];
}
在clang x64和clang armv 8上确实可以正常工作,但在GCC 7.3上有点不太理想
ldr q0, [x0] // clang armv8
movdqu xmm0, XMMWORD PTR [rdi] // gcc x64 7.3
movups xmm0, xmmword ptr [rdi] // clang 15.0.0 x64
sub sp, sp, #16 // GCC arm64 7.3
ldr q0, [x0]
add sp, sp, 16
在webassembly中,结果是灾难(循环)。对于webassembly和clang来说,展开加载是有效的,但对于GCC来说则不然,因为GCC会加载16个单独的字节
v8x16u load(uint8_t const *p) {
v8x16u a{p[0],p[1],p[2],p[3],
p[4],p[5],p[6],p[7],
p[8],p[9],p[10],p[11],
p[12],p[13],p[14],p[15]
};
return a;
}
local.get 0
v128.load 0:p2align=0
end_function
最后,类型双关确实可以编译,但它不会引入UB吗?而且clang似乎在实现中可能有一个bug(因为在这种情况下,使用vs typedef应该同样可以使用AFAIK,保留属性)
v8x16u load_fail(uint8_t const *p) {
using Vec = uint8_t __attribute__((vector_size(16), aligned(1)));
return *reinterpret_cast<const Vec*>(p);
}
movaps xmm0, xmmword ptr [rdi]
v8x16u load_okish(uint8_t const *p) {
typedef uint8_t Vec __attribute__((vector_size(16))) __attribute__((aligned(1)));
return *reinterpret_cast<const Vec*>(p);
movups xmm0, xmmword ptr [rdi]
}
1条答案
按热度按时间pkmbmrz71#
使用适当的
aligned
属性将指针强制转换为向量类型,并解引用它。当指向内存的类型(它的动态类型,可能与声明的类型不同-在放置new之后,或者如果它首先在堆上分配)与vector的底层标量类型兼容时,它不会引入UB。GCC允许标量和向量类型之间的别名,但还没有文档说明。对于Clang/LLVM,它似乎是相同的(允许在没有文档的情况下使用别名)。对于这两个编译器,自动向量化在内部引入对标量数组的向量类型访问的方式自然会使保证福尔斯失效。由于评论中提到的Clang错误,需要使用
typedef
而不是using
引入具有减少对齐的变体。您可以额外引入具有
may_alias
属性的向量变量,用于访问未知/任意类型的内存(当标量实现将使用memcpy
或基于字符的访问时),或者使用char
的向量来执行内存访问,然后将向量位转换为计算所需的类型。使用
memcpy
是有风险的,因为系统头通常会使用__builtin_object_size
的内联变体来覆盖它,以进行加固(所谓的FORTIFY_SOURCE
功能),这会干扰优化,特别是内联的速度/大小估计。显式地使用__builtin_memcpy
可以避免这种情况,但在这一点上,使用自定义向量类型似乎更干净。