AI 移动端框架常用指令·汇总(待续)

卷积操作常用的汇编指令(NEON)

前言

首先我们要知道,ios的芯片虽然是ARM内核的,但是后来慢慢地魔改已经跟公版的ARM有所区别了,因此其对应配套的汇编规范也就有些细微的差别了:

arm32 函数调用约定

arm64函数调用约定

ios函数调用约定

因此安卓端的汇编跟iOS端的汇编就得区别对待了!

1. Android端32bit和64bit汇编的区别

先大概看下arm下SIMD汇编指令语法的区别在哪!

数据来源:《Arm® Compiler Version 6.6 armasm User Guide》

1.1 GCC内联汇编模板

模板就是这个样子

__asm__ [__volatile__] (assembler template
    : [output operand list]                          /* optional */
    : [input operand list]                          /* optional */ 
    : [clobbered register list]                       /* optional */ 
    );

真实的例子就是这样:

    asm volatile(
    "0:                         \n"

        "vdup.16 q0, r0         \n"
        "add %0, %0, #10        \n"
        "vst1.s16   {q0}, [%0]  \n"
    :
    "=r"(src) // %0
    :
    "0"(src), //
    "w"(zxl)  //%2
    :
    "cc", "memory", "q0", "r0", "r1"
    );

1.2 armV8和armV7指令集

armV7指令集主要是针对32bit的,armV8指令集则是针对最新的64bit架构;

首先总体来说,v7指令集在操作Q寄存器时,指令喜欢带个V表示,我这是在操作NEON寄存器,而v8的ISA64指令集就把这个给取消了,我只出指令,具体的什么操作细节,你操作数去细化指明。

1.3 预取

v7这样用:从%3处预取192个byte;

  "pld        [%3, #192]          \n"

v8: pld1kepp这个参数是可以改的,改为预取到L2中,不keep,而是流式缓存,也就是不会真放进cache中,具体的可以去看芯片手册。

 "prfm   pldl1keep, [%1, #512]       \n"

1.4 内存加载

** V7: **

  • 带了前缀v的就是v7 32bit指令的标志;
  • ld1表示是顺序读取,还可以取ld2就是跳一个读取,ld3、ld4就是跳3、4个位置读取,这在RGB分解的时候贼方便;
  • 后缀是f32表示单精度浮点,还可以是s32、s16表示有符号的32、16位整型值。(对比后面的ISA64指令集就可以知道它浮点用的是4s,int16是4h)
  • 这里Q寄存器是用q表示,q5对应d10、d11可以分开单独访问(注:v8就没这么方便了。)
  • 大括号里面最多只有两个Q寄存器。
"vld1.f32   {q10}, [%3]!        \n"
"vld1.s16 {q0, q1}, [%2]!       \n" 

v8:

  • 可以看到指令就是单纯的表达我是个什么样操作而已,具体的什么数据类型啊就全部交给底下的寄存器去表达了。
  • NEON寄存器用V来表示(后缀为8B/16B/4H/8H/2S/4S/2D);
  • 大括号内最多支持4个V寄存器;
"ld1    {v0.4s, v1.4s, v2.4s, v3.4s}, [%2], #64 \n"
"ld1    {v0.8h, v1.8h}, [%2], #32     \n"
"ld1    {v0.4h, v1.4h}, [%2], #32     \n"
  • 无法直接访问V0 的上半部分的D寄存器,目前的情报是:直接访问D0就是v0.4h,要访问V0的高64bit,可以先V0.8h加载满数据,然后以V0.h[4~7]来访问单独的int16数据;再或者就是像这样来访问:把v5.4s里面的4个32bit整型值压缩进v8.8h高64bit里面的4个int16里面。
"sqshrn  v8.4h, v4.4s, %15            \n" // output int16x4_t
"sqshrn2 v8.8h, v5.4s, %15            \n" // output int16x4_t

1.5 以占位符方式访问Q、D寄存器

关于这部分在NCNN github的wiki页面有详细的解释,可以去那看看哟!

V7:

  • 先定义变量,表明_bias0就是一个向量格式了;
 const float* bias = _bias;
 
 float32x4_t _bias0 = bias ? vld1q_f32(bias+g*4) : vdupq_n_f32(0.f);
  • 然后在占位符那里表明我这个_bias0需要用一个浮点寄存器缓存起来;
"w"(_bias0)     // %23
  • 然后我们在f32里面用是这样用的:把%23里面加入一个q表明这个是一个Q寄存器,然后按位与,最终输出到Q12向量寄存器里面。
"vand.f32     q12, %q23, %q23   \n"
  • 因为q0是按照s16的方式导进来,因此,d0相当于有4个int16,因此能索引到d[3]
  "vld1.s16 {q0, q1}, [%2]!       \n" // input int16x4_t
  "vmull.s16   q10,  %P6, d0[3]   \n" // 
  • 首先定义的是int16x4,就是D寄存器,因此在访问的时候就是直接%P14了m,用P指明是D寄存器;
int16x4_t _k4 = vld1_s16(k0 + 16);

 "vmlal.s16   q13, %P14, d3[3]   \n" //_k4 ==> %14

V8:

  • 相对应的V8以占位符方式访问向量寄存器的时候,是直接在后面加后缀来指明立场的;如浮点乘法的时候就是%16.4s指明是单精度浮点(4个single精度浮点值),同样的v21.s[3]是访问4个中的其中一个浮点值。
  "fmla   v28.4s, %16.4s, v21.s[3]       \n"
  • v8的int16操作:以int16 的方式加载一个D寄存器的_k0出来,然后v0.8h表示加载8个int16,然后v0.h[7]跟%6以4h的方式相乘,最后拓展输出到V4寄存器,以4s的格式,也就是4个32位;
  int16x4_t _k0 = vld1_s16(k0     );
  "ld1    {v0.8h, v1.8h}, [%2], #32     \n" // input int16x4_t
  "smull   v4.4s, %6.4h, v0.h[7]        \n" 
  "w"(_k0),    // %6

1.6 小结

v7的浮点操作:
  "vmla.f32   q4, %q6, d0[0]   \n" // q0.s[0]

v8的浮点操作:
  "fmla   v4.4s, %6.4s, v0.s[0]       \n"
  
v7的int16操作:
 "vmull.s16   q10,  %P6, d0[0]   \n" // v0.h[0]
 
v8的int16操作:
 "smull   v4.4s, %6.4h, v0.h[0]        \n" 

2. ios端汇编

In general, iOS adheres to the generic ABI specified by ARM for the ARM64 architecture. However there are some choices to be made within that framework, and some divergences from it. This document describes these issues.

官网的一段申明,指明虽然继承自公版,但其与公版ARM架构仍有所区别;

具体的去这里看吧!ios函数调用约定

2.1 汇编文件

我们先看下如何写汇编文件,并在c++中如何调用它吧~

先大概了解下外围!

这个作者讲的很细致!

知乎专栏

这个作者也讲的很细!

老外总是能把很繁琐的知识(汇编)讲的很简洁!
看看人家的副标题:Learn how to read assembly in iOS – a useful skill when debugging your code or diagnosing why a crash has occurred.

解释下什么是FP(Frame Point)寄存器:
通常在C程序编译过程中,所有函数的局部变量被分配在一个连续的存储区中,一般这个存储区是在堆栈中,这个连续的存储区称为这个函数的存储“帧”,它是通过一个指针访问的。

这里要注意的是Apple所采用的ARM汇编器遵循GNU Assembler规范。

其中,我们可以看到,汇编文件里的注释可以采用C语言标准的注释方式,也可以用C++标准的//注释方式。

  • .text表示代码正文部分。

  • .align根据不同的汇编器会有不同的行为,像这里的.align4可能表示4字节对齐,也可能表示16字节对齐。

GAS规范中表示,可以用.global或.globl来标注全局函数。

在Apple的Assembler中仅支持.globl函数名前要加下划线。

.arm表示后面的函数中的指令都是arm指令。而.thumb表示后面函数中的指令都是thumb或thumb-2指令。其中,如果一个函数是用thumb写的,那么必须用.thumb_func修饰,否则连接器在连接符号时会有问题。

另外,Apple LLVM汇编器中的条件预处理与C语言用的也几乎一样。可以使用#if、#else、#endif、#ifdef、#ifndef、#elif等等。另外,在架构标识上也统一使用了标准的架构标识符,比如:i386表示x86处理器架构;x86_64表示64位的x86处理器;arm表示ARM架构的处理器;arm64表示64位ARM架构处理器。

iOS中ARMv7以及ARM64下的ABI:

ARMv7中,对于通用寄存器,自己写的过程中需要保护R4、R5、R6、R7、R8、R9、R10、R11以及R14寄存器;NEON寄存器需要保存Q4、Q5、Q6、Q7寄存器。

ARM64模式下,通用寄存器X18、X30不能被使用。而需要被自己写的过程所保护的是:X19、X20、X21、X22、X23、X24、X25、X26、X27、X28、X29寄存器;而SIMD寄存器需要保护的是V8、V9、V10、V11、V12、V13、V14、V15。

堆栈的意义在于保存状态。

讲一个最简单的例子吧!如何在工程内添加及使用汇编文件:

  • 新建一个汇编文件asmTest.s
.text   //申明是代码区
.align 4 //4字节对齐
.globl _asmTest //申明全局函数名(注:要下划线开头,在外面调用的时候就不需要了)


_asmTest:
// 单纯测试trn转置指令的;

    mov w9, #1
    mov v6.s[0], w9
    mov w9, #2
    mov v6.s[1], w9
    mov w9, #3
    mov v6.s[2], w9
    mov w9, #4
    mov v6.s[3], w9

    mov w9, #4
    mov v7.s[0], w9
    mov w9, #5
    mov v7.s[1], w9
    mov w9, #6
    mov v7.s[2], w9
    mov w9, #7
    mov v7.s[3], w9

    trn1 v10.4s, v6.4s, v7.4s
    trn2 v11.4s, v6.4s, v7.4s

RET
  • 在调用时,先extern int asmTest(void);,然后直接像正常函数那样使用就行了!
  • 还有一种实现方式就是内联汇编了!直接可以在函数内部使用,很方便且性能不受影响。
    __asm volatile (
                    "mov w9, #1               \n"
                    "mov v6.s[0], w9          \n"
                    "mov w9, #2               \n"
                    "mov v6.s[1],  w9         \n"
                    "mov w9, #3               \n"
                    "mov v6.s[2],  w9         \n"
                    "mov w9, #4               \n"
                    "mov v6.s[3],  w9         \n"

                    "mov w9, #5               \n"
                    "mov v7.s[0], w9          \n"
                    "mov w9, #6               \n"
                    "mov v7.s[1],  w9         \n"
                    "mov w9, #7               \n"
                    "mov v7.s[2],  w9         \n"
                    "mov w9, #8               \n"
                    "mov v7.s[3],  w9         \n"

                    "trn1 v10.4s, v6.4s, v7.4s\n"
                    "trn2 v11.4s, v6.4s, v7.4s\n"
                    :
                    :
                    : "cc", "memory"
    );

2.2 翻译下老外的篇吧!讲的挺好的!

(汇编)讲的挺简洁!

Calling Conventions讲的是汇编函数调用的规范,用来弄出一个统一的标准。

······
······
······

AI 移动端框架常用指令·汇总(待续)_第1张图片
image

哈哈~发现讲的很基础了!还是不翻译了!你们自己去看吧!

三、 实例

下面是pooling汇编实现的kernel部分,里面有arch32跟arch64的对应版本,对比着看就很明显了:

  • 32bit里面喜欢在指令里面表明数据格式,而64bit则把这个任务交给寄存器去申明了!
  • 。。。


你可能感兴趣的:(AI 移动端框架常用指令·汇总(待续))