`
ppgunjack
  • 浏览: 81287 次
  • 性别: 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++函数中那些不可以被声明为虚函数的函数[参考].pdf

    在C++编程语言中,虚函数是实现多态性的重要机制,它允许子类对象通过基类指针或引用调用相应的重写方法。然而,并非所有函数都可以被声明为虚函数,因为它们的特性与虚函数的设计目标不兼容。以下是对C++中不可声明...

    c++函数库查询辞典,光盘内容(原代码)

    10. **性能优化**:讨论C++中的性能优化技巧,如内存对齐、循环展开、内联函数等,以及如何使用`&lt;chrono&gt;`库进行性能测量。 这个光盘内容的原代码部分可能包含示例代码、测试用例和实际库的实现,这对于学习C++库的...

    C++友元函数的分析和理解

    3. 编译器无法进行某些优化:由于友元函数不受类的控制,编译器可能无法对友元函数进行某些内联优化。 在实际应用中,应谨慎使用友元函数,避免过度使用导致设计复杂。如果友元函数仅用于单个类,那么最好将其声明...

    C++高效编程:内存与性能优化

    在C++编程中,内存管理和性能优化是两个关键领域,对于开发高质量、高效能的应用程序至关重要。本主题将深入探讨这两个方面,旨在帮助开发者更好地理解如何编写更优化的C++代码。 内存管理是C++的核心特性之一,...

    c++语言程序设计郑莉第四版 源代码 完整

    源代码可能包含各种函数,如重载函数、模板函数、内联函数等,这些都展示了C++在函数使用上的灵活性。 C++标准库是编程时不可或缺的工具,它提供了大量的容器(如vector、list、set)、算法(如排序、查找)以及...

    【中文】【pdf】【高清】 effective c++

    12. **编译器优化**:书中提到了一些编译器可以自动进行的优化,如内联函数和编译器的O2/O3优化等级,以及如何在代码中配合这些优化。 13. **C++11及以后的更新**:虽然原书可能基于较早的C++版本,但现代C++(如...

    C++与操作系统等面试题53

    在C++中,虚函数主要用于实现接口的一致性和灵活性。然而,并非所有的函数都可以被声明为虚函数。本文将详细介绍哪些类型的函数不能声明为虚函数,并解释其原因。 #### 不能作为虚函数的函数类型及其原因 ##### 1....

    详细手册13010

    **C和C++函数用法详细手册** C和C++是两种非常重要的编程语言,它们在计算机科学领域中有着广泛的应用。C语言以其简洁、高效和底层特性深受程序员喜爱,而C++则在此基础上增加了面向对象编程的支持,使得程序设计...

    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++复习题.docx

    5. **静态成员函数与this指针**:静态成员函数不与特定的对象关联,因此它们不能访问`this`指针。选项A是正确的,B也是正确的。C错误,因为构造函数可以有参数,析构函数也可以接受参数(虽然通常不这么做)。D错误...

    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++中函数使用的基本知识学习教程

    10. **函数指针**: 函数可以被赋值给指针,这样就可以在运行时动态地调用函数。这对于回调函数、算法排序或基于策略的设计模式特别有用。 11. **参数默认值**: 函数参数可以设置默认值,以便在调用时不必为每个...

    C++重要知识点总结

    可以创建一个包含多个函数指针的数组,用于存储和管理多个函数。 **参数和返回类型** 函数指针的类型由函数的参数列表和返回类型决定。 #### 成员初始化表 成员初始化列表是在构造函数中初始化对象成员的一种...

    一些c++习题供初学者看的

    - 内联函数是编译器用来优化代码的一种技术,它试图将函数体插入到每个调用处。内联函数通常用于小型函数,以减少函数调用的开销。 - 非内联函数则是常规的函数调用方式,当函数较大或者在运行时确定是否内联时,...

    C++复习题及重点

    内联函数是一种优化技术,用于减少函数调用的开销,但并不总是能提高效率,编译器会根据情况决定是否进行内联。 2. **字符串**:C++提供了字符串处理的能力,包括字符串与数组、字符串与指针的交互。`std::string` ...

    C++面试300题

    5. **内联函数**:在类内部定义的成员函数通常会被编译器视为内联函数,用于优化函数调用,减少函数调用开销。 6. **this指针**:在成员函数中,`this`是一个隐含的指针,指向调用该成员函数的对象。 7. **默认...

    计算机-C++试卷-A卷.pdf

    16. **静态成员函数**:静态成员函数不与特定对象关联,因此不能访问非静态成员,但可以直接访问静态成员。 17. **访问控制**:友元函数可以访问类的所有私有成员、公有成员和保护成员,而成员函数只能访问同一类的...

    C++模拟题5答案.pdf

    静态成员函数没有`this`指针,因为它们不与特定的对象实例关联,而是属于类本身。它们可以访问类中的静态成员,但不能直接访问非静态成员。在类外定义静态成员函数时,不需要再次使用`static`关键字。 2. 构造函数...

Global site tag (gtag.js) - Google Analytics