`
ppgunjack
  • 浏览: 80293 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

C++中函数指针与函数内联优化的关联性

阅读更多
c++对内联优化的处理是个很重要的知识点,对这个问题的考虑来自这个帖子:
http://www.iteye.com/topic/1055377,其中涉及的另一个链接http://blog.csdn.net/yongzhewuwei_2008/archive/2006/11/16/1387476.aspx,提到了Java在运行时对多态函数的内联优化。
在c++中通过基类指针调用的多态函数是无法被内联优化的,因为基类指针实际指向的对象是基类还是子类是在运行时才能确定的,因此是无法被内联化的。
需要注意的是,造成无法内联化的不是多态或者继承本身,根本原因是在于静态编译条件下对函数指针的调用无法定位到静态代码地址,因此无法将用函数指针来进行函数调用的地方用所调用代码内联化。
举个例子:
void fn1(){
}
void fn2(){
}
int main(){
    void (*pf)(void);

    for(int i=0;i<1000;i++) {
        if(i>20) {
            pf=&fn1;
        }else{
            pf=&fn2;
        }
        (*pf)();
    }
}

在gcc O0下生成的汇编如下:
main.o:     file format pe-i386


Disassembly of section .text:

00000000 <__Z3fn1v>:
   0:	55                   	push   %ebp
   1:	89 e5                	mov    %esp,%ebp
   3:	c9                   	leave  
   4:	c3                   	ret    

00000005 <__Z3fn2v>:
   5:	55                   	push   %ebp
   6:	89 e5                	mov    %esp,%ebp
   8:	c9                   	leave  
   9:	c3                   	ret    

0000000a <_main>:
   a:	55                   	push   %ebp
   b:	89 e5                	mov    %esp,%ebp
   d:	83 e4 f0             	and    $0xfffffff0,%esp
  10:	83 ec 10             	sub    $0x10,%esp
  13:	e8 00 00 00 00       	call   18 <_main+0xe>
  18:	c7 44 24 08 00 00 00 	movl   $0x0,0x8(%esp)		//初始化i
  1f:	00 
  20:	eb 23                	jmp    45 <_main+0x3b>
  22:	83 7c 24 08 14       	cmpl   $0x14,0x8(%esp)		//比较i和20大小
  27:	7e 0a                	jle    33 <_main+0x29>
  29:	c7 44 24 0c 00 00 00 	movl   $0x0,0xc(%esp)		//函数fn1地址赋予pf
  30:	00 
  31:	eb 08                	jmp    3b <_main+0x31>
  33:	c7 44 24 0c 05 00 00 	movl   $0x5,0xc(%esp)		//函数fn2地址赋予pf
  3a:	00 
  3b:	8b 44 24 0c          	mov    0xc(%esp),%eax
  3f:	ff d0                	call   *%eax			//通过函数指针pf调用函数
  41:	ff 44 24 08          	incl   0x8(%esp)
  45:	81 7c 24 08 e7 03 00 	cmpl   $0x3e7,0x8(%esp)
  4c:	00 
  4d:	0f 9e c0             	setle  %al
  50:	84 c0                	test   %al,%al
  52:	75 ce                	jne    22 <_main+0x18>
  54:	b8 00 00 00 00       	mov    $0x0,%eax
  59:	c9                   	leave  
  5a:	c3                   	ret    
  5b:	90                   	nop

上面代码逻辑很清楚了,循环根据条件修改pf变量,而pf会读取到eax寄存器,然后通过call   *%eax进行函数调用,那么如果在call   *%eax处内联函数,则根本没法解决到底内联fn1还是fn2的问题。
在gcc O3下生成的汇编如下:
main.o:     file format pe-i386


Disassembly of section .text:

00000000 <__Z3fn1v>:
   0:	55                   	push   %ebp
   1:	89 e5                	mov    %esp,%ebp
   3:	c9                   	leave  
   4:	c3                   	ret    
   5:	8d 76 00             	lea    0x0(%esi),%esi

00000008 <__Z3fn2v>:
   8:	55                   	push   %ebp
   9:	89 e5                	mov    %esp,%ebp
   b:	c9                   	leave  
   c:	c3                   	ret    
   d:	8d 76 00             	lea    0x0(%esi),%esi

00000010 <_main>:
  10:	55                   	push   %ebp
  11:	89 e5                	mov    %esp,%ebp
  13:	83 e4 f0             	and    $0xfffffff0,%esp
  16:	53                   	push   %ebx
  17:	83 ec 0c             	sub    $0xc,%esp
  1a:	e8 00 00 00 00       	call   1f <_main+0xf>
  1f:	31 db                	xor    %ebx,%ebx	//ebx清零
  21:	b8 08 00 00 00       	mov    $0x8,%eax	//函数fn2地址赋予eax
  26:	66 90                	xchg   %ax,%ax		//2字节无用指令对齐地址位(追求4整数地址)?不太确定
  28:	ff d0                	call   *%eax		//调用fn2
  2a:	43                   	inc    %ebx		//i++
  2b:	81 fb e8 03 00 00    	cmp    $0x3e8,%ebx	//判断循环,ebx充当i
  31:	74 15                	je     48 <_main+0x38>	//相等结束循环
  33:	83 fb 14             	cmp    $0x14,%ebx	//i和20比较
  36:	7f 18                	jg     50 <_main+0x40>	//i>20跳转到50
  38:	b8 08 00 00 00       	mov    $0x8,%eax	//函数fn2地址赋予eax
  3d:	ff d0                	call   *%eax		//调用fn2
  3f:	43                   	inc    %ebx
  40:	81 fb e8 03 00 00    	cmp    $0x3e8,%ebx
  46:	75 eb                	jne    33 <_main+0x23>
  48:	31 c0                	xor    %eax,%eax
  4a:	83 c4 0c             	add    $0xc,%esp
  4d:	5b                   	pop    %ebx
  4e:	c9                   	leave  
  4f:	c3                   	ret    
  50:	b8 00 00 00 00       	mov    $0x0,%eax	////函数fn1地址赋予eax
  55:	eb d1                	jmp    28 <_main+0x18>
  57:	90                   	nop

可以看到在O3优化下,编译器使用了寄存器来代替函数指针变量pf和循环变量i,但依然无法将fn1和fn2内联化。

下面看个稍复杂点的例子:
void fn1(){
}
void fn2(){
}
bool isFn2(void (*pf)(void)){
    if(pf==&fn2) {
        return true;
    }
    return false;
}
int main(){
    void (*pf)(void);
    for(int i=0;i<1000;i++) {
        if(isFn2(pf)) {
            pf=&fn1;
        }else{
            pf=&fn2;
        }
        (*pf)();
    }
}

上面代码逻辑可以看到,将调用的函数指针变量pf到底是否指向fn1还是fn2取决于函数isFn2()的返回,isFn2()会根据当前的函数指针pf指向来来判断返回结果。
在O0优化下,下面可以很明了的看到其跳转逻辑:
00000000 <__Z3fn1v>:
   0:	55                   	push   %ebp
   1:	89 e5                	mov    %esp,%ebp
   3:	c9                   	leave  
   4:	c3                   	ret    

00000005 <__Z3fn2v>:
   5:	55                   	push   %ebp
   6:	89 e5                	mov    %esp,%ebp
   8:	c9                   	leave  
   9:	c3                   	ret    

0000000a <__Z5isFn2PFvvE>:
   a:	55                   	push   %ebp
   b:	89 e5                	mov    %esp,%ebp
   d:	81 7d 08 05 00 00 00 	cmpl   $0x5,0x8(%ebp)
  14:	75 04                	jne    1a <__Z5isFn2PFvvE+0x10>
  16:	b0 01                	mov    $0x1,%al
  18:	eb 02                	jmp    1c <__Z5isFn2PFvvE+0x12>
  1a:	b0 00                	mov    $0x0,%al
  1c:	c9                   	leave  
  1d:	c3                   	ret    

0000001e <_main>:
  1e:	55                   	push   %ebp
  1f:	89 e5                	mov    %esp,%ebp
  21:	83 e4 f0             	and    $0xfffffff0,%esp
  24:	83 ec 20             	sub    $0x20,%esp
  27:	e8 00 00 00 00       	call   2c <_main+0xe>
  2c:	c7 44 24 18 00 00 00 	movl   $0x0,0x18(%esp)
  33:	00 
  34:	eb 2c                	jmp    62 <_main+0x44>
  36:	8b 44 24 1c          	mov    0x1c(%esp),%eax
  3a:	89 04 24             	mov    %eax,(%esp)
  3d:	e8 c8 ff ff ff       	call   a <__Z5isFn2PFvvE>
  42:	84 c0                	test   %al,%al
  44:	74 0a                	je     50 <_main+0x32>
  46:	c7 44 24 1c 00 00 00 	movl   $0x0,0x1c(%esp)
  4d:	00 
  4e:	eb 08                	jmp    58 <_main+0x3a>
  50:	c7 44 24 1c 05 00 00 	movl   $0x5,0x1c(%esp)
  57:	00 
  58:	8b 44 24 1c          	mov    0x1c(%esp),%eax
  5c:	ff d0                	call   *%eax
  5e:	ff 44 24 18          	incl   0x18(%esp)
  62:	81 7c 24 18 e7 03 00 	cmpl   $0x3e7,0x18(%esp)
  69:	00 
  6a:	0f 9e c0             	setle  %al
  6d:	84 c0                	test   %al,%al
  6f:	75 c5                	jne    36 <_main+0x18>
  71:	b8 00 00 00 00       	mov    $0x0,%eax
  76:	c9                   	leave  
  77:	c3                   	ret    

上面可以看到isFn2()函数会被函数main所调用( call   a <__Z5isFn2PFvvE> ),并且在O0优化下是不会被内联的。
但在O3优化下,情况又有所不同:
00000000 <__Z3fn1v>:
   0:	55                   	push   %ebp
   1:	89 e5                	mov    %esp,%ebp
   3:	c9                   	leave  
   4:	c3                   	ret    
   5:	8d 76 00             	lea    0x0(%esi),%esi

00000008 <__Z3fn2v>:
   8:	55                   	push   %ebp
   9:	89 e5                	mov    %esp,%ebp
   b:	c9                   	leave  
   c:	c3                   	ret    
   d:	8d 76 00             	lea    0x0(%esi),%esi

00000010 <__Z5isFn2PFvvE>:
  10:	55                   	push   %ebp
  11:	89 e5                	mov    %esp,%ebp
  13:	81 7d 08 08 00 00 00 	cmpl   $0x8,0x8(%ebp)   <---------------------------
  1a:	0f 94 c0             	sete   %al
  1d:	c9                   	leave  
  1e:	c3                   	ret    
  1f:	90                   	nop

00000020 <_main>:
  20:	55                   	push   %ebp
  21:	89 e5                	mov    %esp,%ebp
  23:	83 e4 f0             	and    $0xfffffff0,%esp
  26:	56                   	push   %esi
  27:	53                   	push   %ebx
  28:	83 ec 08             	sub    $0x8,%esp
  2b:	e8 00 00 00 00       	call   30 <_main+0x10>
  30:	bb e8 03 00 00       	mov    $0x3e8,%ebx
  35:	eb 0b                	jmp    42 <_main+0x22>
  37:	90                   	nop
  38:	be 08 00 00 00       	mov    $0x8,%esi
  3d:	ff d6                	call   *%esi
  3f:	4b                   	dec    %ebx
  40:	74 12                	je     54 <_main+0x34>
  42:	81 fe 08 00 00 00    	cmp    $0x8,%esi         <---------------------------
  48:	75 ee                	jne    38 <_main+0x18>
  4a:	be 00 00 00 00       	mov    $0x0,%esi
  4f:	ff d6                	call   *%esi
  51:	4b                   	dec    %ebx
  52:	75 ee                	jne    42 <_main+0x22>
  54:	31 c0                	xor    %eax,%eax
  56:	83 c4 08             	add    $0x8,%esp
  59:	5b                   	pop    %ebx
  5a:	5e                   	pop    %esi
  5b:	c9                   	leave  
  5c:	c3                   	ret    
  5d:	90                   	nop
  5e:	90                   	nop
  5f:	90                   	nop

注意上面箭头处是被内联化了的isFn2()代码。
可以看到对于函数指针变量pf的调用call *%esi,说明这个地方仍然是无法被内联化的。

通过函数指针调用的函数不能内联化,因此通过基类指针调用的多态函数自然也就无法被内联化,因为多态函数实际是通过虚函数表和偏移项来定位实际调用的函数指针,然后通过这个函数指针访问实际的函数代码。

通过函数指针调用的函数甚至也是不可能被优化消除的。
举个例子:
class A{
public:
    void virtual fn(){}
};
class SubA:public A{
public:
    void virtual fn(){}
};
int main(){
    SubA suba;
    A* a=&suba;
    a->fn();
} 

这段代码在O3优化下,汇编为:
00000014 <_main>:
  14:	55                   	push   %ebp
  15:	89 e5                	mov    %esp,%ebp
  17:	83 e4 f0             	and    $0xfffffff0,%esp
  1a:	83 ec 20             	sub    $0x20,%esp
  1d:	e8 00 00 00 00       	call   22 <_main+0xe>
  22:	c7 44 24 1c 08 00 00 	movl   $0x8,0x1c(%esp)
  29:	00 
  2a:	8d 44 24 1c          	lea    0x1c(%esp),%eax
  2e:	89 04 24             	mov    %eax,(%esp)
  31:	ff 15 08 00 00 00    	call   *0x8
  37:	31 c0                	xor    %eax,%eax
  39:	c9                   	leave  
  3a:	c3                   	ret    
  3b:	90                   	nop

可以看到函数fn()仍然通过虚表的虚函数指针被调用(call   *0x8).
而如果代码
int main(){
    SubA suba;
    A* a=&suba;
    a->fn();
} 换为:
int main(){
    SubA suba;
    SubA* a=&suba;
    a->fn();
}
则对应的O3优化为:
00000014 <_main>:
  14:	55                   	push   %ebp
  15:	89 e5                	mov    %esp,%ebp
  17:	83 e4 f0             	and    $0xfffffff0,%esp
  1a:	e8 00 00 00 00       	call   1f <_main+0xb>
  1f:	31 c0                	xor    %eax,%eax
  21:	c9                   	leave  
  22:	c3                   	ret    
  23:	90                   	nop

上面可以看到O3将 a->fn()的调用完全优化清除掉了。

根据文章开头所给链接的文章提到,Java能运行时动态将基类指针的多态调用替换成内联,那么有个疑问,对于这样逻辑的代码:
    for(int i=0;i<1000;i++) { 
        base=get RandomBaseOrChild();
        base.fn();
    }
java又如何能做到动态内联呢?
分享到:
评论

相关推荐

    C++编程思想习题

    10.1C++中的指针 10.2C+十中的引用 10.2.1函数中的引用 10.2.2参数传递准则 10.3拷贝构造函数 10.3.1传值方式传递和返回 10.3.2拷贝构造函数 10.3.3缺省拷贝构造函数 10.3.4拷贝构造函数方法的选择 10.4指向成员...

    -C++参考大全(第四版) (2010 年度畅销榜

    12.7 在类中定义内联函数 12.8 带参数的构造函数 12.9 带一个参数的构造函数:特例 12.10 静态类成员 12.11 何时执行构造函数和析构函数 12.12 作用域分辨符 12.13 嵌套类 12.14 局部类 12.15 向函数传递对象 12.16 ...

    C++ Primer第四版【中文高清扫描版】.pdf

    17.1.11 函数指针的异常说明 598 17.2 命名空间 599 17.2.1 命名空间的定义 599 17.2.2 嵌套命名空间 603 17.2.3 未命名的命名空间 604 17.2.4 命名空间成员的使用 606 17.2.5 类、命名空间和作用域 609 17.2.6 重载...

    Absolute C++中文版(原书第2版)-完美的C++教程,文档中还包含英文版

    15.1.2 C++中的虚函数 443 15.1.3 抽象类和纯虚函数 448 15.2 指针和虚函数 451 15.2.1 虚函数和扩展类型兼容性 451 15.2.2 向下转换和向上转换 456 15.2.3 C++如何实现虚函数 457 第16章 模板 461 16.1 ...

    C++ Primer中文版(第5版)李普曼 等著 pdf 1/3

     16.2.4 函数指针和实参推断 607  16.2.5 模板实参推断和引用 608  16.2.6 理解std::move 610  16.2.7 转发 612  16.3 重载与模板 614  16.4 可变参数模板 618  16.4.1 编写可变参数函数模板 620  16.4.2 包...

    C++Primer(第5版 )中文版(美)李普曼等著.part2.rar

     16.2.4 函数指针和实参推断 607  16.2.5 模板实参推断和引用 608  16.2.6 理解std::move 610  16.2.7 转发 612  16.3 重载与模板 614  16.4 可变参数模板 618  16.4.1 编写可变参数函数模板 620  16.4.2 包...

    C++大学教程,一本适合初学者的入门教材(part2)

    5.11 函数指针 5.12 字符与字符串处理简介 5.12.1 字符与字符串基础 5.12.2 字符串处理库的字符串操作函数 5.13 有关对象的思考:对象间的交互 小结 术语 自测练习 自测练习答案 练习 特殊小节:建立自己的...

    C++大学教程,一本适合初学者的入门教材(part1)

    5.11 函数指针 5.12 字符与字符串处理简介 5.12.1 字符与字符串基础 5.12.2 字符串处理库的字符串操作函数 5.13 有关对象的思考:对象间的交互 小结 术语 自测练习 自测练习答案 练习 特殊小节:建立自己的...

    Visual C++ 2005入门经典--源代码及课后练习答案

    6.1.2 函数指针作为实参 250 6.1.3 函数指针的数组 251 6.2 初始化函数形参 252 6.3 异常 253 6.3.1 抛出异常 255 6.3.2 捕获异常 256 6.3.3 MFC中的异常处理 257 6.4 处理内存分配错误 258 6.5 ...

    Visual C++ 2010入门经典(第5版)--源代码及课后练习答案

    6.1.2 函数指针作为实参 249 6.1.3 函数指针的数组 250 6.2 初始化函数形参 250 6.3 异常 252 6.3.1 抛出异常 253 6.3.2 捕获异常 254 6.3.3 MFC中的异常处理 255 6.4 处理内存分配错误 256 6.5 函数重载 ...

    Google C++ 编码规范

    析构函数内联的主要原因是其定义在类的定义中,为了方便抑或是对其行为给出文档。 4. 函数参数顺序(Function Parameter Ordering) 定义函数时,参数顺序为:输入参数在前,输出参数在后。 C/C++ 函数参数分为...

    传智播客扫地僧视频讲义源码

    本教程共分为5个部分,第一部分是C语言提高部分,第二部分为C++基础部分,第三部分为C++进阶部分,第四部分为C、C++及数据结构基础部分,第五部分为C_C++与设计模式基础,内容非常详细. 第一部分 C语言提高部分目录...

    C++STL程序员开发指南【可搜索+可编辑】

    1-3-2 在派生类中实现类的基本函数,................... _ ............... 29 1-3-3 内联函数技术,........ ................................... 30 3133 ..... .. .. .. .. .. .. .. .. .. .. .. .. .....

    C++大学教程

    第1章 计算机与C++编程简介-------------------------------------------------1 1.1 简介--------------------------------------------------------------1 1.2 什么是计算机--------------------------------...

Global site tag (gtag.js) - Google Analytics