写这篇文章之前,首先需要感谢我的部长,带我打开了这扇门,以下的知识分享来源于部长的传授。对于底层嵌入式开发而言,现在一份程序的空间利用率已经随着硬件的升级而降低了要求,但时间利用率依然重要。
那么如何写出一份高性能的代码呢,再次之前我先阐述下编译器对.c文件编译的行为。
一 编译步骤
1:预编译
1)展开头文件
2)执行预编译操作符,例如:#include、 #define MAX_CNT (100*100、#if #ifdef #error…
3)特殊符号:__LINE__、__DATE__、___TIME__
TIP:预编译的时候不会检查语法错误
2:编译
1)将源码转化为机器指令的过程,但该过程不是逐字逐句翻译,源码与机器码不是一一对应的关系
2)编译时不确定数据和代码的地址
3:链接
1)将目标文件(.o .a)彼此关联,生成可执行文件的过程
2)变量和函数的地址是在链接的环节确定下来的
TIP:
RO(text)段------存放程序代码和只读数据
对于支持字节访问模式的存储设备,例如nor flash,RO段可以直接复用程序的存储空间
对于块存储设备,必须在程序执行时将RO段数据搬移到RAM中
RW(data)段-----存放具有处置的全局数据(变量、数组等)
因为要支持动态改写,所以RW段的数据必然存储于RAM中,但RW段的初始化数据存放在静态存储区中
BSS段----存放没有赋初值的全局数据(变量、数组等)
1、因为要支持动态改写、所以RW段的数据也存储在RAM中
2、与RW段数据不同的是,由于不需要存放初值,所以BSS段的数据几乎不占静态存储空间,但会占相应尺寸的运行空间
因此当我们需要定义一个常量时,最好前缀const,这不仅仅防止常量被篡改,也节省了运行空间。例如:
const u8 mac_addr[]={0x8c, 0x1e, 0x23, 0Xb9, 0Xab, 0x10};
占用静态存储空间: 6byte ROM
占用运行空间: 0 byte
u8 mac_addr[]={0x8c, 0x1e, 0x23, 0Xb9, 0Xab, 0x10};
占用静态存储空间: 6byte ROM
占用运行空间: 6 byte RAM
二 编译器的优化
大多数程序和库在编译时默认的优化级别是"2"(使用gcc选项:"-O2"),但你也可以使用更高的选项-O3、-O4、-O5等使程序性能得到优化。
例如下面1.c文件
int main()
{
int i;
for(i=0; i<3000; i++)
{
if(i>3000)
return 3000;
}
return 0;
}
使用gcc text.c 后生成的汇编语言为
.file "1.c"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $0, -4(%rbp)
jmp .L2
.L5:
cmpl $3000, -4(%rbp)
jle .L3
movl $3000, %eax
jmp .L4
.L3:
addl $1, -4(%rbp)
.L2:
cmpl $2999, -4(%rbp)
jle .L5
movl $0, %eax
.L4:
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
使用优化选项gcc 1.c –O3后生成的汇编语言为
.file "1.c"
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
xorl %eax, %eax
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
可以看出gcc还是很聪明的,辨别出循环语句为无用功,省略了该步骤。
Gcc编译时不加优化选项
编译器会对源码进行逐条翻译,保证编译后生成的指令跟源码的行为是对应的。
Gcc编译时加优化选项
编译器会尝试猜测编码者的意图,并以此为目的对代码进行合并、精简、重排等操作。
优化导致的结果
1)编译后生成的指令码在行为,顺序上都不能保证与源码一一对应,尤其是在比较高的优化等级下。
2)编译器会尽可能使用一条复杂指令来完成多步简单的操作(SIMD)
3)编译器会直接干掉其认为没有实际意义的代码
三:利用硬件的加速特性
硬件的强项在于计算的并行性,如果软件程序能充分调用硬件的并行特性,将使系统的性能得到极大的提升。
1)深入了解你正在使用的CPU的指令集,了解不同的操作需要消耗多少机器指令,了解哪些指令具有并发特性(SIMD);
2)对于密集的数据访问类操作,尽量使用与CPU数据总线位宽相同的局部变量.
3)如果有标准的库函数可以调用,就不要自己写,绝大多数的人水平远远达不到编写标准库大牛的水平
4)编译器尽管很聪明,但源码要尽量的引向你想要的优化方向.
TIP:
例如对一个数求余
3.c:
int main()
{
int i=118;
printf( "%d\n",i%8);
return 0;
}
Gcc 3.c –S 后得到该该段代码的汇编语言
.file "3.c"
.section .rodata
.LC0:
.string "%d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $118, -4(%rbp)
movl -4(%rbp), %eax
cltd
shrl $29, %edx
addl %edx, %eax
andl $7, %eax
subl %edx, %eax
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
而使用下面这种方式求余
4.c:
int main()
{
int i=118;
printf("%d\n", i&0x07);
return 0;
}
Gcc 4.c –S 得到的汇编文件为:
.file "4.c"
.section .rodata
.LC0:
.string "%d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $118, -4(%rbp)
movl -4(%rbp), %eax
andl $7, %eax
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
二者对边i%8对应的汇编为
cltd
shrl $29, %edx
addl %edx, %eax
andl $7, %eax
subl %edx, %eax
i&0x07对应的汇编为
andl $7, %eax
i%8 和i&0x07虽然都能达到求余的目的,但i&0x07明显执行的操作更加少。
应此对于2、4、8、16等特殊数字求余时,可以使用&。