https://zhuanlan.zhihu.com/p/24402180
KEIL平台主要有四个工具:
其作用及关系如下图所示:
本文主要讲述KEIL平台编程的优化策略,主要内容如下:
本文主要内容来源于ARM® Compiler v5.06 for μVision® Version 5 armcc User Guide
代码体积vs执行速度
Keil编译器默认配置,主要目的是减少代码体积
目的是加快执行速度
优化等级及调试信息
最少的优化,可以最大程度上配合产生代码调试信息,可以在任何代码行打断点,特别是死代码处。
有限的优化,去除无用的inline和无用的static函数、死代码消除等,在影响到调试信息的地方均不进行优化。在适当的代码体积和充分的调试之间平衡,代码编写阶段最常用的优化等级。
高度优化,调试信息不友好,有可能会修改代码和函数调用执行流程,自动对函数进行内联等。
最大程度优化,产生极少量的调试信息。会进行更多代码优化,例如循环展开,更激进的函数内联等。
另外,可以通过单独设置 --loop_optimization_level=option 来控制循环展开的优化等级。
C代码的循环结束条件
int fact1(int n)
{
int i, fact = 1;
for (i = 1; i <= n; i++)
fact *= i;
return (fact);
}
在 -O2 -Otime条件下汇编为:
fact1 PROC
MOV r2, r0
MOV r0, #1
CMP r2, #1
MOV r1, r0
BXLT lr
|L1.20|
MUL r0, r1, r0
ADD r1, r1, #1
CMP r1, r2
BLE |L1.20|
BX lr
ENDP
int fact2(int n)
{
unsigned int i, fact = 1;
for (i = n; i != 0; i--)
fact *= i;
return (fact);
}
在 -O2 -Otime条件下汇编为
fact2 PROC
MOVS r1, r0
MOV r0, #1
BXEQ lr
|L1.12|
MUL r0, r1, r0
SUBS r1, r1, #1
BNE |L1.12|
BX lr
ENDP
以上可以看到, ++式中 ADD 和 CMP 指令增大了汇编指令条数。
同样,在 while 和 do 也是同样情况。
C代码中的循环展开
普通循环
int countbit1(unsigned int n)
{
int bits = 0;
while (n != 0)
{
if (n & 1) bits++;
n >>= 1;
}
return bits;
}
循环展开
int countbit2(unsigned int n)
{
int bits = 0;
while (n != 0)
{
if (n & 1) bits++;
if (n & 2) bits++;
if (n & 4) bits++;
if (n & 8) bits++;
n >>= 4;
}
return bits;
}
代码的循环展开可以减少循环体执行的次数,但是也在一定程度上增大了代码的体积,循环展开需要适量,一般循环展开4个一组。
在-O3优化等级下,编译器会适当进行循环展开。
编译器在函数内联时的决策
函数内联是在代码体积和执行性能之间所做的权衡,是否内联由编译器决定。
如果优化选项是 -Ospace , 则编译器会倾向于比较少的内联,以减少代码体积;如果优化选项为 -Otime , 则编译器则会倾向于更多的内联,但也会避免代码体积的大量增加。
使用 __inline, __forceinline 等关键字(或者 attribute 属性控制, 或者 #pragma 属性控制也有相应的用法,详细下文会说), 可以给编译器建议,但是最终是否内联还是由编译器决定。
另外,内联函数的体积应该比较小。
编译器在以下情况下,如果有可能内联,则尽量内联:
编译器在以下情况下,会在合适的情况下进行内联(编译器自己决定):
编译在决定是否内联时会综合考虑一下因素:
在编译器认为不合适的情况下,即使声明了 __forceinline 函数也不会内联。
inline和static
具有外部链接属性的函数在内联时会保留一份函数代码在最终的二进制文件中,所以外部链接属性的函数内联会增大代码体积。
但是声明为 static 的函数就不会存在这个问题,static 表明函数只需要内部调用,所以不需要再保留原来的代码。
注意,如果明确不需外部调用的函数,请加上 static
链接器在链接时不会自动去掉具有外部链接属性的函数,以防某些外部程序调用,可以使用以下方式去除无用函数:
- --split_sections 这个编译选项会把每一个函数都单独放到一个section
- __attribute__((section("name"))) 把特定的函数放到自定义的section中
- --feedback 这个编译选项,会对代码进行两次编译分析其代码的使用,并去掉无用函数,keil推荐使用此种方案
避免 inline
由于inline会对debug造成影响,可以通过 - -no_inline 编译选项禁止所有的内联,或者通过 - -no_autoinline 禁止编译器自动内联。
优化等级较高时,会出现一些低优化等级不会出现的问题,例如没有使用关键字 volatile(其他的部分主要是代码中出现未定义行为,即UB).
没有使用 volatile 修饰变量时:
int buffer_full;
int read_stream(void)
{
int count = 0;
while (!buffer_full)
{
count++;
}
return count;
}
-O2 优化等级其汇编代码为:
read_stream PROC
LDR r1, |L1.28|
MOV r0, #0
LDR r1, [r1, #0]
|L1.12|
CMP r1, #0
ADDEQ r0, r0, #1
BEQ |L1.12| ; infinite loop
BX lr
ENDP
|L1.28|
DCD ||.data||
AREA ||.data||, DATA, ALIGN=2
buffer_full
DCD 0x00000000
使用 volatile 修饰 buffer_full 时,-O2下汇编代码为:
read_stream PROC
LDR r1, |L1.28|
MOV r0, #0
|L1.8|
LDR r2, [r1, #0]; ; buffer_full
CMP r2, #0
ADDEQ r0, r0, #1
BEQ |L1.8|
BX lr
ENDP
|L1.28|
DCD ||.data||
AREA ||.data||, DATA, ALIGN=2
buffer_full
DCD 0x00000000
可以看到,没有使用 volatile 时,while 循环查询的变量被优化了(只查询一次变量,并把它存在寄存器中,循环结束条件判断直接和保存变量值的寄存器进行比较,而不再更新寄存器的值),而 volatile 修饰后,则每次循环都查询(可以在汇编中看到,每一次循环体执行开始,首先会加载变量值到寄存器中)。
特别是在中断、多线程及寄存器读取中一定要注意使用 volatile 修饰易变的变量。
当函数没有读、或者写全局变量的函数,可以使用 __attribute__((const)) 或者 __pure 修饰
可以使用__attribute__((pure))修饰
没有读写全局变量的函数,任何时候调用(只要传入参数相同)都会返回相同的结果,编译器可以利用此信息进行优化,示例如下:
int fact(int n)
{
int f = 1;
while (n > 0)
f *= n--;
return f;
}
int foo(int n)
{
return fact(n)+fact(n);
}
-O2 下生成的汇编
fact PROC
...
foo PROC
MOV r3, r0
PUSH {lr}
BL fact
MOV r2, r0
MOV r0, r3
BL fact
ADD r0, r0, r2
POP {pc}
ENDP
可以看到,此时汇编代码中调用了两次fact函数,然后对其结果进行累加。
fact 由 __pure 修饰
int fact(int n) __pure
{
int f = 1;
while (n > 0)
f *= n--;
return f;
}
int foo(int n)
{
return fact(n)+fact(n);
}
同样编译条件下生成的汇编为:
fact PROC
...
foo PROC
PUSH {lr}
BL fact
LSL r0,r0,#1
POP {pc}
ENDP
这时,只调用了一次fact函数,然后对结果直接 LSL 左移一位。
C 语言编译出来的汇编代码具有自然对齐特性,如下所示:
以上自然对齐特性编译出来的汇编指令执行效率很高。
例如以下 struct 在 a 和 c 之间会有三个字节空隙:
struct example_st {
int a;
char b;
int c;
}
可以使用以上修饰的对象为包括:struct, union, pointer
对于结构体,有两种方式进行修饰:
__packed struct mystruct {
char c;
short s;
} /* not recommended */
不建议使用这种方式,因为这样会增大结构体中自然对齐数据的访问时间。
建议单独对结构体重非对齐的数据进行定义:
struct mystruct {
char c;
__packed short s;
}
一般情况下不建议使用非自然对齐数据。
//注释符使用起来更为方便
C99支持以下循环方式:
for(int i = 100; i > 0; i--)
{
//do something
}
使用起来更为方便
*结构体赋值方式
struct mystruct {
const char *name;
int age;
}
mustruct person = {.name = "wxj", .age = 22};
#include
int test(int n)
{
assert(n > 0);
int array[n];
//do something
}
注意:动态数据只能用于局部变量,并且在生成之后不可以再改变其数据长度;动态数据的数据存在 heap 中。
可以代表当前的函数
void foo(void)
{
printf("This function is called '%s'.\n", __func__);
}
Keil中也可以使用 __FUNC__, 其功能和 __FILE__, __LINE__ 相似,主要用于 DEBUG 输出调试信息。
#define LOG(format, ...) fprintf(stderr, format, __VA_ARGS__)
表明两个指针指向同一个地址,下例是不允许的
void copy_array(int n, int *restrict a, int *restrict b)
{
while (n-- > 0)
*a++ = *b++;
}
#include
bool flag = false;
C对寄存器和栈的使用遵循 ARM Architecture Procedure Call Standard (AAPCS), 其内容主要规定了C函数调用时参数传递、结果返回、中间变量等C函数过程对寄存器和栈使用。
按照以上过程,就可以实现C和汇编的嵌入。
为了减少C执行过程中对栈的使用,可以采取如下方式:
int test(void)
{
int local;
{
int a;
// do something
}
{
int b;
// do something
}
return local;
}
C默认未显式初始化的全局变量均初始化为0,但在某些情况下不希望被初始化为0,可以采用如下两种方式:
#pragma arm section zidata = "non_initialized"
/* uninitialized data in non_initialized section
* (without the pragma, would be in .bss section by default)
*/
int i, j;
#pragma arm section zidata /* back to default (.bss section) */
int k = 0, l = 0; /* zero-initialized data in .bss section */
__attribute__((section("no_initialized"))) int i, j;
其使用情境为错误快速恢复,当嵌入式系统出现错误时,可以设置保持RAM的供电,进行SoftReset,这时没有初始化的全局变量的值仍然保持,可以快速恢复现场。
The __packed qualifier does not affect type in GNU mode.
以上四种类型的编译器控制选项,可以只使用一种类型,也可以同时混合使用,有些控制选项的功能一致,可以互换,有些则为特有功能。
以下只是简单介绍,以便于读者理解,详细介绍请自行阅读ARM编译器使用手册,链接附于文末。
以下主要以 __attribute__ 中常用的属性对其使用进行示例说明:
__attribute__((always_inline)) 函数最大可能内联
__attribute__((noinline)) 禁止函数内联
static int max(int x, int y) __attribute__((always_inline));
static int max(int x, int y)
{
return x > y ? x : y; // always inline if possible
}
int fn(void) __attribute__((noinline));
int fn(void)
{
return 42;
}
__attribute__((pure)) 表示函数不写全局内存
__attribute__((const) 表示函数不读、写全局内存
#include
// __attribute__((const)) functions do not read or modify any global memory
int my_double(int b) __attribute__((const));
int my_double(int b) {
return b*2;
}
int main(void) {
int i;
int result;
for (i = 0; i < 10; i++)
{
result = my_double(i);
printf (" i = %d ; result = %d \n", i, result);
}
}
__attribute__((constructor[(priority)])) 表示函数在程序进入 main() 之后自动执行, priority 为 100 及大于 100 的数, 数字越小,越先执行,并且 100 为默认值。
int my_constructor(void) __attribute__((constructor));
int my_constructor2(void) __attribute__((constructor(101)));
int my_constructor3(void) __attribute__((constructor(102)));
int my_constructor(void) /* This is the 3rd constructor */
{ /* function to be called */
...
return 0;
}
int my_constructor2(void) /* This is the 1st constructor */
{ /* function to be called */
...
return 0;
}
int my_constructor3(void) /* This is the 2nd constructor */
{ /* function to be called */
...
return 0;
}
__attribute__((destructor[(priority)])) 表示函数在 main() 函数执行完成,或者 exit() 开始执行时调用
int my_destructor(void) __attribute__((destructor));
int my_destructor(void) /* This function is called after main() */
{ /* completes or after exit() is called. */
...
return 0;
}
__attribute__((weak)) 表明函数是弱链接的,如果有比它强的连接,则调用其他函数
int func(void) __attribute__((weak));
int func(void) __attribute__((weak))
{
// do something
}
int func(void) ;
int func(void)
{
// do something
}
int main(void)
{
//do something
func();
// do something
return 0;
}
__attribute__((weakref("target"))) 表明此函数应该链接名称为target的函数
extern void y(void);
static void x(void) __attribute__((weakref("y")));
void foo (void)
{
...
x();
...
}
以上函数中 foo 调用的是 y。
以上是用法的简单示例,编译器控制特性的使用,能够最大程度上优化代码,并具有极大的灵活性。
有些CPU的控制 C无法直接办到,需要使用内联汇编,但是以上这些内置指令直接实现为汇编,可以直接使用这些指令控制 CPU 的行为,例如 __wfe, __wfi可以控制 CPU 的休眠特性。
#define debug(format, ...) fprintf (stderr, format, __VA_ARGS__)
void variadic_macros(void)
{
debug ("a test string is printed out along with %x %x %x\n", 12, 14, 20);
}
例如:
static int y = c + 10;
在标准C语言上是不允许的, 但是编译器拓展允许这种方式。
在标准C语言里, void * 空指针只可以和结构体、联合体、变量、其他指针等互转,但是和函数指针的转换属于未定义行为,而Keil所做的编译器拓展,允许void * 和函数指针的互换。
制定变量存储于寄存器
void foo(void)
{
register int i;
int *j = &i;
}
需要使用 - -gnu 编译选项
extern char STACK$$Base;
extern char STACK$$Length;
#define STACK_BASE &STACK$$Base
#define STACK_TOP ((void*)((uint32_t)STACK_BASE + (uint32_t)&STACK$$Length))
例如替换 foo() 函数:
extern void ExtraFunc(void);
extern void $Super$$foo(void):
/* this function is called instead of the original foo() */
void $Sub$$foo(void)
{
ExtraFunc(); /* does some extra setup work */
$Super$$foo(); /* calls the original foo() function */
/* To avoid calling the original foo() function
* omit the $Super$$foo(); function call.
*/
}
$Supper$$foo 指代原来的函数 $Sub$$foo 用来替换的新函数,则链接器链接此函数取代原来的foo()函数。
以上为对KEIL平台的工具的基本认识,如果需要进一步学习,可以阅读KEIL的官方文档 ARM Product Manuals
Author : 王小军
Email :[email protected]