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++编程语言中,虚函数是实现多态性的重要机制,它允许子类对象通过基类指针或引用调用相应的重写方法。然而,并非所有函数都可以被声明为虚函数,因为它们的特性与虚函数的设计目标不兼容。以下是对C++中不可声明...
10. **性能优化**:讨论C++中的性能优化技巧,如内存对齐、循环展开、内联函数等,以及如何使用`<chrono>`库进行性能测量。 这个光盘内容的原代码部分可能包含示例代码、测试用例和实际库的实现,这对于学习C++库的...
3. 编译器无法进行某些优化:由于友元函数不受类的控制,编译器可能无法对友元函数进行某些内联优化。 在实际应用中,应谨慎使用友元函数,避免过度使用导致设计复杂。如果友元函数仅用于单个类,那么最好将其声明...
在C++编程中,内存管理和性能优化是两个关键领域,对于开发高质量、高效能的应用程序至关重要。本主题将深入探讨这两个方面,旨在帮助开发者更好地理解如何编写更优化的C++代码。 内存管理是C++的核心特性之一,...
源代码可能包含各种函数,如重载函数、模板函数、内联函数等,这些都展示了C++在函数使用上的灵活性。 C++标准库是编程时不可或缺的工具,它提供了大量的容器(如vector、list、set)、算法(如排序、查找)以及...
12. **编译器优化**:书中提到了一些编译器可以自动进行的优化,如内联函数和编译器的O2/O3优化等级,以及如何在代码中配合这些优化。 13. **C++11及以后的更新**:虽然原书可能基于较早的C++版本,但现代C++(如...
在C++中,虚函数主要用于实现接口的一致性和灵活性。然而,并非所有的函数都可以被声明为虚函数。本文将详细介绍哪些类型的函数不能声明为虚函数,并解释其原因。 #### 不能作为虚函数的函数类型及其原因 ##### 1....
**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指向成员...
12.7 在类中定义内联函数 12.8 带参数的构造函数 12.9 带一个参数的构造函数:特例 12.10 静态类成员 12.11 何时执行构造函数和析构函数 12.12 作用域分辨符 12.13 嵌套类 12.14 局部类 12.15 向函数传递对象 12.16 ...
5. **静态成员函数与this指针**:静态成员函数不与特定的对象关联,因此它们不能访问`this`指针。选项A是正确的,B也是正确的。C错误,因为构造函数可以有参数,析构函数也可以接受参数(虽然通常不这么做)。D错误...
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 包...
10. **函数指针**: 函数可以被赋值给指针,这样就可以在运行时动态地调用函数。这对于回调函数、算法排序或基于策略的设计模式特别有用。 11. **参数默认值**: 函数参数可以设置默认值,以便在调用时不必为每个...
可以创建一个包含多个函数指针的数组,用于存储和管理多个函数。 **参数和返回类型** 函数指针的类型由函数的参数列表和返回类型决定。 #### 成员初始化表 成员初始化列表是在构造函数中初始化对象成员的一种...
- 内联函数是编译器用来优化代码的一种技术,它试图将函数体插入到每个调用处。内联函数通常用于小型函数,以减少函数调用的开销。 - 非内联函数则是常规的函数调用方式,当函数较大或者在运行时确定是否内联时,...
内联函数是一种优化技术,用于减少函数调用的开销,但并不总是能提高效率,编译器会根据情况决定是否进行内联。 2. **字符串**:C++提供了字符串处理的能力,包括字符串与数组、字符串与指针的交互。`std::string` ...
5. **内联函数**:在类内部定义的成员函数通常会被编译器视为内联函数,用于优化函数调用,减少函数调用开销。 6. **this指针**:在成员函数中,`this`是一个隐含的指针,指向调用该成员函数的对象。 7. **默认...
16. **静态成员函数**:静态成员函数不与特定对象关联,因此不能访问非静态成员,但可以直接访问静态成员。 17. **访问控制**:友元函数可以访问类的所有私有成员、公有成员和保护成员,而成员函数只能访问同一类的...
静态成员函数没有`this`指针,因为它们不与特定的对象实例关联,而是属于类本身。它们可以访问类中的静态成员,但不能直接访问非静态成员。在类外定义静态成员函数时,不需要再次使用`static`关键字。 2. 构造函数...