ARM NEON 编程系列9——ARM C语言编程优化策略(神文)

https://zhuanlan.zhihu.com/p/24402180


ARM NEON 编程系列9——ARM C语言编程优化策略(神文)_第1张图片

ARM C语言编程优化策略(KEIL平台)

王小军 王小军
8 个月前
  • ARM C语言编程优化策略
    • 1. 内容介绍
    • 2. 优化实战
      • 2.1. 编译器优化选项
      • 2.2. C循环优化
      • 2.3. 内联函数
      • 2.4. volatile 关键字的使用
      • 2.5. 纯净函数
      • 2.6. 数据对齐特性
      • 2.7. C99 中易用的特性
      • 2.8. C对栈和寄存器的使用
      • 2.9. 阻止未初始化变量初始为0
    • 3. 编译器特性
      • 3.1. 关键字
      • 3.2. __declspec 属性
      • 3.3. __attribute__
      • 3.4. pragmas
      • 3.5. 使用及说明
      • 3.6. 内置指令
    • 4. 常用编译器支持语言拓展
      • 4.1. C89/90 下可以使用的 C99标准
      • 4.2. 标准C语言拓展
    • 5. 链接器应用
      • 5.1. 访问 section 的相关特性
      • 5.2. 使用 $Super$$ 和 $Sub$$ 打补丁

1. 内容介绍

KEIL平台主要有四个工具:

  • armcc,C/C++编译器
  • armasm, 汇编器
  • armlnk,链接器
  • fromelf,产生二进制代码

其作用及关系如下图所示:

ARM NEON 编程系列9——ARM C语言编程优化策略(神文)_第2张图片

本文主要讲述KEIL平台编程的优化策略,主要内容如下:

  • 优化实战
  • 编译器特性
  • 语言拓展
  • 链接器应用

本文主要内容来源于ARM® Compiler v5.06 for μVision® Version 5 armcc User Guide

2. 优化实战

2.1. 编译器优化选项

代码体积vs执行速度

  • -Ospace

Keil编译器默认配置,主要目的是减少代码体积

  • -Otime

目的是加快执行速度

优化等级及调试信息

  • -O0

最少的优化,可以最大程度上配合产生代码调试信息,可以在任何代码行打断点,特别是死代码处。

  • -O1

有限的优化,去除无用的inline和无用的static函数、死代码消除等,在影响到调试信息的地方均不进行优化。在适当的代码体积和充分的调试之间平衡,代码编写阶段最常用的优化等级。

  • -O2

高度优化,调试信息不友好,有可能会修改代码和函数调用执行流程,自动对函数进行内联等。

  • -O3

最大程度优化,产生极少量的调试信息。会进行更多代码优化,例如循环展开,更激进的函数内联等。

另外,可以通过单独设置 --loop_optimization_level=option 来控制循环展开的优化等级。

2.2. C循环优化

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优化等级下,编译器会适当进行循环展开。

2.3. 内联函数

编译器在函数内联时的决策

函数内联是在代码体积和执行性能之间所做的权衡,是否内联由编译器决定。

如果优化选项是 -Ospace , 则编译器会倾向于比较少的内联,以减少代码体积;如果优化选项为 -Otime , 则编译器则会倾向于更多的内联,但也会避免代码体积的大量增加。

使用 __inline, __forceinline 等关键字(或者 attribute 属性控制, 或者 #pragma 属性控制也有相应的用法,详细下文会说), 可以给编译器建议,但是最终是否内联还是由编译器决定。

另外,内联函数的体积应该比较小。

编译器在以下情况下,如果有可能内联,则尽量内联:

  • __forceinline , __attribute__((always_inline)) 定义的函数
  • 编译选项中有 --forceinline

编译器在以下情况下,会在合适的情况下进行内联(编译器自己决定):

  • __inline, __attribute__((inline))修饰的函数
  • 优化选项为-O2,或-O3,或者存在 --autoinline,及时有些函数没有显式声明inline,也有可能inline

编译在决定是否内联时会综合考虑一下因素:

  • 函数的体积和被调用次数,小函数更易被内联,调用次数少更易被内联
  • 当前的优化选项等级,等级越高越易被内联
  • -Ospace vs -Otime, -Otime函数更易被内联
  • 函数链接属性是 external 还是 static,static更易被内联
  • 函数有多少形参
  • 函数的返回值是否被使用等

在编译器认为不合适的情况下,即使声明了 __forceinline 函数也不会内联。

inline和static

具有外部链接属性的函数在内联时会保留一份函数代码在最终的二进制文件中,所以外部链接属性的函数内联会增大代码体积。

但是声明为 static 的函数就不会存在这个问题,static 表明函数只需要内部调用,所以不需要再保留原来的代码。

注意,如果明确不需外部调用的函数,请加上 static

链接器在链接时不会自动去掉具有外部链接属性的函数,以防某些外部程序调用,可以使用以下方式去除无用函数:

  • --split_sections 这个编译选项会把每一个函数都单独放到一个section
  • __attribute__((section("name"))) 把特定的函数放到自定义的section中
  • --feedback 这个编译选项,会对代码进行两次编译分析其代码的使用,并去掉无用函数,keil推荐使用此种方案

避免 inline

由于inline会对debug造成影响,可以通过 - -no_inline 编译选项禁止所有的内联,或者通过 - -no_autoinline 禁止编译器自动内联。

2.4. volatile 关键字的使用

优化等级较高时,会出现一些低优化等级不会出现的问题,例如没有使用关键字 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 修饰易变的变量。

2.5. 纯净函数

  • 函数没有读、写全局内存

当函数没有读、或者写全局变量的函数,可以使用 __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 左移一位。

2.6. 数据对齐特性

  • 自然对齐特性

C 语言编译出来的汇编代码具有自然对齐特性,如下所示:

ARM NEON 编程系列9——ARM C语言编程优化策略(神文)_第3张图片

以上自然对齐特性编译出来的汇编指令执行效率很高。

例如以下 struct 在 a 和 c 之间会有三个字节空隙:

struct example_st {
    int a;
    char b;
    int c;
}
  • 利用 __packed 或 __attribute__((packed)) 非对齐数据

可以使用以上修饰的对象为包括:struct, union, pointer

对于结构体,有两种方式进行修饰:

__packed struct mystruct {
    char c;
    short s;
}   /* not recommended */

不建议使用这种方式,因为这样会增大结构体中自然对齐数据的访问时间。

建议单独对结构体重非对齐的数据进行定义:

struct mystruct {
    char c;
    __packed short s;
}

一般情况下不建议使用非自然对齐数据。

2.7. C99 中易用的特性

  • // 注释符号

//注释符使用起来更为方便

  • 定义和语句混合

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 中。

  • __func__ 宏定义

可以代表当前的函数

void foo(void)
{
    printf("This function is called '%s'.\n", __func__);
}

Keil中也可以使用 __FUNC__, 其功能和 __FILE__, __LINE__ 相似,主要用于 DEBUG 输出调试信息。

  • 宏定义中可以使用变长参数
#define LOG(format, ...) fprintf(stderr, format, __VA_ARGS__)
  • restrict 指针

表明两个指针指向同一个地址,下例是不允许的

void copy_array(int n, int *restrict a, int *restrict b)
{
    while (n-- > 0)
        *a++ = *b++;
}
  • 新增布尔类型
#include 

bool flag = false;

2.8. C对栈和寄存器的使用

C对寄存器和栈的使用遵循 ARM Architecture Procedure Call Standard (AAPCS), 其内容主要规定了C函数调用时参数传递、结果返回、中间变量等C函数过程对寄存器和栈使用。

  • 其规定了C函数的形参通过R0-R3四个寄存器传递,其余通过栈传递,所以函数的参数一般不要超过四个,如果参数过多,可以通过结构指针的方式传入;
  • 调用函数时,父函数需要保存R0-R3中自己需要用到的寄存器,子函数刚进入时需要保存R4-R12,SP,LR,PC及XPSR需要使用的寄存器。

按照以上过程,就可以实现C和汇编的嵌入。

为了减少C执行过程中对栈的使用,可以采取如下方式:

  • 函数要小,并且使用少量的局部变量;
  • 避免大的数组和结构体;
  • 避免递归;
  • 使用C的局部域,并且只在变量需要时才声明,这样可以做到栈内存复用,例如:
int test(void)
{
    int local;
    {
        int a;
        // do something
    }
    
    {
        int b;
        // do something
    }
    
    return local;
}

2.9. 阻止未初始化变量初始为0

C默认未显式初始化的全局变量均初始化为0,但在某些情况下不希望被初始化为0,可以采用如下两种方式:

  • 方式一:使用pragma
#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
__attribute__((section("no_initialized"))) int i, j;

其使用情境为错误快速恢复,当嵌入式系统出现错误时,可以设置保持RAM的供电,进行SoftReset,这时没有初始化的全局变量的值仍然保持,可以快速恢复现场。

3. 编译器特性

3.1. 关键字

ARM NEON 编程系列9——ARM C语言编程优化策略(神文)_第4张图片

3.2. __declspec 属性

ARM NEON 编程系列9——ARM C语言编程优化策略(神文)_第5张图片

3.3. __attribute__

  • 函数属性
ARM NEON 编程系列9——ARM C语言编程优化策略(神文)_第6张图片

  • 类型属性
ARM NEON 编程系列9——ARM C语言编程优化策略(神文)_第7张图片

The __packed qualifier does not affect type in GNU mode.

  • 变量属性
ARM NEON 编程系列9——ARM C语言编程优化策略(神文)_第8张图片

3.4. pragmas

ARM NEON 编程系列9——ARM C语言编程优化策略(神文)_第9张图片

3.5. 使用及说明

以上四种类型的编译器控制选项,可以只使用一种类型,也可以同时混合使用,有些控制选项的功能一致,可以互换,有些则为特有功能。

以下只是简单介绍,以便于读者理解,详细介绍请自行阅读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。

以上是用法的简单示例,编译器控制特性的使用,能够最大程度上优化代码,并具有极大的灵活性。

3.6. 内置指令

ARM NEON 编程系列9——ARM C语言编程优化策略(神文)_第10张图片

有些CPU的控制 C无法直接办到,需要使用内联汇编,但是以上这些内置指令直接实现为汇编,可以直接使用这些指令控制 CPU 的行为,例如 __wfe, __wfi可以控制 CPU 的休眠特性。

4. 常用编译器支持语言拓展

4.1. C89/90 下可以使用的 C99标准

  • 在 C89/90 语言标准下可以使用 // 注释
  • 可变参数宏
#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);
}
  • long long 与 unsigned long long, 和 C89/90 使用的 __int64 功能相同
  • restrict pointer

4.2. 标准C语言拓展

  • 常数表达式

例如:

static int y = c + 10;

在标准C语言上是不允许的, 但是编译器拓展允许这种方式。

  • void * 空指针可以和函数指针互相转换

在标准C语言里, void * 空指针只可以和结构体、联合体、变量、其他指针等互转,但是和函数指针的转换属于未定义行为,而Keil所做的编译器拓展,允许void * 和函数指针的互换。

  • register

制定变量存储于寄存器

void foo(void)
{
    register int i;
    int *j = &i;
}
  • 支持所有 GNU 对C语言的拓展

需要使用 - -gnu 编译选项

5. 链接器应用

5.1. 访问 section 的相关特性

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))

5.2. 使用 $Super$$ 和 $Sub$$ 打补丁

例如替换 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]

「真诚赞赏,手留余香」
1 人赞赏
TSCHI ZHANG
ARM 编译器 C(编程语言)





你可能感兴趣的:(嵌入式基础)