NEON码农指导 Chapter 2 : Compiling NEON Instructions

Translated from 《NEON Programmer’s Guide》

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


  本章介绍了如何使用C或汇编,写针对NEON硬件的代码。以及一些工具和库,用来支持此功能。

1. Vectorization

  向量化

  NEON被设计成一种附加的加载/存储体系结构,以提供对C / C ++等语言的良好向量化编译器支持。这实现了高度的并行性,可以手动编码NEON指令,实现非常高性能的应用程序。它包括低成本升级和降低数据大小,还包括结构化的加载能力,可以访问那些交错在内存的多个数据流。

  可以把NEON当做常规ARM代码的一部分来写。与使用外部硬件加速器相比,这使NEON编程更简单,更高效。NEON指令可用于读取和写入外部存储器,在NEON寄存器和其他ARM寄存器之间移动数据,以及执行SIMD操作。

  所有已编译的代码和子例程都将遵循EABI(Embedded Application Binary Interface),EABI指定了哪些寄存器可能损坏,以及哪些寄存器必须保留。

  向量化编译器可以获取您的C或C ++源代码,并将其向量化,从而可以有效利用NEON硬件。 这意味着您可以编写可移植的C代码,同时仍然获得NEON指令所能实现的性能水平

  为了辅助向量化,请将循环迭代的次数设为向量长度的倍数。 GCC和ARM编译器工具链都有选项,可以为NEON技术启用自动向量化。但由于C和C ++标准未涵盖并发方面,因此要向编译器提供其他信息,才能得到NEON的好处。 修改的源代码也是是标准语言规范的一部分,因此不会影响代码在平台和体系结构之间的可移植性。

  当向量化编译器可以确定程序员的意图时,其效果最佳。 相比针对特定处理器进行了高度优化的代码,人们能够理解的简单代码更容易向量化。


1.1 Enabling auto-vectorization in ARM Compiler toolchain

  在ARM交叉编译链中启动自动向量化

  DS-5™Professional中的ARM编译器工具链包括对向量化编译器的支持。 要启用自动向量化,您必须针对具有NEON单元的处理器。 所需的命令行选项是:

  • --vectorize ,启动向量化
  • --CPU 7A--Cpu Cortex A8 ,选择指定的架构和核,支持NEON
  • -O2-O3,选择高级或者低级的优化
  • -Otime ,为速度做优化(而不针对空间)

  使用armcc命令行参数--remarks可提供有关执行优化的更多信息,或提供有关编译器无法执行某些优化的问题。


1.2 Enabling auto-vectorization in GCC compiler

  在GCC编译其中启动自动向量化

  为了在GCC中启动自动向量化,用下面的命令行选项:

  • -ftree-vectorize
  • -mfpu=neon
  • -mcpu ,指定内核或者架构

  用优化级别-O3编译,相当于-ftree-vectorize

  如果没有指定-mcpu选项,则GCC将使用内置的默认内核,生成的代码可能运行缓慢或根本不运行。

  在许多支持SIMD操作的架构上,-ftree-vectorize是可用的。


1.3 C pointer aliasing

  C指针别名

  优化标准C(ISO C90)的主要挑战是,你可以解引用指针,而这个指针可能(根据标准)指向相同或重叠数据集。这会导致什么问题?后面的例子有讲。

  随着C标准的发展,通过在C99和C++中添加关键字limit来解决此问题。在指针声明中添加限制是一种保证,只有该指针才能访问其指向的地址(就像unique_ptr智能指针)。 这使编译器可以进行设置和退出限制方面的工作,可以在事先通知的情况下预加载数据,并缓存中间结果。

  ARM编译器允许在所有模式下使用关键字__restrict。如果指定命令行选项--restrict,则可以使用不带下划线的关键字restrict。GCC有类似的选择,有关更多信息,请参见GCC文档。


1.4 Natural types

  自然类型

  由于历史原因、内存大小、外围因素等,算法常被设计用于处理某种类型数据。通常将这些类型转换成处理器的自然类型,因为对这种大小的数据进行数学运算通常会更快,并且仅在传递或存储数据时才计算截断和溢出。


1.5 Array grouping

  数组分组

  处理器中用于存储内存指针的寄存器很少(例如x86),对于这样的处理器,通常将多个数组组合起来。 然后用一个指针,通过指针位移来访问不同部分的数据。这会让编译器误以为偏移指针导致数据重叠。除非您可以保证不对阵列进行任何写操作,才能避免这种情况。将复合数组拆分为单个数组,以简化指针的使用能消除这种风险。


1.6 Inside knowledge

  内部知识

  要把没有size信息的数组转换为NEON代码,编译器必须假定size可能在0到4GB之间。 在没有其他信息的情况下,编译器必须生成设置代码(用于测试数组是否太小而无法使用整个NEON寄存器),以及用于清理的代码(使用标量管道(pipeline)来消耗数组中的最后几项)。

  在某些情况下,数组大小在编译时是已知的,应直接指定而不是作为参数传递。在其他情况下,工程师通常比编译器更了解矩阵的布局。例如,数组通常表示为2的幂。程序员可能知道循环迭代计数将始终是2、4、8、16、32的倍数,依此类推。可以编写代码来利用它。


1.7 Enabling the NEON unit in bare-metal applications

  在裸机应用中启用NEON单元

  裸机应用程序是直接在硬件上运行的应用程序,无需内核或操作系统支持。
  默认情况下,NEON单元在重置时处于禁用状态,因此对于需要NEON指令的裸机应用,必须手动启用它。EnableNEON代码段显示了如何手动启用NEON设备。

// hello.c:

#include 
// Bare-minimum start-up code to run NEON code
__asm void EnableNEON(void)
{
    MRC p15,0,r0,c1,c0,2 // Read CP Access register
    ORR r0,r0,#0x00f00000 // Enable full access to NEON/VFP by enabling access to Coprocessors 10 and 11
    MCR p15,0,r0,c1,c0,2 // Write CP Access register
    ISB
    MOV r0,#0x40000000 // Switch on the VFP and NEON hardware
    MSR FPEXC,r0 // Set EN bit in FPEXC
}

  为带有NEON单元的处理器编译裸机应用程序时,编译器可能会使用NEON指令。例如,ARM编译器工具链armcc默认情况下使用-O2优化,如果指定了-Otime--vectorize选项,则它会尝试使用NEON单元对处理器的代码进行向量化。

  可以像这样把上面的代码编译成裸机应用程序:

armcc -c --cpu=Cortex-A8 --debug hello.c -o hello.o
armlink --entry=EnableNEON hello.o -o hello.axf

1.8 Enabling the NEON unit in a Linux stock kernel

  在原版Linux内核中启用NEON单元

  原版的内核在Linux在www.kernel.org上发布,没有经过修改。如果您使用原版的Linux内核来运行应用程序,则无需手动启用NEON单元。内核在遇到第一条NEON指令时自动启用NEON单元。

  如果NEON单元被禁用,而应用程序尝试执行NEON指令,它将抛出<未定义的指令>异常。内核使用这个异常来启用NEON单元,然后执行NEON指令。NEON单元保持启用状态,直到有上下文切换。当需要上下文切换时,内核可能会禁用NEON单元以节省时间和电源。


1.9 Enabling the NEON unit in a Linux custom kernel

  在自定义的Linux内核中启用NEON单元

  如果使用修改过的的Linux内核运行应用程序,则必须启用NEON单元。要启用NEON单元,必须使用内核配置设置来选择:

  • Floating point emulation → VFP-format floating point maths
  • Floating point emulation → Advanced SIMD (NEON) Extension support

图1显示了通用快速板的配置设置。

图1 定制Linux内核的设置

NEON码农指导 Chapter 2 : Compiling NEON Instructions_第1张图片

  如果存在/proc/config.gz,则可以使用以下命令在内核中测试对NEON的支持:

zcat /proc/config.gz | grep NEON

  如果存在NEON单元,命令会输出:

CONFIG_NEON=y

  为了确保处理器支持NEON扩展,可以使用命令:

cat /proc/cpuinfo | grep neon

  如果它支持NEON扩展,则输出:

Features : swp half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt

1.10 Optimizing for vectorization

  向量化优化

  C和C++语言没有提供指定并发行为的语法,因此编译器无法安全地生成并行代码。但是,开发人员可以提供其他信息,让编译器知道在什么地方可以安全地进行向量化。

  与内部函数(intrinsics)不同,这些修改不依赖于体系结构,并且可能会改善任何目标平台上的向量化。在那些无法进行向量化的平台上,这些修改不会对性能造成负面影响。

  下面描述主要规则:

  • 短、简单的循环效果最好(即使代码中有多个循环)
  • 避免用break来退出循环
  • 把迭代次数设为2的幂
  • 让编译器能明确迭代次数
  • 在一个循环内部调用的函数,应该用内联(inlined)函数
  • 在数组中,用索引好过用指针
  • 间接寻址(多个索引或解引用)不会向量化。
  • 使用restrict关键字告诉编译器,让指针不引用内存的重叠区域。

Indicate knowledge of number of loop iterations

关于循环迭代次数的知识

  如果循环具有固定的迭代计数,或者您知道迭代计数始终为2的幂,则对于编译器而言显而易见的是,这使编译器能够执行优化,否则该优化是不安全的或困难的。

  下面的代码显示了一个函数,该函数计算int大小的元素个数(len)。如果您知道传进来的参数len始终是4的倍数,在比较循环次数的时候,将最低两位置零(len & ~3),通过这种方式向编译器指示这一点。这样可以确保循环始终执行4的倍数次。这样编译器可以安全地向量化,与此同时:

  • 无需为了在运行时检查len而添加代码。
  • 无需添加代码来处理悬空的迭代。
//  指定循环计数器是4的倍数
int accumulate(int * c, int len)
{
    int i, retval;
    for(i = 0, retval = 0; i < (len & ~3) ; i++)  // 
    {
        retval = retval + c[i]
    }
    return retval;
}
例1 指定循环计数器为4的倍数

Avoid loop-carried dependencies

避免循环携带的依赖

  如果您的代码包含一个循环,某次循环受到上一次循环结果的影响,则编译器无法对其进行向量化。最好重新检查代码,消除这种循环中的依赖。


Use the restrict keyword

使用restrict关键字

  C99引入了restrict关键字,可用于通知编译器,通过特定指针访问的位置,不能通过当前作用域内的任何其他指针访问。
  下面的代码展示了一种情况,其中e指向要更新位置,对这个指针使用restrict会使向量化变得安全,否则就不安全。

int accumulate2(char * c, char * d, char * restrict e, int len)
{
    int i;
    for(i=0 ; i < (len & ~3) ; i++)
    {
        e[i] = d[i] + c[i];
    }
    return i;
}
例2

  如果没有restrict关键字,则编译器会假定e[i]可以引用与d[i+1]相同的位置,这可能会产生循环依赖关系,从而阻止它对向量序列进行向量化处理。程序员可以用restrict告诉编译器,通过e访问的任何位置只能通过此函数中的指针e访问。这意味着编译器可以忽略混叠的可能性,并可以对序列进行向量化处理。

  使用restrict关键字不能使编译器对函数调用执行其他检查。因此,如果传递给函数的cd值与e的值重叠,则向量化代码可能无法正确执行。

  编译的不是C99标准的代码时,GCC和ARM编译器工具链都支持替代形式__restrict____restrict。 当在编译命令行指定了--restrict时,ARM编译工具链还支持在C90和C++中使用restrict关键字。


Avoid conditions inside loops

避免循环内的条件

  通常,编译器无法向量化包含条件语句的循环。在最佳情况下,它会复制循环,但在许多情况下根本无法向量化。


Use suitable data types

使用合适的数据类型

  当在没有SIMD的情况下,优化某些对16位或8位数据运行的算法时,将它们当做32位变量来用,有时会获得更好的性能。在写自动地向量化的软件时,为了获得最佳性能,请始终使用可容纳所需值的最小数据类型。这样NEON寄存器可以容纳更多数据元素,并并行执行更多操作。在给定的周期内,NEON单元可以处理的8位值是16位值的两倍。

  另外,NEON技术不支持某些数据类型,而某些操作仅支持某些数据类型。例如,它不支持双精度浮点数,因此在单精度浮点数足够的情况下,使用双精度数可能会阻止编译器对代码进行向量化处理。NEON技术仅对某些操作支持64位整数,因此请尽可能避免使用long long变量。

注意

  NEON技术包括一组可以执行结构化加载和存储操作的指令。这些指令只能用于向量访问所有成员大小均相同的数据结构。


Floating-point vectorization

浮点向量化

  浮点运算可能会导致精度降低。可以再排列一下操作的顺序或浮点输入的数据,以减少精度损失。更改操作或输入的顺序可能会导致精度进一步下降。 因此,默认情况下不会对某些浮点运算进行向量化,因为向量化会更改操作的顺序,导致程序逻辑的改变。如果算法不需要这种精度水平,则可以在命令行上为armcc指定--fpmode=fast或为GCC指定-ffast-math以启用这些优化。

  下面展示了一段代码,使用上面其中一个命令行参数来进行向量化。在这种情况下,它将执行并行累加,可能会降低结果的精度。

float g(float const *a)
{
	float r = 0;
	int i;
	for (i = 0 ; i < 32 ; ++i)
		r += a[i];
	return r;
}
例3

  NEON单元始终以“对齐到零(Flush-to-zero)”模式运行,导致不符合IEEE754。默认情况下,armcc使用--fpmode=std以符合标准。 但是,如果命令行参数指定要求符合IEEE 754的模式选项,例如--fpmode=ieee_full,则大多数浮点操作无法向量化。


2. Generating NEON code using the vectorizing compiler

使用向量化编译器生成NEON代码

  向量化编译器会预估可向量化的循环,以及潜在的NEON应用。如果你写的C或C ++代码使编译器可以确定代码的意图,则编译器将更有效地对其进行优化。尽管编译器可以在不修改源代码的情况下生成一些NEON代码,但是某些编码样式可以促进更佳的输出。当向量化器(vectorizer)发现代码具有潜在的向量化机会,但没有足够的信息时,它可以为用户生成注释,以提示用户对源代码进行更改,以提供更多有用的信息。尽管这些修改能帮助向量化编译器,但它们都是标准C表示法,并允许使用任何符合C99*的编译器进行重新编译。解析restrict关键字需要用到C99。在其他编译模式下,armcc还允许使用等效的ARM特定扩展名__restrict


2.1 Compiler command line options

编译器命令行选项

  如果有向量化许可证选项,可以通过使用O2O3Otimevectorizecpu选项告诉编译器生成NEON代码。cpu选项必须指定具有NEON硬件的处理器。

  由于数组清理和其他开销,SIMD代码有时大于等效的ARM代码。

  要在Cortex-A8目标上生成快速NEON代码,应使用以下命令行:

armcc --cpu=Cortex-A8 -O3 -Otime --vectorize ...

  如果您没有向量化编译器的许可证,则此命令将回复一条错误消息。


Using the vectorizing compiler

使用向量化编译器

我们可以简洁地编写C代码:

/* file.c */
unsigned int vector_add_of_n(unsigned int* ptr, unsigned int items)
{
    unsigned int result=0;
    unsigned int i;
    
    for (i=0; i<(items*4); i+=1)
    {
        result += ptr[i];
    }
    
    return result;
}

注意

  • 通过使用(items*4),我们告诉编译器数组的大小是4的倍数,这种就属于有效信息。尽管向量化程序不需要创建NEON代码,但它为编译器提供了有关数组的更多信息。在这种情况下,它知道该数组可以与向量数组一起使用,并且不需要任何额外的标量代码来处理任何备用项的清理。
  • 传递给函数的值items值必须是数组中实际项目数的 1 / 4 1/4 1/4,例如:
vector_add_of_n(p_list, item_count/4);

  用以下命令编译上述代码:

armcc --cpu=Cortex-A8 -O3 –c -Otime –-vectorize file.c

  使用命令fromelf –c file.o查看生成的文件:

vector_add_of_n PROC
LSLS r3,r1,#2
MOV r2,r0
MOV r0,#0
BEQ |L1.72|
LSL r3,r1,#2
VMOV.I8 q0,#0
LSRS r1,r3,#2
BEQ |L1.48| |L1.32|
VLD1.32 {d2,d3},[r2]!
VADD.I32 q0,q0,q1
SUBS r1,r1,#1
BNE |L1.32| |L1.48|
CMP r3,#4
BCC |L1.72|
VADD.I32 d0,d0,d1
VPADD.I32 d0,d0,d0
VMOV.32 r1,d0[0]
ADD r0,r0,r1 |L1.72|
BX lr

  尽管这代码比手写的代码长,但例程的主要部分(内部循环)的长度相同,并且包含相同的指令。 这意味着,如果数据集大小合理,则执行的时间差很小。但是在Cortex-A8处理器上,与手写代码相比,编译器生成的代码是非最佳计划的。因此,Cortex-A8处理器的性能差异随数据集大小而定。


3. Vectorizing examples

使用向量化编译器生成NEON代码


3.1 Vectorization example on unrolling addition function

展开附加功能的向量化示例

  考虑下面的代码:

Void add_int (int * __restrict pa, int * __restrict pb, unsigned int n, int x)
{
    unsigned int i;
    for (i = 0; i < (n & ~3); i++)
        pa[i] = pb [i] + x;
}

1.分析每次循环:

  • 指针访问对向量化安全吗?
  • 正在使用什么数据类型,它们如何映射到NEON寄存器?
  • 有多少个循环迭代?

2.将循环展开到适当的迭代次数,然后执行其他转换,例如使用指针。
NEON码农指导 Chapter 2 : Compiling NEON Instructions_第2张图片

3.将每个展开的操作映射到NEON向量通道上,并生成相应的NEON指令。


3.2 Vectorizing example with vectorizing compilation

向量化示例与向量化编译

  下表显示了使用向量化编译和不使用向量化编译的代码的比较:
NEON码农指导 Chapter 2 : Compiling NEON Instructions_第3张图片

3.3 Vectorizing examples with different command line switches

用不同的命令行,对例子进行向量化

  本节包含非常简单的编译器向量化例子。 这些示例显示了各种命令行开关的影响以及各种源代码更改的影响。


Optimized for code space

针对代码空间进行了优化

  在此示例中,编译器选项-Ospace生成较小的代码, 生成的代码未针对速度进行优化。

void add_int (int *pa, int * pb, unsigned int n, int x)
{
    unsigned int i;
    for(i = 0; i < n; i++)
        pa[i] = pb[i] + x;
}
例4 优化代码空间

  附带下面的选项来编译:

armcc --cpu=Cortex-A8 -O3 -Ospace

  汇编结果:

add_int PROC
    PUSH {r4, r5, 1r}
    MOV r4, #0
|L0.8|
    CMP r4,r2
    LDRCC r5, [r1, r4, LSL, #2]
    ADCC r5, r5, r3
    STRCC r5, [r0, r4, LSL, #2]
    ADCC r4,r4, #1
    BCC |L0.8|
    POP {r4, r5, pc}
例5 生成的汇编语言

Optimized for time

针对时间优化

  在此示例中,编译器选项-Otime生成代码, 结果比例4的代码更快更长。

void add_int (int *pa, int * pb, unsigned int n, int x)
{
    unsigned int i;
    for(i = 0; i < n; i++)
        pa[i] = pb[i] + x;
}
例6 针对时间优化

  附带下面的选项来编译:

armcc --cpu=Cortex-A8 -O3 -Otime

汇编结果:

add_int PROC
    CMP r2,#0
    BXEQ lr
    TST r2,#1
    SUB r1,r1,#4
    SUB r0,r0,#4
    PUSH {r4}
    BEQ |L0.48|
    LDR r4,[r1,#4]!
    ADD r12,r0,#4
    ADD r4,r4,r3
    STR r4,[r0,#4]
    MOV r0,r12
|L0.48|
    LSRS r2,r2,#1
    BEQ |L0.96|
|L0.56|
    LDR r12,[r1,#4]
    SUBS r2,r2,#1
    ADD r12,r12,r3
    STR r12,[r0,#4]
    ADD r12,r0,#8
    LDR r4,[r1,#8]!
    ADD r4,r4,r3
    STR r4,[r0,#8]
    MOV r0,r12
    BNE |L0.56|
|L0.96|
    POP {r4}
    BX lr
例7 生成的汇编代码

Optimized using knowledge of array size

用已知数组大小来优化

  在此示例中,将for循环迭代范围设置为(n & ~3)。 这告诉编译器数组pa的大小是4的倍数。

void add_int (int *pa, int * pb, unsigned int n, int x)
{
    unsigned int i;
    for(i = 0; i < (n&~3); i++)
        pa[i] = pb[i] + x;
}
例8 用已知数组大小来优化

  附带下面的选项来编译:

armcc --cpu=Cortex-A8 -O3 -Otime

汇编结果:

add_int
    BICS r12,r12,#3
    BXEQ lr
    LSR r2,r2,#2
    SUB r1,r1,#4
    SUB r0,r0,#4
    LSL r2,r2,#1
    PUSH {r4}
|L0.28|
    LDR r12,[r1,#4]
    SUBS r2,r2,#1
    ADD r12,r12,#3
    STR r12,[r0,#4]
    ADD r12,r0,#8
    LDR r4,[r1,#8]!
    ADD r4,r4,r3
    STR r4,[r0,#8]
    MOV r0,r12
    BNE |L0.28|
    POP {r4}
    BX lr
例9 生成的汇编代码

Optimized using auto-vectorize and knowledge of array size

用自动向量化和已知数组大小来优化

  在此示例中,编译器选项--vectorize使编译器使用NEON指令VLD1VADDVST1

void add_int (int *pa, int * pb, unsigned int n, int x)
{
    unsigned int i;
    for(i = 0; i < (n&~3); i++)
        pa[i] = pb[i] + x;
}
例10

  附带下面的选项来编译:

armcc --cpu=Cortex-A8 -O3 -Otime --vectorize

汇编结果:

add_int
    PUSH r4,r5}
    SUB r4,r0,r1
    ASR r12,r4,#2
    CMP r12,#0
    BLE |L0.32|
    BIC r12,r2,#3
    CMP r12,r4,ASR #2
    BHI |L0.76|
|L0.32|
    BICS r12,r2,#3
    BEQ |L0.68|
    VDUP.32 q1,r3
    LSR r2,r2,#2
|L0.48|
    VLD1.32 {d0,d1},[r1]!
    SUBS r2,r2,#1
    VADD.I32 q0,q0,q1
    VST1.32 {do,d1},[r0]!
    BNE |L0.48|
|L0.68|
    POP {r4,r5}
    BX lr
|L0.76|
    BIC r2,r2,#3
    CMP r2,#0
    BEQ |L0.68|
    MOV r2,#0
    BLS |L0.68|
|L0.96|
    LDR r4,[r11,r2,LSL #2]
    ADD r5,r0,r2,LSL #2
    ADD r4,r4,r3
    STR r4,[r0,r2,LSL #2]
    ADD r4,r1,r2,LSL #2
    ADD r2,r2,#2
    LDR r4,[r4,#4]
    CMP r12,r2
    ADD r4,r4,r3
    STR r4,[r5,#4]
    BHI |L0.96|
    POP {r4,r5}
    BX lr
例11

Optimized using the restrict keyword

使用限制关键字进行了优化

  本例中,数组指针papb使用了关键字restrict

void add_int (int* restrict pa, int* restrict pb, unsigned int n, int x)
{
    unsigned int i;
    for(i = 0; i < (n&~3); i++)
        pa[i] = pb[i] + x;
}
例12

  附带下面的选项来编译:

armcc --cpu=Cortex-A8 -O3 -Otime --vectorize

汇编结果:

add_int PROC
    BICS r12,r2,#3
    BEQ |L0.36|
    VDUP.32 q1,r3
    LSR r2,r3,#2
|L0.16|
    VLD1.32 {d0, d1}, [r1]!
    SUBS r2,r2,#1
    VADD.I32 q0,q0,q1
    VST1.32 {d0,d1},[r0]!
    BNE |L0.16|
|L0.36|
    BX lr
例13

4. NEON assembler and ABI restrictions

NEON汇编程序和ABI限制

  对于极高的性能,手写NEON汇编程序是经验丰富的程序员的最佳方法。GNU汇编器(gas)和ARM编译器工具链汇编器(armasm)均支持NEON指令的汇编。编写汇编器函数时,您必须注意ARM EABI,它决定了如何使用寄存器。ARM嵌入式应用程序二进制接口(Embedded Application Binary Interface EABI) 指定哪些寄存器用于传递参数,返回结果、或必须保留。 除了ARM内核寄存器外,它还指定了32个D寄存器的使用,下表对此进行了总结。

D0 - D7 参数寄存器和返回寄存器。
如果子例程没有参数或返回值,则这些寄存器中的值可能未初始化。
D8 - D15 callee-saved寄存器(callee:被调用者)
D16 - D31 caller-saved寄存器(caller:调用者)
  通常,数据值实际上没有传递给NEON代码子例程(subroutine),因此使用NEON寄存器的最佳顺序是:
  1. D0 - D7
  2. D16 - D31
  3. D8 - D15(如果他们被保存了)

  子例程(subroutines)必须在使用寄存器S16-S31(也是D8-D15Q4-Q7)之前保存(preserve)他们。
  寄存器S0-S15D0-D7Q0-Q3)不需要被保存(preserved),他们可以在标准程序调用的变量中,用于传递参数或者返回结果。
  寄存器D16-D31Q8-Q15)不需要被保存(preserved),

  程序调用标准(Procedure Call Standard)指定了两种方法,可以传递浮点参数:

  • 对于软件浮点,他们通过ARM寄存器R0-R3来传递,如果有要求,可以在栈上。
  • 另外一种,处理器中存在浮点硬件,是在NEON寄存器中传递参数。

4.1 Passing arguments in NEON and floating-point registers

在NEON和浮点寄存器中传递参数

  这种硬件浮点变量表现为以下方式:
  整数参数的处理方式与softfp中的处理方式完全相同。因此,如果我们考虑下面的函数f,我们会看到32位值的a将传递给R0中的函数,并且由于值b必须在一对奇数/偶数寄存器中传递,因此它将进入R2 / R3,而R1没有被使用。

void f(uint32_t a, uint64_t b)
    r0: a
    r1: unused
    r2: b[31:0]
    r3: b[63:32]

  FP参数填充D0-D7(或S0-S15),与任何整数参数无关。 这意味着整数参数可以流到堆栈上,并且FP参数仍将放入NEON寄存器中(如果有足够的可用空间)。

  FP参数能够回填(back-fill),因此我们看在整形参数中,很少去获取未使用的插槽。(FP arguments are able to back-fill, so it’s less common to get the unused slots that we see in integer arguments. )考虑下面的示例:

void f(float a, double b)
    d0:
        s0: a
        s1: unused
    d1: b

  此处,b在被分配到d1的时候自动对齐(d1与VFP s2 / s3占用相同的物理寄存器)。
void f(fl

void f(float a, double b, float c)
    d0:
    s0: a
    s1: c
    d1: b

  在此示例中,编译器能够将c放入s1中,因此不需要将其放入s4中。

  实际上,这是通过对参数sdq使用单独的计数器来实现(并描述)的,并且计数器始终指向该大小的下一个可用插槽(slot)。在上面的第二个FP示例中,首先分配a,因为它在列表中排在第一位,并且进入第一个可用的S寄存器,即s0。接下来,将b分配到第一个可用的D寄存器,该寄存器为d1,因为a使用的是d0的一部分。分配c时,第一个可用的S寄存器为s1。随后的双或单参数将分别输入d2s4

  还有一种情况是把参数填到NEON寄存器:当参数必须要溢出到栈的时候,就不会发生回填,并且为其他参数分配堆栈槽的方式,与为整数参数分配方式完全相同。

void f(double a, double b, double c, double d, double e, double f, float g, double h, double i, float j)
    d0: a
    d1: b
    d2: c
    d3: d
    d4: e
    d5: f
    d6:
    s12: g
    s13: unused
    d7: h
    *sp: i
    *sp+8: j
    *sp+12: unused (4 bytes of padding for 8-byte sp alignment)

  参数a-f按照预期分配给d0-d5。单精度的g分配到s12hd7去了。下一个参数i无法填到寄存器里,所以存到栈上。(它将会和栈上的整数参数交错到一起,要是有的话)。然而s13仍然没有被使用,j必须去到栈上,因为当FP参数到达堆栈时,我们无法回填到寄存器。

  DQ寄存器也可用于保存矢量数据。这种情况不会发生在典型的C代码中。

  可变参数过程(即没有固定数量的参数的过程)不使用NEON寄存器。 相反,它们在softfp中被处理,因为它们在ARM内核寄存器(或堆栈)中传递。 请注意,单精度可变参数会转换为双精度,如softfp一样。


5. NEON libraries

NEON库

有一些免费的开源软件,让NEON得以使用,例如:

  • Ne10库函数,这些函数的C接口,提供汇编程序和NEON实现。 参http://projectne10.github.com/Ne10/
  • OpenMAX,一组用于处理音频,视频和静止图像的API。 它是Khronos组创建的标准的一部分。 有为了NEON的OpenMAX DL层的免费ARM实现。 参见http://www.khronos.org/openmax/
  • ffmpeg,是LGPL许可下的许多不同音频和视频标准的编解码器集合,见http://ffmpeg.org/
  • Eigen3,是线性代数、矩阵数学C++的模板库,位于eigen.tuxfamily.org/
  • Pixman,一个2D图形库(Cairo图形的一部分),位于http://pixman.org/
  • x264,一个免版权的GPL H.264视频编码器,位于http://www.videolan.org/developers/x264.html
  • Math-neon,位于http://code.google.com/p/math-neon/

6. Intrinsics

  NEON C / C++内部函数(intrinsics)在armcc,GCC / g++和llvm中可用。 它们使用相同的语法,因此可以使用任何这些编译器,来编译使用内部函数(intrinsics)的源代码。 它们在第4章中有更详细的描述。

  NEON内部函数(intrinsics)提供了一种编写NEON代码的方法,该方法比汇编程序代码更易于维护,同时仍然可以控制生成的NEON指令。

  内部函数(intrinsics)使用新的数据类型,与NEON寄存器DQ相对应。 数据类型允许创建C变量,这些变量直接映射到NEON寄存器。

  NEON内部函数(intrinsics)的编写类似于函数调用,该函数使用这些变量作为参数或返回值。 但是,编译器将内部函数直接转换为NEON指令,而不执行子例程调用。

  NEON内部函数(intrinsics)提供对NEON指令的低级访问。编译器帮你做了那些很难的工作,这些工作通常要用写汇编语言来做,例如:

  • 寄存器分配
  • 代码调度或重新排序指令,以获得最佳性能。 C编译器知道目标处理器是什么,它们可以对代码重新排序以确保最少的停顿数。

  内在函数(intrinsics)的主要缺点是无法使编译器准确输出所需的代码,因此在转至NEON汇编程序代码时仍有改进的可能性。


7. Detecting presence of a NEON unit

检测NEON单元的存在

  由于在处理器的实现中可以省去NEON单元,因此可能有必要测试NEON单元是否存在。


7.1 Build-time NEON unit detection

编译时检测NEON单元

  这是检测NEON单元是否存在的最简单方法。在ARM编译器工具链(armcc)v4.0和更高版本或GCC中,当向编译器提供了一组合适的处理器和FPU选项时,将会定义预定义的宏__ARM_NEON__。 armasm等效的预定义宏是TARGET_FEATURE_NEON
  可以将其用于C源文件,该源文件针对具有NEON单元的系统和不具有NEON单元的系统优化了代码。


7.2 Run-time NEON unit detection

运行时检测NEON单元

  要在运行时检测NEON单元,需要操作系统的帮助。这是因为ARM体系结构故意不向用户模式的应用程序开放处理器功能。在Linux下,/proc/cpuinfo里用人话描述了这些信息。

  在Tegra2(带有FPU的双核Cortex-A9处理器),cat /proc/cpuinfo显示:

…
Features : swp half thumb fastmult vfp edsp thumbee vfpv3 vfpv3d16
…

  带有NEON单元的ARM四核Cortex-A9处理器显示不同的结果:

…
Features : swp half thumb fastmult vfp edsp thumbee neon vfpv3
…

  由于/proc/cpuinfo输出是基于文本的,因此通常最好查看辅助向量/proc/self/auxv。 它包含二进制格式的内核hwcap。 可以轻松地在/proc/self/auxv文件中搜索AT_HWCAP记录,以检查HWCAP_NEON位(4096)。

  一些Linux发行版(例如Ubuntu 09.10或更高版本)透明地利用NEON单元。修改了ld.so链接程序脚本,以通过glibc读取hwcap。还为了启用NEON的共享库,添加其他搜索路径。对于Ubuntu,新的搜索路径/lib/neon/vfp包含来自/lib的经过NEON优化的库版本。


8. Writing code to imply SIMD

8.1 Writing loops to imply SIMD

  当数据结构化地存储时,优秀地做法是写个寻缘同时使用结构的所有内容。这样可以更好地利用缓存。对于工作寄存器很少的机器,可能会把处理流程分到三个单独的循环:

for (...) { outbuffer[i].r = ....; }
for (...) { outbuffer[i].g = ....; }
for (...) { outbuffer[i].b = ....; }

  通过简单的重写,合并为一个循环,可以在具有高速缓存的处理器上获得更好的结果。这还允许向量化编译器访问结构的每个部分并向量化循环:

for (...)
{
outbuffer[i].r = ....;
outbuffer[i].g = ....;
outbuffer[i].b = ....;
}

8.2 Tell the compiler where to unroll inner loops

告诉编译器在哪里展开内部循环

  对于armcc,可以使用以下语句:

#pragma unroll (n)

  在for语句之前,要求编译器将循环展开一定次数。您可以使用它来指示编译器可以展开内部循环,这可以使编译器在更复杂的算法中,向量化外部循环。

  其他编译器可能有不同的选项来展开内部循环。


8.3 Write structures to imply SIMD

  “加载”操作使用结构体的所有数据项,编译器仅对“加载”操作进行向量化。在某些情况下,会填充结构以保持对齐。下面程序显示了一个像素结构,把它扩展1个Byte,以将每个像素对齐到1个Word(这里一个word是32位)。由于有未使用的字节,编译器不会向量化它的“加载”操作。NEON的“加载”指令可以加载未对齐的结构。因此,在这种情况下,最好删除这个填充的字节,使编译器可以向量化“加载”操作。

struct aligned_pixel 
{ 
    char r; 
    char g;
    char b; 
    char not_used; /* Padding used to keep r aligned to a 32-bit word */
}screen[10];
例14

  NEON的结构加载指令要求结构体的所有元素长度相同,因此下面例15的代码不会被向量化

struct pixel
{
    char r;
    short g; /* Green channel contains more information */
    char b;
}screen[10];
例15

  要是g必须要更高的精度,请考虑把其它变量的长度也扩展,好让这个结构体能被向量化地加载。


9. GCC command line options

GCC命令行选项
  GCC的用于ARM处理器的命令行选项最初是好多年前设计的,那时候的架构比现在的要简单。随着体系结构的发展,为了更好滴生成代码,命令行选项也改了。我们已经努力尝试确保现有选项集的含义不会被改变。 现在需要一套复杂的选项才能让编译器为Cortex-A系列处理器生成最佳的代码。主要的选项有-mcpu-mfpu-mfloat-abi


9.1 Option to specify the CPU

用于指定CPU的选项

  编译文件的时候,必须要让编译器知道你的代码要在什么处理器上运行。所以一个主要的选项是-mcpu=[cpu-name][cpu-name]是小写的处理器名字,例如在Cortex-A9上就是-mcpu=cortex-a9,GCC支持下表的Cortex-A系列处理器:

CPU 选项
Cortex-A5 -mcpu=cortex-a5
Cortex-A7 -mcpu=cortex-a7
Cortex-A8 -mcpu=cortex-a9
Cortex-A9 -mcpu=cortex-a9
Cortex-A15 -mcpu=cortex-a15

  如果你的GCC版本不能识别上表的核,那它可能太旧了,要考虑升级。如果没有指定要使用的处理器,则GCC将使用其内置默认值。默认值可能会有所不同,具体取决于当时编译器是怎么被build的。生成的代码可能在你的CPU上无法运行,或者跑得很慢。


9.2 OOption to specify the FPU

用于指定FPU的选项

  几乎所有的Cortex-A处理器都带有浮点单元,并且大多数还带有NEON单元。但是,处理器的浮点单元(Floating-Point Unit,FPU)决定了具体的指令集是否可用。

  GCC需要另外一个单独的选项-mfpu来指定FPU。它不会尝试通过-mcpu选项确定FPU。 -mfpu选项控制哪种浮点和SIMD指令是可用的。

  下面的表给出了每个CPU的推荐选项:

处理器 仅FP FP + SIMD
Cortex-A5 -mfpu=vfpv3-fp16
-mfpu=vfpv3-d16-fp16
-mfpu=neon-fp16
Cortex-A7 -mfpu=vfpv4
-mfpu=vfpv4-d16
-mfpu=neon-vfpv4
Cortex-A8 -mfpu=vfpv3
-mfpu=neon
Cortex-A9 -mfpu=vfpv3-fp16
-mfpu=vfpv3-d16-fp16
-mfpu=neon-fp16
Cortex-A15 -mfpu=vfpv4
-mfpu=neon-vfpv4

  VFPv3和VFPv4实现提供32个双精度寄存器。但是,当不存在NEON单元时,前16个寄存器(D16-D31)变为可选。 这由选项名称中的-d16表示,这意味着前16个D寄存器不可用。选项名称的fp16组件指定半精度(16位)浮点的加载、存储、转换指令的存在。这是对VFPv3的扩展,但在所有VFPv4实现中也可用。


9.3 Option to enable use of NEON and floating-point instructions

用于开启NEON和浮点指令集的选项

  GCC仅使用浮点和NEON指令,如果明确告知GCC可以安全使用。 控制它的选项是-mfloat-abi。注意,-mfloat-abi也可以更改编译器遵循的ABI。

  -mfloat-abi有3中选项:

  • mfloat-abi=soft:不使用任何FPU和NEON指令。仅使用核心寄存器集。使用库调用模拟所有浮点运算。
  • -mfloat-abi=softfp:使用与-mfloat-abi=soft相同的调用约定,但在适当的手使用浮点和NEON指令。用这个选项编译的应用程序可以与soft float库链接。如果有相关的硬件指令可用,则可以使用此选项来提高代码的性能,并且使代码符合soft-float环境。
  • mfloat-abi=hard:适当地使用浮点和NEON指令,并更改ABI调用约定,以生成更有效的函数调用。 可以在NEON寄存器中的函数之间传递浮点和向量类型,这大大减少了复制量。 这也意味着在堆栈上传递参数时需要更少的调用。

  用于-mfloat-abi的正确选项取决于您的目标系统。对于以平台为目标的编译器,默认选项通常是正确和最佳的选择。Ubuntu 12.04默认使用-mfloat-abi=hard。有关ARM体系结构的ABI的更多信息,请参见ARM体系结构的过程调用标准(Procedure Call Standard for the ARM Architecture),http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/index.html


9.4 Vectorizing floating-point operations

向量化浮点选项

  NEON体系结构包含适用于整数和浮点数据类型的操作。GCC具有强大的自动向量化单元,可以检测何时适合使用向量引擎来优化代码并提高性能。但是,编译器可能不希望对代码进行向量化处理。 造成这种情况的原因多种多样:


Optimization level for the vectorizer

向量化器的优化级别

  默认情况下,向量化程序仅在优化级别-O3启用。 有一些选项可以使向量化程序启用其他级别的优化。有关这些选项,请参见GCC文档。


IEEE compliance

IEEE的符合

  NEON单元中的浮点运算使用IEEE单精度格式来保存正常的操作范围值。 为了最小化NEON单元所需的电量并最大化性能,如果输入或结果为非正常值或NaN值(超出正常工作范围),则NEON单元将不完全符合IEEE标准。

  GCC的默认配置是生成严格符合IEEE浮点算术规则的代码。因此,即使启用了向量化程序,默认情况下,GCC也不使用NEON指令对浮点代码进行向量化。

  GCC提供了许多命令行选项来精确控制所需的IEEE遵从级别。 在大多数情况下,使用-ffast-math选项放宽规则并启用向量化是安全的。另外,您可以在GCC 4.6或更高版本上使用-Ofast选项来达到相同的效果。它打开-O3和许多其他优化来从您的代码中获得最佳性能。 要了解使用-ffast-math选项的所有效果,请参阅GCC文档。

注意

  • NEON单元仅支持对单精度数据进行向量运算。
  • 如果您的代码使用的不是单精度格式的浮点数据,则向量化可能不起作用。
  • 您还必须确保浮点常量(字面的变量)不会强制编译器以双精度执行计算。在C和C++中,用1.0F而不是1.0,以确保编译器使用单精度格式。

9.5 Example GCC command line usage for NEON code optimization

用于NEON代码优化的GCC命令行用法

  确定目标环境后,可以对目标使用GCC命令行选项。下面的例子适用于具有NEON单元的Cortex-A15处理器,其中操作系统支持在NEON寄存器中传递参数。如果有一个数组具有浮点数据类型,然有浮点代码可以操作这些数据,则可以在GCC命令行上指定硬浮点处理:

arm-gcc -O3 -mcpu=cortex-a15 -mfpu=neon-vfpv4 -mfloat-abi=hard -ffast-math -o myprog.exe myprog.c
例16

  下面的例17用于带有NEON单元的Cortex-A9处理器,它支持在NEON寄存器上传递参数。如果有浮点代码,那你可以用GCC命令行指定hard浮点处理:

arm-gcc -O3 -mcpu=cortex-a9 -mfpu=neon-vfpv3-fp16 -mfloat-abi=hard -ffast-math -o
myprog.exe myprog.c
例17

  例18适用于不带NEON单元的Cortex-A7处理器,其中操作系统仅支持在ARM的核心寄存器中传递参数,但可以支持使用浮点硬件进行处理。如果有浮点代码,则可以在GCC命令行上指定softfp浮点处理:

arm-gcc -O3 -mcpu=cortex-a7 -mfpu=vfpv4-d16 -mfloat-abi=softfp -ffast-math -o
myprog2.exe myprog2.c
例18

  例19是针对Cortex-A8处理器(运行于无法使用NEON寄存器的环境中)的。这可能是因为代码位于中断处理程序的中间,并且浮点上下文为USER状态保留。在这种情况下,您可以在GCC命令行上指定soft浮点处理:


arm-gcc -O3 -mcpu=cortex-a8 -mfloat-abi=soft -c -o myfile.o myfile.c
例18

9.6 GCC information dump

倒出GCC信息

  如果要了解有关GCC正在执行的操作的更多信息,请使用-fdump-tree-vect-ftree-vectorizer-verbose=level选项,其中level是1到9之间的数字。较低的值输出的信息较少。这些选项控制生成的信息量。尽管生成的大多数信息仅对编译器开发人员有意义,但您可能会在输出中找到提示,这些提示解释了为什么编译器没有按预期对代码进行向量化。

你可能感兴趣的:(Neon)