NEON码农指导 Chapter 4 : NEON Intrinsics

Translated from 《NEON Programmer’s Guide》

翻译可能有偏差,描述可能有错误,请以原著为准


Chapter 1 : Introduction
Chapter 2 : Compiling NEON Instructions
Chapter 3 : NEON Instruction Set Architecture


  本章介绍了ARM编译器工具链如何提供内部函数(intrinsics),为所有处于ARM和Thumb状态的Cortex-A系列处理器生成NEON代码。


1. Introduction

简介

  内部函数(intrinsics)由ARM编译器工具链提供,用intrinsics来写NEON代码,比写汇编程序更容易维护,同时仍然可以控制生成哪些NEON指令。

  NEON的intrinsics像一种函数调用,编译器会用适当的NEON指令或NEON指令序列来替换它。但是你必须优化你的代码,以充分利用NEON带来速度的提高。

  这里有一些数据类型来对应NEON的寄存器(包括D寄存器和Q寄存器),包括不同size的元素。然后你可以创建一些C语言的变量直接映射到NEON寄存器,这些变量会传到NEON intrinsics函数里。编译器会直接生成NEON指令,而不是执行一个函数调用。

  intrinsics的函数和数据类型,以及简短形式的intrinsics,为C / C++代码访问低层的NEON功能提供了支持。软件可以传递NEON向量,就像处理函数参数或者返回值那样。也可以像一般变量那样声明NEON向量。

  intrinsics提供了绝大多数控制,和写汇编差不多,但是把寄存器的分配留给编译器来做,这样你就可以专注于你的算法了。另外,编译器会像普通C / C++代码那样优化intrinsics,有可能的话,会把你写的代码替换成更高效的序列。

  NEON的intrinsics都在arm_neon.h头文件里面定义了,里面也包含了向量类型。

注意:
在ARMv7架构之前是不支持NEON指令集的。在早期的架构上面编译,或者在不包含NEON单元的ARMv7(有的处理器虽然是ARMv7架构,但是不带NEON)架构上编译,编译器会把NEON intrinsics当成普通的函数调用,这样会导致错误。


2. Vector data types for NEON intrinsics

简介

  NEON向量数据类型根据以下模式命名:
x_t

  例如:

  • int16x4_t是一个向量,其中包含4个16位的短整型变量(4个lane,每个lane存16位的数)。
  • float32x4_t是一个向量,其中包含4个32位的浮点型变量(4个lane,每个lane存32位的数)。

下表列出了向量数据类型:

表1
64位类型(D寄存器) 128位类型(Q寄存器)
int8x8_t int8x16_t
int16x4_t int16x8_t
int32x2_t int32x4_t
int64x1_t int64x2_t
uint8x8_t uint8x16_t
uint16x4_t uint16x8_t
uint32x2_t uint32x4_t
uint64x1_t uint64x2_t
float16x4_t float16x8_t
float32x2_t float32x4_t
poly8x8_t poly8x16_t
poly16x4_t poly16x8_t

  你可以用上述向量类型来指定intrinsics的输入和输出。一些intrinsics可以用向量类型的数组,它们组合了2个、3个或者4个相同的向量类型:
xx_t

  这些类型是原始的C结构体,包含一个叫做val的元素。
  这些类型映射到<被NEON加载和存储时所访问的>寄存器上,以实现用一条指令加载 / 访问多达4个寄存器。
  该结构体的一个定义示例如下:

struct int16x4x2_t
{
    int16x4_t val[2];
} <var_name>;

  这些类型仅在加载、存储、转置(transpose)、交错(interleave)、反交错(de-interleave)指令中使用,为了对实际数据执行操作,从单个寄存器中选取元素,例如.val[0].val[1]

  有的数组类型长度为2~4,其中包含的向量类型都列在表1了。

注意:
向量数据类型向量数据类型的数组不能通过直接文本赋值初始化。您可以使用一个‘加载’ intrinsics 或使用vcreate intrinsics来初始化它们。


3. Prototype of NEON Intrinsics

NEON intrinsics 的原型

   intrinsics使用与NEON统一汇编语法类似的命名规则:
_

   它提供了一个额外的标志q来指示intrinsics操作128位的向量。

例如:

  • vmul_s16将两个有符号16位向量的值相乘。
    这会被编译成VMUL.I16 d2, d0, d1
  • vaddl_u8是一个长指令,将两个包含无符号8位值的64位向量相加。结果是128位的向量,内含无符号16位的值。
    这会被编译成VADDL.U8 q1, d0, d1

   除了指令中写到的寄存器,别的寄存器也可能会被用到,这要看编译器怎么优化。它甚至会以某些方式得到不同的编译结果,以便提高性能。

注意
那些用到__fp16intrinsics,仅在拥有NEON半精度VFP扩展的平台上可用。
要开启__fp16的话,要在命令行输入--fp16_format选项,详情见ARM编译器工具链的文档。


4. Using NEON intrinsics

使用NEON intrinsics

   ARM编译工具链定义了intrinsics,详情在arm_neon.h头文件里,表4.1中的向量类型也包含在内。

   intrinsics是ARM ABI的一部分,因此可以在ARM编译工具链和GCC之间移植。

   带q后缀的intrinsics通常操作于Q寄存器, 不带q后缀的intrinsics通常操作于D寄存器, 然而有一些还是用的Q寄存器。

   下面的例子展示了一个intrinsics,带有两个不同变量:
   uint8x8_t vadd_u8(uint8x8_t a, uint8x8_t b);
   这个vadd_u8不含q后缀,在此例中,输入输出向量都是64位向量,用的是D寄存器。

   uint8x16_t vaddq_u8(uint8x16_t a, uint8x16_t b);
   上面这个vaddq_u8带有q后缀,因此输入和输出向量都是128位,用了Q寄存器。

   uint16x8_t vaddl_u8(uint8x8_t a, uint8x8_t b);
   上面这个vaddl_u8不含q后缀,输入向量是64位,输出向量是128位。

   一些NEON intrinsics使用32位ARM通用寄存器作为输入参数来保存标量值。例如有的intrinsics从一个向量提取单个值(vget_lane_u8)、设置向量的某个通道(vset_lane_u8)、从一个字面的值创建一个向量(vcreate_u8)、把向量的所有通道设置到某个值(vdup_n_u8)。

   对每种类型使用不同的intrinsics,会导致很难在不兼容的类型之间进行突发性操作,因为编译器会对“哪些寄存器持有哪些类型”保持追踪。

   编译器同样会重新安排程序流,使用可选的更快的指令集。不能保证生成的指令集一定与intrinsic所指示的指令集相匹配。这在从一个微体系结构转移到另一个微体系结构的时候特别有用。

   例1的代码展示了一个函数,用一个4通道(每个通道是32位无符号整形)的向量作为输入参数,返回一个值(所有通道都被加倍了)。

#include 
uint32x4_t double_elements(uint32x4_t input)
{
    return(vaddq_u32(input, input));
}
例1

   例2是从例1生成的反汇编代码,该代码(指例1)是为hard float ABI编译的。double_elements()函数转换成一条NEON指令和一个返回序列。

double_elements  PROC
VADD.I32  q0,q0,q0
BX    lr
ENDP
例2

   例3也是例1的反汇编版本,该代码(指例1)是为software linkage编译的。在这种情况下,在使用前必须把参数从ARM通用寄存器拷贝到一个NEON寄存器。完成计算后,再把返回值从NEON寄存器拷贝到ARM通用寄存器。

double_elements  PROC
VMOV  d0, r0, r1
VMOV  d1, r2, r3
VADD.I32  q0, q0, q0
VMOV  r0, r1, d0
VMOV  r2, r3, d1
BX lr
ENDP
例3

   GCC和armcc支持相同的intrinsic,因此用NEON intrinsic编写的代码在工具链之间是完全可移植的。必须在每个使用intrinsic的源文件中包含arm_neon.h头文件,并且必须指定命令行选项。

   使用intrinsic优化源模块是很有用的,就算处理器没有实现NEON技术,也可以为它编译。__ARM_NEON__宏是GCC定义的,在为实现可NEON技术的目标编译的时候定义。RVCT 4.0 build 591或更高版本,以及ARM编译器工具链也定义了这个宏。软件可以使用这个宏来提供文件中的<优化过的或者纯C/C++版本的>函数,这些函数由传递给编译器的命令行参数选择。

   对于intrinsic函数和向量数据类型的更多信息,请查看《ARM Compiler toolchain Compiler Reference Guide》,GCC文档。


5. Variables and constants in NEON code

NEON代码里的变量和内容

   本节是一些例子,用NEON代码访问变量数据和常量数据。


5.1 Declaring a variable

声明变量

   声明一个新的变量,和C代码里面的一样简单:

uint32x2_t vec64a, vec64b;  // create two D-register variables

5.2 Using constants

使用常量

   使用常量很简单,下面的代码把一个常量复制到一个向量的每个元素里:

uint8x8 start_value = vdup_n_u8(0);

   要将一般64位常量加载到向量中,请使用:

uint8x8 start_value = vreinterpret_u8_u64(vcreate_u64(0x123456789ABCDEFULL));

5.3 Moving results back to normal C variables

把结果返回到普通的C变量

   若要从NEON寄存器访问结果,请用VST把它保存到内存中,或者用 “get lane” 类型的操作把它移回ARM寄存器。

result = vget_lane_u32(vec64a, 0); // extract lane 0

5.4 Accessing D registers from a Q register

从Q寄存器中访问D寄存器

   用vget_lowvget_highD寄存器访问Q寄存器。

vec64a = vget_low_u32(vec128); // split 128-bit vector 
vec64b = vget_high_u32(vec128); // into 2x 64-bit vectors

5.5 Casting NEON variables between different types

在不同的NEON变量类型之间转换

   NEON的intrinsic是强类型的,在不同的向量类型之间必须显式转换。在D寄存器上用vreinterpret来转换,在Q寄存器上用vreinterpretq。这些intrinsic不生成任何代码,只是让你可以转换NEON类型。

uint8x8_t byteval;
uint32x2_t wordval;
byteval = vreinterpret_u8_u32(wordval);

uint8x16_t byteval2;
uint32x4_t wordval2;
byteval2 = vreinterpretq_u8_u32(wordval2);

注意:
输出类型u8是在vreinterpret后面列出来的,在输出类型u32之前。


6. Accessing vector types from C

从C访问向量类型

  必须包含arm_neon.h头文件,才能用intrinsic和C风格的类型进行向量操作。C类型以下面的形式编写:

  • uint8x16_t:这是一个向量,包含无符号8位整形。向量中有16个元素,因此该向量必须用128位Q寄存器
  • int16x4_t:这是一个向量,包含有符号16位整形,向量中有4个元素,因此向量必须在64位的D寄存器中。

  由于ARM标量类型和NEON向量类型之间是不兼容的,就算它们之间具有相同的位长,也无法把标量分配到向量中。标量值和指针只能和直接使用标量的NEON指令一起使用。

  例如,要从NEON向量的通道0取出无符号的32位整数,用:

result = vget_lane_u32(vec64a, 0)

  在armcc中,除了赋值以外,不能用标准C运算符来操作向量类型。因此用VADDintrinsic 更为合适,而不是用操作符+。但是,GCC允许标准C运算符对NEON向量类型进行操作,从而使代码更具可读性。

  如果向量类型只是元素的数量不同(uint32x2_t, uint32x4_t),有专门的指令将128位值的上半部分或者下半部分的向量元素分配给64位寄存器,反之亦然。如果可以将寄存器安排为别名(scheduled as aliases),则此操作不使用任何代码空间。要用128位寄存器的低64位,如下:

vec64 = vget_low_u32(vec128);

7. Loading data from memory into vectors

从内存加载到向量

  本节介绍如何使用NEON intrinsic 创建向量。内存中的连续数据可以加载到单个向量或多个向量, NEON intrinsic 干这件事的是vld1_datatype。 例如,要加载一个向量(4个16位无符号数据),请使用NEON intrinsic vld1_u16

  下面的例子中,数组A包含8个16位元素,此例展示了如何把数据从这个数组加载到一个向量中。

#include 
#include 
unsigned short int A[ ] = {1,2,3,4}; // array with 4 elements
int main(void)
{
    uint16x4_t v;       // declare a vector of four 16-bit lanes
    v = vld1_u16(A);    // load the array from memory into a vector
    v = vadd_u16(v,v);  // double each element in the vector
    vst1_u16(A, v);     // store the vector back to memory
    return 0;
}

  你可以查看intrinsic的说明(在文档附录),看看vldl_datatype可以用哪些datatype


8. Constructing a vector from a literal bit pattern

从字面位模板创建向量

  你可以从字面值来创建向量,NEON的intrinsicvcreate_datatype来做这件事。例如你想要加载一个向量(8个无符号8位数据),你可以用intrinsicvcreate_u8。下面例子展示了如何从字面数据创建一个向量。

#include 
int main (void)
{
    uint8x8_t v;  // define v as a vector with 8 lanes of 8-bit data
    unsigned char A[8];  // allocate memory for eight 8-bit data
    v = vcreate_u8(0x0102030405060708);  // create a vector that contains the values 1,2,3,4,5,6,7,8
    vst1_u8(A, v);  // store the vector to memory, in this case, to array A
    return 0;
}

  更多关于vcreate_datatype支持的类型请看文档附录。


9. Constructing multiple vectors from interleaved memory

从交错的内存中构建多个向量

  在内存中的数据经常是交错(interleaved)的,NEONintrinsic支持2路,3路,4路交错模式。

  例如内存的一块区域中可能包含立体声数据,左右通道的数据是交错的,这是一个2路交错模式的例子。

  还有一个例子:内存中包含24位RGB图像,它是从红、绿、蓝通道3路交错的8位数据。当内存中包含交错数据的时候,“解交错(de-interleaving)”可以让你加载一个向量存储所有红色值,一个单独的向量存储所有绿色值,一个单独的向量存储所有蓝色值。

  NEON 解交错的 intrinsicvldn_datatype,其中n表示交错模式,可以是234,如果你想把24位RGB图像解交错到3个不同的向量中,可以用vld3_u8。下面看看怎么做:

#include 
int main (void)
{
    uint8x8x3_t v; // This represents 3 vectors. 
                   // Each vector has eight lanes of 8-bit data.
    unsigned char A[24]; // This array represents a 24-bit RGB image. 
    v = vld3_u8(A); // This de-interleaves the 24-bit image from array A 
                    // and stores them in 3 separate vectors
                    // v.val[0] is the first vector in V. It is for the red channel
                    // v.val[1] is the second vector in V. It is for the green channel
                    // v.val[2] is the third vector in V. It is for the blue channel.

    v.val[0] = vadd_u8(v.val[0],v.val[0]);  // Double the red channel
    vst3_u8(A, v);  // store the vector back into the array, with the red channel doubled.
    return 0;
}

  更多关于vldn_datatype intrinsic的类型看文档附录的Load and store部分。


10. Loading a single lane of a vector from memory

从内存中加载单个通道的向量

  如果要从分散在内存中的数据构造向量,则必须使用单独的 intrinsic 分别加载每个通道。

  实现这个的NEON intrinsicvld1_lane_datatype。例如你要加载向量的一个通道(8位无符号数据),要用vld1_lane_u8。下面的例子展示了怎样从内存中加载向量的单个通道:

#include 
#include 
????(文档里面没东西)

11. Programming using NEON intrinsics

编程

  直接在汇编器中或通过 intrinsic 函数接口编写NEON代码,需要对使用的数据类型以及可用的NEON指令有透彻的了解。要知道要使用什么NEON运算,查看如何将算法拆分为并行运算很有用。从SIMD的角度来看,交换操作(例如加,最小和最大)特别容易。

  数组中的8个成员相加:

unsigned int acc=0;

for (i=0; i<8; i+=1)
{
    acc += array[i]; // a + b + c + d + e + f + g + h
}

  利用加法的结合律,可以把这个循环展开成几个加法((a+e) + (b+f)) + ((c+g) + (d+h))

unsigned int acc1=0; 
unsigned int acc2=0; 
unsigned int acc3=0; 
unsigned int acc4=0; 

for (i=0; i<8; i+=4)
{
    acc1 += array[i]; // (a, e)
    acc2 += array[i+1]; // (b, f)
    acc3 += array[i+2]; // (c, g)
    acc4 += array[i+3]; // (d, h)
}

acc1+=acc2; // (a + e) + (b + f)
acc3+=acc4; // (c + g) + (d + h)
acc1+=acc3; // ((a + e) + (b + f))+((c + g) + (d + h))

  上面的代码展示了一个向量持有4个32位值,用于累加器和临时寄存器。假定相加的数组元素是在32位通道的,然后可以使用SIMD指令进行操作。 将代码扩展为4的任意倍数:

#include 

uint32_t vector_add_of_n(uint32_t* ptr, uint32_t items)
{
    uint32_t result,* i;
    uint32x2_t vec64a, vec64b;
    uint32x4_t vec128 = vdupq_n_u32(0); // clear accumulators
    
    for (i=ptr; i<(ptr+(items/4)); i+=4)
    {
        uint32x4_t temp128 = vld1q_u32(i); // load four 32-bit values
        vec128=vaddq_u32(vec128, temp128); // add 128-bit vectors
    }
    
    vec64a = vget_low_u32(vec128); // split 128-bit vector
    vec64b = vget_high_u32(vec128); // into two 64-bit vectors
    vec64a = vadd_u32(vec64a, vec64b); // add 64-bit vectors together
    
    result = vget_lane_u32(vec64a, 0); // extract lanes and
    result += vget_lane_u32(vec64a, 1); // add together scalars
    return result;
}

  vget_high_u32vget_low_u32与任何NEON指令都不相似。这些 intrinsic 指示编译器从输入Q寄存器中引用D寄存器的高位或者低位。因此,这些操作不会转换为实际的代码,但是会影响用于存储vec64avec64b的寄存器。

  根据编译器的版本,目标处理器和优化选项,生成的代码如下:

vector_add_of_n PROC
    VMOV.I8    q0,#0
    BIC        r1,r1,#3
    ADD        r1,r1,r0
    CMP        r1,r0
    BLS        |L1.36|
|L1.20|
    VLD1.32    {d2,d3},[r0]!
    VADD.I32   q0,q0,q1
    CMP        r1,r0
    BHI        |L1.20|
|L1.36|
    VADD.I32   d0,d0,d1
    VMOV.32    r1,d0[1]
    VMOV.32    r0,d0[0]
    ADD        r0,r0,r1
    BX         lr
    ENDP

12. Instructions without an equivalent intrinsic

一些指令没有对等的 intrinsics

  大多数的NEON指令都有等效的intrinsics,但是下面的没有:
VSWPVLDMVLDRVMRSVMSRVPOPVPUSHVSTMVSTRVBITVBIF

注意:
无法明确生成VBIFVBIT,但是 intrinsics VBSL可以生成任意VBSLVBIT或者VBIF指令。

  VSWP指令没有对应的intrinsics,因为编译器可以在需要的时候生成VSWP指令,例如用C风格变量分配来交换变量的时候。

  VLDMVLDRVSTMVSTR主要用于上下文切换,这些指令具有对齐约束。 编写 intrinsics 时,使用vldx intrinsics更简单。除非明确指定,否则vldx intrinsics不需要对齐。

  VMRSVMSR访问NEON的条件标志,数据处理的时候不需要用NEON intrinsics

  VPOPVPUSH用于传递给函数的参数中。减少变量重用,或使用更多NEON intrinsics 变量,可使寄存器分配器保持对活动的寄存器的跟踪。

你可能感兴趣的:(Neon)