arm 内联汇编基础

一、 Arm架构寄存器体系熟悉

基于arm neon 实现的代码有 intrinsic 和inline assembly 两种实现。

1.1 通用寄存器

arm v7 有 16 个 32-bit 通用寄存器,用 r0-r15 表示。

arm v8 有 31 个 64-bit 通用寄存器,用 x0-x30 表示,和 v7 不一样的是,这 31 个寄存器也可以作为 32-bit 寄存器来用,用 w0-w30 表示,其中 wn 是 xn 的低 32 位,如下图所示:

函数前四个参数,会按顺序被放入寄存器 r0-r3(w0-w3), 剩下会采用压栈的方式保存

寄存器寄存器 别名 用途
r0 a1 第一个函数参数: Scratch 寄存器
r1 a2 第二个函数参数: Scratch 寄存器
r2 a3 第三个函数参数: Scratch 寄存器
r3 a4 第四个函数参数: Scratch 寄存器
r4 v1 寄存器变量
r5 v2 寄存器变量
r6 v3 寄存器变量
r7 v4 寄存器变量
r8 v5 寄存器变量
r9 v6 rfp 寄存器变量 实际的帧指针
r10 sl 栈接线
r11 fp 参数指针
r12 ip 临时
r13 sp 栈指针
r14 lr 连接寄存器
r15 pc 程序计数

1.2 向量寄存器

armv7 包含 16 个 128-bit 向量寄存器,用 q0-q15 表示,其中每个 q 寄存器又可以拆分成两个 64-bit 向量寄存器来用,用 d0-d31 来表示。

armv8 则有更多的向量寄存器,32 个 128-bit 向量寄存器,用 v0-v31 来表示。

每个 128-bit 向量寄存器可以当做:

  • 包含 2 个 64-bit 元素的向量寄存器来用,表达形式是 vn.2d;

  • 包含 4 个 32-bit 元素的向量寄存器来用,表达形式是 vn.4s;

  • 包含 8 个 16-bit 元素的向量寄存器来用,表达形式是 vn.8h;

  • 包含 16 个 8-bit 元素的向量寄存器来用,表达形式是 vn.16b;

或者每个向量寄存器也可以只用低 64-bit:

  • 1 个 64-bit 元素的向量寄存器来用,表达形式是 vn.1d;

  • 2 个 32-bit 元素的向量寄存器来用,表达形式是 vn.2s;

  • 4 个 16-bit 元素的向量寄存器来用,表达形式是 vn.4h;

  • 8 个 8-bit 元素的向量寄存器来用,表达形式是 vn.8b;

利用指令集加速,无一例外地要利用专用寄存器这种在 CPU 上稀少、宝贵的资源。专用寄存器用少了 CPU 的性能不能充分发挥,用多了则会产生寄存器溢出 (Register Spilling)(https://blog.csdn.net/qq_41112170/article/details/90286091) 这种对性能有明显负面影响的问题。因此,我们至少需要了解在编写 Neon 代码时,有多少个专用寄存器可供利用。

二、内联汇编

2.1 基础写法

__asm__ qualifiers ( // 汇编代码部分 
: OutputOperands //在内联汇编代码中被修改的变量列表 
: InputOperands //在内联汇编代码中用到的变量列表 
: Clobbers //在内联汇编代码中用到的寄存器列表 );

qualifiers:一般是用 volatile 修饰词 ,关键字__volatile__:也可以写“volatile”,理由同上;__volatile__是可选的,作用是禁止编译器对后面汇编指令再进行优化。一般自己写的汇编,考虑性能,已经做过优化,编译器再优化的话,可能效果反而更差,所以通常还是带上这个关键字;

括号里:是真正的汇编代码,主要有四部分组成,第一部分是具体的汇编代码,是必须的;其他三个为辅助参数,可选;各部分之间用冒号“:”分割,即使参数为空,也要加冒号;

  • OutputOperands:在内联汇编中会被修改的变量列表,变量之间用','隔开, 每个变量的格式是: [asmSymbolicName] "constraint"(cvariablename) cvariablename:表示变量原来的名字; asmSymbolicName:表示变量在内联汇编代码中的别名,一般和 cvariablename 一样,在汇编代码中就可以通过%[asmSymbolicName]去使用该变量; constraint: 一般填=r,具体解释见文档[6]

  • InputOperands:在内联汇编中用到的所有变量列表,变量之间用','隔开, 每个变量的格式是: [asmSymbolicName] "constraint"(cexpression) 和输出不一样地方是,首先要按OutputOperands列表的顺序再列一遍,但是constraint用数字代替从0开始,然后才是写其他只读变量,只读变量constraintr

  • Clobbers: 一般是"cc", "memory"开头,然后接着填内联汇编中用到的通用寄存器和向量寄存器 "cc"表示内联汇编代码修改了标志寄存器; "memory"表示汇编代码对输入和输出操作数执行内存读取或写入操作(读写参数列表之一的变量指向的内存);

  • 输入列表 ("r" (some_input)): 这表明 some_input 是一个输入操作数,它的值在汇编执行前被读取。"r" 约束表示 some_input 被存储在某个寄存器中,具体哪个寄存器由编译器决定。

  • 输出列表 ("+r" (result)): 这表明 result 是一个输出操作数,它的值在汇编执行后被写回。"+" 约束表示 result 既可以作为输入也可以作为输出,汇编代码可以读取它的初始值,并在执行过程中更新它的值。

约束说明:

  • "r":将值放入任意一个可用的寄存器中。

  • "+r":将值放入任意一个可用的寄存器中,并且该寄存器在操作后还会被写回,即它既可以作为输入也可以作为输出。

  • "+w":类似于 "+r",但表示该值在汇编代码中可能会被修改,并且修改后的值需要写回原始变量。

  • "m":表示该值应该被加载到内存地址中,通常与指针一起使用。

asm("mov %0,%1"
        :"+r"(val1)
        :"r"(val2)
        :);

由上面对指令语法的描述进行分析:

  • 输出操作数为 val1,属性为 "=r"。

  • 输入操作数为 val2,属性为 "r"

  • code 部分为 mov %1,%0,

  • %0 表示输入输出列表中的第一个操作数,

  • %1 表示操作数列表中提供的第二个操作数,以此类推,这条汇编指令很明显就是将第二个操作数(val2)赋值给第一个操作数(val1),所以最后的结果为 val1 = 222. 。

    int x=10, y;
    __asm__ ("mov %[in],%[out]"
       : [out]"=r"(y)
       : [in]"r"(x)
       :
    );
    

如果指定了别名的话,那在汇编模板中,引用该变量,就可以使用别名,增加可读性,

2.2 操作符含义

  • "=" 表示只写,通常用于所有输出操作数的属性

  • "+" 表示读写,只能被列为输出操作数的属性,否则编译会报错。

  • & :只能用作输出

限定符

ARM指令集含义

r

通用寄存器

f

浮点寄存器

m

内存地址

为保持寄存器,内存数据一致性,提供三个类型

类型 作用
r0…r15 告诉编译器汇编代码中修改了寄存器r0…r15 (v8 是x, v)
cc 告诉编译器汇编代码会导致CPU状态位的改变
memory 告诉编译器汇编代码会读取或修改内存中某个地址存放的值

三、样例分析

对于刚入门优化的同学,改写汇编最好先从 C++ 改写 intrinsic 开始,然后再根据 intrinsic 的代码去改写汇编,一般 intrinsic 的指令和汇编指令都能对应的上,当然高手可以直接跳过去写汇编,但是对于新手建议还是一步步来。

而且比较重要的一点是,我认为 算法上的改进更为重要,假设你 C++ 算法层面代码已经定下来了,对于性能还想有更进一步的提升,那么可以尝试去写 neon 汇编(内联或者纯汇编),但不是说汇编是万能的,这个和你的优化经验还有算法本身的复杂度有很大关系,可能你吭哧坑次改完,发现还做了负优化,因为编译器本身也会做向量化。

3.1 两个数组加权和

第一个例子是两个数组对应元素加权和,例子足够简单,方便讲解改写汇编的一些思路。 下面代码为了可读性会相应的作简.

3.1.1 c++ 实现

bool arrWeightedAvg(const float *arr1,
                    const float arr1Weight,
                    const float *arr2,
                    const float arr2Weight,
                    const int len,
                    float *resultArr) {
  for (int i = 0; i < len; ++i) {
    resultArr[i] = arr1[i] * arr1Weight + arr2[i] * arr2Weight;
  }
  return true;
}

3.1.2 改 intrinsic

对于 intrinsic 代码是兼容 armv7 和 v8 的,所以不同架构之间迁移也方便,不需要改代码:

bool arrWeightedAvgIntrinsic(const float *arr1,
                             const float arr1Weight,
                             const float *arr2,
                             const float arr2Weight,
                             const int len,
                             float *resultArr) {
  int neonLen = len >> 2;
  int remain = len - (neonLen << 2);
  // 这里向量化主要思路是循环内每次
  // 处理4个元素的加权和
  // 所以neonLen是数组长度len除4
  // 而剩下的尾部元素按正常处理

  float *resultArrPtr  = resultArr;
  const float *arr1Ptr = arr1;
  const float *arr2Ptr = arr2;

  // 因为一次处理4个元素
  // 所以权值要拷贝4份放到
  // 一个float32x4_t类型变量中
  // 也相当于是128-bit向量寄存器
  float32x4_t arr1Wf4 = vdupq_n_f32(arr1Weight);
  float32x4_t arr2Wf4 = vdupq_n_f32(arr2Weight);

  for (int i = 0; i < neonLen; ++i) {
    // 分别读4个数组元素
    float32x4_t arr1f4 = vld1q_f32(arr1Ptr);
    float32x4_t arr2f4 = vld1q_f32(arr2Ptr);

    // eltwise乘法
    arr1f4 = vmulq_f32(arr1f4, arr1Wf4);
    arr2f4 = vmulq_f32(arr2f4, arr2Wf4);

    // eltwise加法
    float32x4_t resultf4 = vaddq_f32(arr1f4, arr2f4);

    // 写结果
    vst1q_f32(resultArrPtr, resultf4);

    arr1Ptr += 4;
    arr2Ptr += 4;
    resultArrPtr += 4;
  }
  // 处理尾部元素
  for (; remain > 0; remain --) {
    *resultArrPtr = (*arr1Ptr) * arr1Weight + (*arr2Ptr) * arr2Weight;
    resultArrPtr ++;
    arr1Ptr ++;
    arr2Ptr ++;
  }

  return true;
}

3.1.3 arm v7 内联汇编

bool arrWeightedAvgIntrinsic(const float *arr1,
                             const float arr1Weight,
                             const float *arr2,
                             const float arr2Weight,
                             const int len,
                             float *resultArr) {
  int neonLen = len >> 2;
  int remain = len - (neonLen << 2);
  // 这里向量化主要思路是循环内每次
  // 处理4个元素的加权和
  // 所以neonLen是数组长度len除4
  // 而剩下的尾部元素按正常处理

  float *resultArrPtr  = resultArr;
  const float *arr1Ptr = arr1;
  const float *arr2Ptr = arr2;

  // 因为一次处理4个元素
  // 所以权值要拷贝4份放到
  // 一个float32x4_t类型变量中
  // 也相当于是128-bit向量寄存器
  float32x4_t arr1Wf4 = vdupq_n_f32(arr1Weight);
  float32x4_t arr2Wf4 = vdupq_n_f32(arr2Weight);

  for (int i = 0; i < neonLen; ++i) {
    // 分别读4个数组元素
    float32x4_t arr1f4 = vld1q_f32(arr1Ptr);
    float32x4_t arr2f4 = vld1q_f32(arr2Ptr);

    // eltwise乘法
    arr1f4 = vmulq_f32(arr1f4, arr1Wf4);
    arr2f4 = vmulq_f32(arr2f4, arr2Wf4);

    // eltwise加法
    float32x4_t resultf4 = vaddq_f32(arr1f4, arr2f4);

    // 写结果
    vst1q_f32(resultArrPtr, resultf4);

    arr1Ptr += 4;
    arr2Ptr += 4;
    resultArrPtr += 4;
  }
  // 处理尾部元素
  for (; remain > 0; remain --) {
    *resultArrPtr = (*arr1Ptr) * arr1Weight + (*arr2Ptr) * arr2Weight;
    resultArrPtr ++;
    arr1Ptr ++;
    arr2Ptr ++;
  }

  return true;
}

3.1.4 armv8 内联汇编

#ifdef __aarch64__  // armv8
  __asm__ volatile(
    "mov   x0, %[arr1Weight]                  \n"   // 将weight1的值移动到通用寄存器x0中。
    "dup   v0.4s, w0                          \n"   //w0是x0的低32位, 复制值到向量寄存器v0中,当成4*32来使用。
    
    "mov   x1, %[arr2Weight]                  \n"
    "dup   v1.4s, w1                          \n"
    
    "0:                                       \n"    //循环结束条件,小于0.

    "prfm  pldl1keep, [%[arr1Ptr], #128]      \n"    //预读取arr1地址开始的128bit 数据,就是4个32bit的数据。
    "ld1      {v2.4s}, [%[arr1Ptr]], #16      \n"    // 将数据加载到v2 向量寄存器中, 并且地址自增16个字节。

    "prfm  pldl1keep, [%[arr2Ptr], #128]      \n"
    "ld1      {v3.4s}, [%[arr2Ptr]], #16      \n"
    
    "fmul       v4.4s, v2.4s, v0.4s           \n"    //数组1和权重相乘。保存在v4 寄存器中。
    "fmul       v5.4s, v3.4s, v1.4s           \n"    // 数据2和权重相乘,保存在v5 寄存器中。

    "fadd       v6.4s, v4.4s, v5.4s           \n"   //将寄存器v4, v5的值相加, 保存在v6寄存器中。

    "subs       %[neonLen], %[neonLen], #1    \n"  // 对应 neonLen--  sub指令后面加个s表示会更新条件flag
   
    
    "st1    {v6.4s}, [%[resultArrPtr]], #16   \n" //将寄存器v6的结果写入到目的地址resultarrptr, 地址自增16字节。(4个数,一个数四字节)
    
    "bgt        0b                            \n"  //b跳转指令, gt 判断是不是大于0条件判断, 大于0, 跳转到0的位置。

    :[arr1Ptr]        "+r"(arr1Ptr),
     [arr2Ptr]        "+r"(arr2Ptr),
     [resultArrPtr]   "+r"(resultArrPtr),
     [neonLen]        "+r"(neonLen)
    :[arr1Weight]     "r"(arr1Weight),
     [arr2Weight]     "r"(arr2Weight)
    :"cc", "memory", "x0", "x1", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7"
  );

反编译系统的编译文件,进行汇编代码对比,学习。

./llvm-objdump -d /home/lsq/wind/wind_develop_my/wind/build_android/src/backend/cpu/CMakeFiles/Wi

ndCPU.dir/kernel/neon/matmul_quant_test.cc.o

内联汇编的目的是进行汇编指令的优化,尽可能的直接操作寄存器,利用新特性,进行代码的加速。更多的指令需要查找官方文档进行学习。

3.2 汇编指令对应的机器码生成

.inst 0x4e80a4d8 是一个汇编指令,用于在 ARM 架构中直接插入机器码。这个指令的格式是 .inst <机器码>,其中 <机器码> 是一个 32 位或 64 位的十六进制值,表示一条机器指令。

具体来说,0x4e80a4d8 是一个 32 位的机器码。为了理解这个机器码是如何编码的,我们需要查看 ARMv8 指令集的文档,特别是 NEON 指令集的文档。

3.2.1 使用 LLVM 工具

可以安装 llvm 工具链,然后运行如下命令:、

echo "smmla v16.4s, v4.16b, v0.16b" | llvm-mc -arch=aarch64 -mattr=+neon,+i8mm -show-encoding

这将会输出汇编指令对应的机器码。如果没有安装 llvm-mc 工具,可以参考以下汇编器指令来生成机器码。

3.2.2 使用 GNU 汇编器

你可以使用 arm-none-eabi-as 工具来编译汇编代码并生成机器码。下面是一个示例:

echo ".arch armv8-a; smmla v16.4s, v4.16b, v0.16b" | arm-none-eabi-as -o - -a -

3.2.3 在线工具

https://armconverter.com/

3.2.4 反编译编译产物

./llvm-objdump -d /home/lsq/wind/wind_develop_my/wind/build_android/src/backend/cpu/CMakeFiles/WindCPU.dir/kernel/neon/matmul_quant_test.cc.o 反汇编结果:

".inst 0x4e80a490 \n" // smmla v16.4s, v4.16b, v0.16b //v0_01s

// *y0_0

".inst 0x4e81a4b5 \n" // smmla v21.4s, v5.16b, v1.16b //v0_0hs

// *y0_1

".inst 0x4e82a4da \n" // smmla v26.4s, v6.16b, v2.16b //v0_1ls

// *y0_2

".inst 0x4e83a4ff \n" // smmla v31.4s, v7.16b, v3.16b// v0_1hs

四、附录

https://medium.com/@warmap_/%E8%BD%AC-%E5%A6%82%E4%BD%95%E5%9C%A8c%E6%88%96c-%E4%BB%A3%E7%A0%81%E4%B8%AD%E5%B5%8C%E5%85%A5arm%E6%B1%87%E7%BC%96%E4%BB%A3%E7%A0%81-a3704e164de8

http://giantpandacv.com/project/%E9%83%A8%E7%BD%B2%E4%BC%98%E5%8C%96/AI%20%E7%A7%BB%E5%8A%A8%E7%AB%AF%E7%AE%97%E6%B3%95%E4%BC%98%E5%8C%96/%E7%A7%BB%E5%8A%A8%E7%AB%AFarm%20cpu%E4%BC%98%E5%8C%96%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/%E7%A7%BB%E5%8A%A8%E7%AB%AFarm%20cpu%E4%BC%98%E5%8C%96%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E7%AC%AC4%E5%BC%B9--%E5%86%85%E8%81%94%E6%B1%87%E7%BC%96%E5%85%A5%E9%97%A8/

相关推荐

  1. 汇编清楚变量指定位

    2024-07-20 19:10:02       18 阅读
  2. ARM汇编基础语法

    2024-07-20 19:10:02       22 阅读
  3. 【C++基础函数

    2024-07-20 19:10:02       27 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-20 19:10:02       52 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-20 19:10:02       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-20 19:10:02       45 阅读
  4. Python语言-面向对象

    2024-07-20 19:10:02       55 阅读

热门阅读

  1. lua语法思维导图

    2024-07-20 19:10:02       11 阅读
  2. Perl脚本的魔法:打造自定义文件系统视图

    2024-07-20 19:10:02       19 阅读
  3. nginx的docker-compose文件

    2024-07-20 19:10:02       15 阅读
  4. 蒙皮(Skinning)

    2024-07-20 19:10:02       17 阅读
  5. PS小技巧:在有褶皱的地方贴图 (300字畅享版)

    2024-07-20 19:10:02       15 阅读
  6. 恶补,正态分布

    2024-07-20 19:10:02       22 阅读
  7. 达梦日志路径

    2024-07-20 19:10:02       18 阅读
  8. sqlalchemy_dm

    2024-07-20 19:10:02       18 阅读