如题,有时为了提高部分代码运行性能,可以将部分C代码实现,用汇编改写(自己编写常比编译器优化效果更好)
1.先看案例:
#include <stdio.h>
static int compare_data(int a, int b)
{
int val;
__asm__ __volatile__ (
"cmp %1, %2\n"
"csel %0, %1, %2, hi\n"
: "+r" (val)
: "r" (a), "r" (b)
: "memory");
return val;
}
int main()
{
int val;
val = compare_data(5, 6);
printf("big data: %d\n", val);
val = compare_data(6, 4);
printf("big data: %d\n", val);
return 0;
}
编译运行:
aarch64-linux-gnu-gcc -o main main.c --static -g
# ./main
big data: 6
big data: 6
2.语法解析:
C/C++中嵌入汇编,常用格式如下:
__asm__ [__volatile__] ( assembler template
: [output operand list] /* optional */
: [input operand list] /* optional */
: [clobbered register list] /* optional */
);
关键字__asm__:其实也可以写成“asm”。但是“asm”并不是所有版本的GCC编译器都支持的,而且可能有命名冲突问题,所以用“asm”的话,兼容性更好。
关键字__volatile__:也可以写“volatile”,理由同上;__volatile__是可选的,作用是禁止编译器对后面汇编指令再进行优化。一般自己写的汇编,考虑性能,已经做过优化,编译器再优化的话,可能效果反而更差,所以通常还是带上这个关键字;
括号里:是真正的汇编代码,主要有四部分组成,第一部分是具体的汇编代码,是必须的;其他三个为辅助参数,可选;各部分之间用冒号“:”分割,即使参数为空,也要加冒号;
2.1 assembler template:
所有汇编代码必须用双引号括起来,如果多行汇编代码,每一条语句都要用双引号括起来,代码后面加上换行符("\n"或"\n\t"),具体形式如下:
__asm__ __volatile__ ( "instruction 1\n\t"
"instruction 2\n\t"
......
"last instruction"
);
注:即使一行汇编代码也没有,也要传入空字符串"",否则会报错;
2.2 [output/input operand list] :输出/入参数列表;
分别对应汇编与C语言交互的C表达式,若有多个,用逗号隔开;
在前面汇编中,还可以用"%n",直接引用后面的操作数;
__asm__("mov %0, %1, ror #1"
: "=r"(result)
: "r"(value)
);
这里%0代表第一个操作数,result变量;
%1代表第二个操作数,即value变量;
操作数格式:
每一个操作数,最多由四部分组成:
[name]"modifier+constraint"(C expression)
name:别名,可选;
modifier:修改符,要用双引号括起来;
constraint:限定符,要用双引号括起来;
c表达式:用小括号括起来;
(1)如果指定了别名的话,那在汇编模板中,引用该变量,就可以使用别名,增加可读性,例如:
int x=10, y;
__asm__ ("mov %[in],%[out]"
: [out]"=r"(y)
: [in]"r"(x)
:
);
(2) 说说限定符,操作数在这里的作用是将C语言定义的变量与汇编中使用的变量进行一 一对应,但并不是所有的汇编指令都可以接受任何类型变量,因此汇编器需要知道这些变量到底用在什么地方,传递前做一些转换。常用限定符如下表
限定符 | ARM指令集含义 |
---|---|
r | 通用寄存器 |
f | 浮点寄存器 |
m | 内存地址 |
(3)修改符号,在限定符之前,可选,为空的话,表明该操作数是只读的。
GCC定义了三个修改符,分别是:
修改符 | 含义 |
---|---|
= | 只写操作数,一般用于输出操作数中 |
+ | 可读且可写 |
& | 寄存器只能用于输出 |
如果想让一个C变量既作为输入操作数,也作为输出操作数的话,可以使用“+”限定符,并且这个操作数只需要在输出操作数列表中列出就行了。例如:
__asm__("mov %0, %0, ror #1"
: "=r"(y)
: "0"(y)
);
“&”:为了避免编译器优化输出和输入使用同一个寄存器,可在输出操作数中使用“&”修改符,明确告诉编译器,代表输出操作数的寄存器一定不能使用输入操作数已经使用过的寄存器。
2.3 修改寄存器列表
为保持寄存器,内存数据一致性,提供三个类型
类型 | 作用 |
---|---|
r0…r15 | 告诉编译器汇编代码中修改了通用寄存器r0…r15 |
cc | 告诉编译器汇编代码会导致CPU状态位的改变 |
memory | 告诉编译器汇编代码会读取或修改内存中某个地址存放的值 |
添加“memory”之后,编译器会在执行汇编代码之前,保证将保存在寄存器中,没有更新到内存中的值全部都写入到内存中。
此列表中的每一项都要用双引号("")括起来,每项之间要用逗号(“,”)分割。