`
simohayha
  • 浏览: 1400178 次
  • 性别: Icon_minigender_1
  • 来自: 火星
社区版块
存档分类
最新评论

gcc的几个自动优化

阅读更多
我的gcc版本是4.4.1

先来看const和define以及enum定义,编译器会做什么样的优化:

enum { constant=23 };
#define CONSTANT 23
static  const int Static_Constant=23;
const int Constant = 23;

int foo() {
    a(constant+3);
    a(CONSTANT+4);
    a(Static_Constant+5);
    return Static_Constant + Constant;
}


ok,我们然后来看对应的汇编代码(我这里用O2编译):

foo:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
//constant+3=26
	movl	$26, (%esp)
	call	a
///CONSTANT+4=27
	movl	$27, (%esp)
	call	a
///Static_Constant+5=28
	movl	$28, (%esp)
	call	a
///Static_Constant + Constant = 23 + 23 = 46
	movl	$46, %eax
	leave
	ret
	.size	foo, .-foo
.......................................

///被保留。
Constant:
	.long	23
	.ident	"GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"


1 可以看到编译器会将它们的值直接计算然后再调用函数a,也就是+那一步被编译器优化掉了。

2 没有static修饰的const变量的那块内存被保留了,尽管foo并没有引用这块内存。

接下来看宏和内联,编译器会做什么不同的优化:

先看测试代码:

#define abs(x) ((x)>0?(x):-(x))

static long abs2(long x) { 
    return x >= 0 ? x : -x; 
}  

long foo2(long a) {
    return abs(a);
}


long bar(long a) { 
    return abs2(a);
}


然后来看汇编代码。这里只是汇编代码·片断:

foo2:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %eax
	popl	%ebp
	movl	%eax, %edx
	sarl	$31, %edx
	xorl	%edx, %eax
	subl	%edx, %eax
	ret
	.size	foo2, .-foo2
	.p2align 4,,15
.globl bar
	.type	bar, @function
bar:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %eax
	popl	%ebp
	movl	%eax, %edx
	sarl	$31, %edx
	xorl	%edx, %eax
	subl	%edx, %eax
	ret
	.size	bar, .-bar
	.p2align 4,,15


bar和foo2的代码完全一样,由此可见bar函数自动被内联了。

下面这个测试我用的gcc是4.3

现在我们来去掉static,然后来看会出现什么情况,源码:

long abs2(long x) { 
    return x >= 0 ? x : -x; 
}  

long foo2(long a) {
    return abs(a);
}


long bar(long a) { 
    return abs2(a);
}


来看汇编:


abs2:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %eax
	popl	%ebp
	movl	%eax, %edx
	sarl	$31, %edx
	xorl	%edx, %eax
	subl	%edx, %eax
	ret
bar:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %eax
	popl	%ebp
	movl	%eax, %edx
	sarl	$31, %edx
	xorl	%edx, %eax
	subl	%edx, %eax
	ret


可以看到依然会被内联,可是和没有加static相比,会生成abs2这个段.这是因为编译器要做最保守的处理,如果不加static的话,abs2可能还被其他的文件所调用,因此会保留这个abs2.

这里来做个总结吧.实验代码就不贴了,有兴趣可以自己去试试.

1 用O3编译,函数加不加static ,都会被内联.


2 如果函数被调用一次,并且函数体很大

如果为static修饰的函数,他无论如何都会被内联.

如果不加static的话,就需要你强制O3优化了,用O3也会生成abs2那个段.如果O2编译,则会生成跳转指令.

3 如果函数被调用多次.

加不加static修饰的函数,调用次数超过一定的数值都会生成跳转指令.


因此我们一个文件内的内部函数尽量都声明为static的。

接下来来看数组的边界检测,编译器会如何优化:

static char array[100000];

static int write_to(int ofs,char val) {
///检测边界(这个会被优化掉)
    if (ofs>=0 && ofs<100000)
        array[ofs]=val;
}

int main() {
    int i;
    for (i=0; i<100000; ++i) array[i]=0;//<这里没有检测边界
    for (i=0; i<100000; ++i) write_to(i,-1);
}


然后来看对应的汇编代码,其中L6表示第一个赋值,L7为第二个赋值:


.L6:
	movl	$0, array(%eax)
	addl	$4, %eax
	cmpl	$100000, %eax
	jne	.L6
	xorl	%eax, %eax
	.p2align 4,,7
	.p2align 3
.L7:
	movl	$-1, array(%eax)
	addl	$4, %eax
	cmpl	$100000, %eax
///可以看到边界检测被优化掉了
	jne	.L7
	popl	%ebp
	ret
	.size	main, .-main
	.p2align 4,,15


可以看到生成了相同的代码,边界检测被编译器优化了(也就是删除掉了).也就是循环的时候(如果我们for循环的边界检测刚好包含本身的边界检测的话,我们不需要多余的边界检测.)如果将for循环的边界检测改为大于100000的话,我们就会看到,编译器会生成相应的边界检测的.


static char array[100000];
 static int write_to(int ofs,char val){
          if(ofs>=0&&ofs<99999)
                  array[ofs]=val;
  }
  int main(){
         int i;
          for(i=0;i<99998;++i)array[i]=0;
          for(i=1;i<100000;++i)write_to(i,-1);
 }


汇编代码(只看write_to那部分):


.L6:
	movb	$-1, array(%eax)
	addl	$1, %eax
	cmpl	$100000, %eax
	je	.L4
	cmpl	$99999, %eax
	jne	.L6
.L4:
	popl	%ecx
	popl	%ebp
	leal	-4(%ecx), %esp


可以看到当for的边界检测包含write_to的边界的时候就会生成这段.

再来看另外的一些边界检测优化:


int regular(int i) {
///这里判断i的边界。
    if (i>5 && i<100)
        return 1;
    exit(0);
}


来看regular的汇编代码:

regular:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	movl	8(%ebp), %eax
///先将i减去6
	subl	$6, %eax
///然后和93比较
	cmpl	$93, %eax
	ja	.L18
	movl	$1, %eax
	leave
	ret
.L18:
///设置返回值为0
	movl	$0, (%esp)
	call	exit


可以看到i>5 && i<100被优化为 i-6 <93。并且编译器知道exit不会有返回值,因此还会设置正确的返回值。

接下来来看for和while,编译器会有什么不一样的处理:


char array[100000];
int foo5(int a)
{
    int i;
    for (i=1; i<a; i++)
        array[i]=array[i-1]+1; 

}

void foo6(int a)
{
    int i =1;
    while (i<a) {
        array[i]=array[i-1]+1;
        i++;
    }
}


来看生成的汇编:

foo5:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %ecx
	pushl	%ebx
	cmpl	$1, %ecx
	jle	.L23
	movzbl	array, %ebx
	movl	$1, %eax
foo6:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %ecx
	pushl	%ebx
	cmpl	$1, %ecx
	jle	.L29
	movzbl	array, %ebx
	movl	$1, %eax


可以看到完全一模一样,我记得在老的版本的gcc中,for和while生成的汇编是不一样的。

还有gcc的switch优化为跳转表可以看我前面的blog.

还有一些就是数值计算的优化了,不过这里就不介绍了。








分享到:
评论
2 楼 lurker0 2010-02-13  
》》可以看到i>5 && i<100被优化为 i-6 <93。

如果i为99  左侧条件为真,右侧条件为假,就不等价。

这里是不是有问题?
1 楼 seen 2010-02-03  
>>可以看到i>5 && i<100被优化为 i-6 <93

从数学上来说是说不通的。
这里有点意思。。。以前不知道还有这一手。。。在CF上玩了点小花样

相关推荐

    GCC编译优化指南 很好的资料

    文档虽然没有详细列举所有GCC优化标志,但根据上下文推测,以下几个方面是进行GCC优化时应关注的重点: 1. **编译器警告标志**:使用如`-Wall`、`-Wextra`等标志来开启更多的警告信息,可以帮助开发者发现潜在的...

    gcc教程gcc打包资料

    GCC教程通常会涵盖以下几个关键部分: 1. **安装与配置**:首先,你需要了解如何在不同的操作系统上安装GCC,如在Ubuntu、Windows或MacOS上。这可能涉及到下载源代码、配置编译选项、编译和安装过程。 2. **基本...

    gcc 4.8.5离线安装包

    安装GCC 4.8.5的步骤通常包括以下几个阶段: 1. **解压**: 首先,你需要使用`tar`命令来解压`gcc.tar.gz`文件。例如,你可以使用`tar -zxvf gcc.tar.gz`命令将其解压到当前目录。 2. **配置**: 解压完成后,进入...

    GCC.中文手册_it_gcc中文手册_gcc手册_

    GCC的编译过程分为几个主要阶段:预处理、编译、汇编和链接。预处理阶段处理宏定义、条件编译指令,并生成.i中间文件。编译阶段将预处理后的代码转换为汇编语言,生成.s文件。汇编阶段将汇编代码转换成机器码,形成....

    gcc2.95完整安装包

    2. **配置**: 进入解压后的目录,运行`./configure`脚本,这是一个自动配置脚本,会检查用户的系统环境并生成适合的Makefile文件。在运行前可能需要设置一些环境变量,例如指定安装路径和依赖库的位置。 3. **编译*...

    tdm-gcc-10.3.0.exe

    TDM-GCC的安装通常包括以下几个步骤: 1. 下载并运行"tdm-gcc-10.3.0.exe"。 2. 按照安装向导的指示进行,选择安装路径和组件。 3. 安装完成后,确保将编译器的路径添加到系统的PATH环境变量中,以便在命令行中直接...

    linux系统gcc离线安装包

    在压缩包子文件的文件名称列表中,我们可以看到以下几个组件: 1. `gcc-4.8.5-28.el7.x86_64.rpm`:这是GCC主程序的RPM包,包含了编译器的基本功能,用于将源代码转换为可执行程序。 2. `kernel-headers-3.10.0-...

    TDM-GCC 4.9.2

    3. **易于安装**:压缩包中的"Dev-Cpp 5.11 TDM-GCC 4.9.2 Setup.exe"是一个安装程序,只需简单几步就能完成安装,省去了手动配置编译环境的繁琐过程。 4. **集成开发环境**:虽然TDM-GCC本身是一个编译器,但它...

    an introdution to gcc

    它的工作流程分为几个主要步骤:预处理、编译、汇编和链接。预处理阶段处理#include指令,宏替换和条件编译;编译阶段将预处理后的源代码转换为中间语言——汇编代码;汇编阶段将汇编代码转换为机器码;最后,链接器...

    Linux的GCC说明

    GCC的编译过程通常分为几个步骤: 1. **预处理**:预处理器(cpp)处理源代码中的宏定义、条件编译指令,并处理#include文件,生成.i(或E)文件。 2. **编译**:编译器(cc1或g++)将预处理后的代码转换为汇编语言...

    linux下GCC编译C程序

    在Linux环境下,C程序的编译通常涉及以下几个步骤: 1. **编译单个源文件**:创建一个简单的C程序,例如经典的"Hello World"程序,然后使用`gcc`命令编译。例如: ```bash gcc -o hello hello.c ``` 这会生成名...

    GCC中文手册.rar

    在GCC中,有以下几个关键的知识点: 1. **基本使用**:GCC的基本用法是通过命令行接口调用,例如`gcc -o 输出文件名 源代码文件名`。这将把源代码编译并链接成指定的输出文件。 2. **预处理阶段**:GCC首先进行...

    GCC参数大全资料合集,各种技巧

    在GCC的优化参数方面,有以下几个常用的选项: - `-O0`:关闭优化,这对于调试代码非常有用。 - `-O1`,`-O2`,`-O3`:分别代表不同程度的优化,`-O3`是最高级别的优化,但可能会增加编译时间。 - `-Os`:优化代码...

    tdm64-gcc-10.3.0-2.exe

    3. **配置**:TDM-GCC通常会自动配置好MinGW-w64运行时库,使得编译的程序可以直接在Windows上运行。用户可能需要根据自己的需求调整编译器的配置选项,例如优化级别、警告级别等。 4. **使用**:通过命令行工具...

    gcc-4.8.3.zip(第一部分)

    在GCC 4.8.3中,有以下几个重要的改进和新特性: 1. **C++11/14支持**:GCC 4.8系列开始全面支持C++11标准,并在4.8.3中进一步完善了C++14的一些特性,提升了标准库的实现质量。 2. **性能优化**:GCC 4.8.3包含了...

    An_Introduction_to_GCC_中文__入门全局观书籍

    GCC的发展历程可以概述为几个重要阶段: 1. GCC第一版于1987年发布,标志着自由软件开发领域的重大突破。 2. 1992年GCC推出2.0版本,引入了C++编译能力。 3. 1997年,为了改进优化和对C++的支持,GCC的实验性分支...

    GCC Frontend HOWTO

    通常,编译器的工作过程包括以下几个阶段: 1. **词法分析**:将源代码分解成有意义的符号或“标记”。 2. **语法分析**:将这些标记组织成抽象语法树(AST)。 3. **语义分析**:检查语法树是否符合语言的规则,并...

Global site tag (gtag.js) - Google Analytics