LLVM 中的 ARM 高级 SIMD (NEON) 内建函数和类型
LLVM 现在支持 ARM 为高级 SIMD(又名“NEON”)指令集定义的所有内建函数,但如果您正在从 GCC 迁移到 LLVM,可能会遇到一些实现差异。LLVM 比 GCC 更密切地遵循 ARM 对标准 NEON 类型的规范。它在检查 NEON 内建函数参数类型方面也更加严格。这篇文章介绍了 LLVM 和 GCC 之间与 NEON 相关的差异,并提供了一些关于如何调整您的代码以与 LLVM 一起工作的示例。
背景
NEON 是 ARM 架构的矢量处理扩展。它包含在大多数最新的 ARM 处理器中,例如 Cortex A8 和 A9。一些 NEON 指令执行的操作在 C 或 C++ 中难以指定,因此 ARM 为这些操作定义了一套标准的内建函数。例如,vqadd_s16 内建函数执行两个 64 位矢量的饱和加法,其元素为 16 位有符号整数。ARM 还定义了一套标准的 NEON 矢量类型,用于与这些内建函数一起使用。例如,vqadd_s16 内建函数的参数和返回值的类型为 int16x4_t。这些内建函数和类型在 <arm_neon.h> 头文件中声明,该文件由编译器提供。
这些 NEON 内建函数至少存在两个先前的实现。ARM 的 RealView Compilation Tools (RVCT) 编译器提供了完整的一套内建函数,并且毫不奇怪,RVCT 严格遵循 ARM 的规范。GCC 也实现了 NEON 内建函数,但它在某些方面与 RVCT 和 ARM 的规范有所不同(至少在 llvm-gcc 源自的 4.2.1 版本中是这样的)。
LLVM 中 NEON 内建函数的当前状态是,llvm-gcc 完全支持它们,尽管无疑还有进一步提高性能的空间。Clang 还不支持 NEON。欢迎提交补丁!
不同的类型
ARM 将 NEON 矢量类型定义为不透明的“容器化矢量”。这些类型在 <arm_neon.h> 中定义为 C 结构体。用户可见的类型名称是对这些内部结构体的 typedef。例如,4 个浮点数的矢量类型定义为
typedef struct __simd128_float32_t float32x4_t;
内部结构体的內容没有指定,因此您对这些类型的唯一可移植操作是将它们传递给 NEON 内建函数。
GCC 具有自己的语法来指定矢量类型。这种语法不是特定于 NEON 的。矢量类型通过添加一个带有字节总大小的 "__vector_size__" 属性来定义。例如
typedef float float32x4_t __attribute__ ((__vector_size__ (16)));
GCC 的标准 NEON 类型实现使用它自己的矢量语法,而不是使用不透明的容器化矢量。
那么 LLVM 呢?我们折衷,两者都做!NEON 类型在 <arm_neon.h> 中定义为结构体,遵循 ARM 的规范,但这些结构体的內容是使用 GCC 语法定义的矢量。每个内部结构体都包含一个名为“val”的单个元素,该元素具有 GCC 矢量类型。GCC 矢量类型使用 "__neon_" 前缀定义为标准 NEON 类型名称。因此,如果您想直接访问 GCC 矢量类型,您可以使用 LLVM。该代码将不可移植——它无法与 RVCT 一起使用——但这可能会简化从 GCC 的过渡。
这种 NEON 类型差异有什么影响?主要区别在于,LLVM NEON 类型是聚合类型,而不是标量类型,因此您无法执行诸如将它们强制转换为整型类型之类的操作。您也不能使用“asm”寄存器属性将 NEON 变量分配到特定 NEON 寄存器,因为这在聚合类型中不受支持。请参阅以下关于您初始化矢量方式的相关差异。
更严格的类型检查
LLVM 的 NEON 内建函数的参数比 GCC 的参数受到更严格的类型检查。只要总大小保持不变,并且您不混合整数和浮点数矢量,GCC 的矢量类型就可以强制转换为其他矢量类型。GCC 的 NEON 内建函数的参数也会受到相同处理。您可以为 int32x2_t 参数传递一个 uint8x8_t 值,而 GCC 甚至不会发出警告。LLVM 要求参数类型完全匹配。如果您的代码在矢量类型方面比较随意,您需要清理它才能使用 LLVM 编译。
如果您确实要强制转换 NEON 矢量类型,正确的方法是使用 vreinterpret 内建函数。例如,vreinterpret_s32_u8 将执行上面提到的从 uint8x8_t 到 int32x2_t 的强制转换。
如何初始化矢量?
GCC 的矢量类型可以直接分配一个花括号括起来的值列表,该列表对应于矢量元素。例如
int32x2_t vec = { 1, 2 };
初始化一个元素值为 1 和 2 的矢量。这很方便,但不可移植。通常,为矢量分配值的最优方法是从内存中使用 NEON 内建函数加载它。这完全是可移植的,并且通常与其他方法一样快或更快。
有些特殊情况,您可以做得更好。如果所有矢量元素都具有相同的值,则使用 vdup 内建函数之一将是一个不错的解决方案。您可以使用 vcreate 内建函数从 64 位值构建矢量,并且 vcombine 内建函数可以将两个这样的矢量组合在一起形成一个 128 位矢量。但是,将值从通用 ARM 寄存器移动到 NEON 寄存器文件可能非常慢,因此这可能比加载快。如果矢量元素是浮点数,那么它们可能已经位于正确的寄存器文件中,使用 vset_lane 内建函数将它们组合在一起形成一个矢量可能更快。为这些不同情况生成最快的代码是一项正在进行的工作,因此您可能需要尝试不同的方法,以查看哪种方法最快。