写出高性能的C代码---深入理解编译器和硬件平台

 写这篇文章之前,首先需要感谢我的部长,带我打开了这扇门,以下的知识分享来源于部长的传授。对于底层嵌入式开发而言,现在一份程序的空间利用率已经随着硬件的升级而降低了要求,但时间利用率依然重要。

  那么如何写出一份高性能的代码呢,再次之前我先阐述下编译器对.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等特殊数字求余时,可以使用&。

猜你喜欢

转载自blog.csdn.net/wjb123sw99/article/details/81147003