编译时多态还是运行时多态

时间:2022-01-13 21:56:22

今天来研究一下编译时多态和运行时多态。
运行时的多态,比如C++用虚函数,或者比如有些语言支持委托。这样实际运行的代码是运行时决定的,这样必须要承担一定的开销:

1、间接调用。比如说虚表。
2、调用的函数编译期不能确定,所以不能展开。

这样性能上是有很大的损失的。如果这个调用出现在一个循环里,或者被其他地方频繁的调用,效率会有很大的差距。

其实有些时候,我们并不是真的需要真正的运行时多态,有时可能就是编译期的多态。

举例:

编译时多态还是运行时多态class  ServerBase
编译时多态还是运行时多态编译时多态还是运行时多态
{
编译时多态还是运行时多态public:
编译时多态还是运行时多态    
virtual void Open() = 0
;
编译时多态还是运行时多态    virtual ~ServerBase() {};
编译时多态还是运行时多态}
;
编译时多态还是运行时多态
编译时多态还是运行时多态
class
 FtpServer
编译时多态还是运行时多态编译时多态还是运行时多态
{
编译时多态还是运行时多态public:
编译时多态还是运行时多态    
virtual void
 Open()
编译时多态还是运行时多态编译时多态还是运行时多态    
{
编译时多态还是运行时多态        
//编译时多态还是运行时多态

编译时多态还是运行时多态
    }

编译时多态还是运行时多态}
;
编译时多态还是运行时多态
编译时多态还是运行时多态
class
 HttpServer
编译时多态还是运行时多态编译时多态还是运行时多态
{
编译时多态还是运行时多态public:
编译时多态还是运行时多态    
virtual void
 Open()
编译时多态还是运行时多态编译时多态还是运行时多态    
{
编译时多态还是运行时多态        
//编译时多态还是运行时多态

编译时多态还是运行时多态
    }

编译时多态还是运行时多态}
;

 

然后你的程序里使用起来是这样的:

编译时多态还是运行时多态ServerBase* pServer = new HttpServer();
编译时多态还是运行时多态pServer->Open();

这样你做了个HttpServer。如果这里就是个具体应用,没有要求你运行期间能替换协议,那么这个设计明显有额外的开销。让我们先做几个例子来看看调用一个函数的开销。

例子:

编译时多态还是运行时多态class  CPolymorphismBase
编译时多态还是运行时多态编译时多态还是运行时多态
{
编译时多态还是运行时多态
public
:
编译时多态还是运行时多态    
void
 MemberFunction()
编译时多态还是运行时多态编译时多态还是运行时多态    
{
编译时多态还是运行时多态        std::cout
<<"MemberFunction Base"
;
编译时多态还是运行时多态    }

编译时多态还是运行时多态
编译时多态还是运行时多态    
virtual void VirtualFunction()
编译时多态还是运行时多态编译时多态还是运行时多态    
{
编译时多态还是运行时多态        std::cout
<<"VirtualFunction Base"
;
编译时多态还是运行时多态    }
编译时多态还是运行时多态
编译时多态还是运行时多态    virtual ~CPolymorphismBase() {}
编译时多态还是运行时多态}
;
编译时多态还是运行时多态
编译时多态还是运行时多态
class CPolymorphismDerived : public
 CPolymorphismBase
编译时多态还是运行时多态编译时多态还是运行时多态
{
编译时多态还是运行时多态
public
:
编译时多态还是运行时多态    
void
 MemberFunction()
编译时多态还是运行时多态编译时多态还是运行时多态    
{
编译时多态还是运行时多态        std::cout
<<"MemberFunction Derived"
;
编译时多态还是运行时多态    }

编译时多态还是运行时多态
编译时多态还是运行时多态    
virtual void VirtualFunction()
编译时多态还是运行时多态编译时多态还是运行时多态    
{
编译时多态还是运行时多态        std::cout
<<"VirtualFunction Derived"
;
编译时多态还是运行时多态    }

编译时多态还是运行时多态}
;



首先我们来看看普通的成员函数的开销是怎么样的。

编译时多态还是运行时多态     CPolymorphismDerived derivedObject;
编译时多态还是运行时多态    CPolymorphismDerived
* pDerivedObject = new
 CPolymorphismDerived();
编译时多态还是运行时多态
编译时多态还是运行时多态    derivedObject.MemberFunction();
编译时多态还是运行时多态    pDerivedObject
->MemberFunction();


运行一下,看到汇编内容如下:

编译时多态还是运行时多态     derivedObject.MemberFunction();
编译时多态还是运行时多态
00401034
  mov         eax,dword ptr [__imp_std::cout (40204Ch)] 
编译时多态还是运行时多态
00401039  push        offset string "MemberFunction Derived"
 (40213Ch) 
编译时多态还是运行时多态0040103E  push        eax  
编译时多态还是运行时多态0040103F  call        std::
operator<<<std::char_traits<char> >
 (401190h) 
编译时多态还是运行时多态    pDerivedObject
->
MemberFunction();
编译时多态还是运行时多态
00401044
  mov         ecx,dword ptr [__imp_std::cout (40204Ch)] 
编译时多态还是运行时多态0040104A  push        offset 
string "MemberFunction Derived"
 (40213Ch) 
编译时多态还是运行时多态0040104F  push        ecx  
编译时多态还是运行时多态
00401050  call        std::operator<<<std::char_traits<char> > (401190h) 


可以看到,函数不但没有虚表的开销,而且已经在这里展开了,编译器替我们完成了inline优化,这样的效率当然是最高的。

然后我们来看虚函数。

编译时多态还是运行时多态     CPolymorphismDerived derivedObject;
编译时多态还是运行时多态    CPolymorphismDerived
* pDerivedObject = new
 CPolymorphismDerived();
编译时多态还是运行时多态    CPolymorphismBase
* pDerivedObjectBasePoint =
 pDerivedObject;
编译时多态还是运行时多态
编译时多态还是运行时多态    derivedObject.VirtualFunction();
编译时多态还是运行时多态    pDerivedObject
->
VirtualFunction();
编译时多态还是运行时多态    pDerivedObjectBasePoint
->VirtualFunction();


运行一下,看到汇编内容如下:

编译时多态还是运行时多态     derivedObject.VirtualFunction();
编译时多态还是运行时多态0040103B  mov         eax,dword ptr [__imp_std::cout (40204Ch)] 
编译时多态还是运行时多态
00401040  push        offset string "VirtualFunction Derived"
 (40213Ch) 
编译时多态还是运行时多态
00401045
  push        eax  
编译时多态还是运行时多态
00401046  call        std::operator<<<std::char_traits<char> >
 (4011A0h) 
编译时多态还是运行时多态    pDerivedObject
->
VirtualFunction();
编译时多态还是运行时多态0040104B  mov         edx,dword ptr [esi] 
编译时多态还是运行时多态0040104D  mov         eax,dword ptr [edx] 
编译时多态还是运行时多态0040104F  add         esp,
8
 
编译时多态还是运行时多态
00401052
  mov         ecx,esi 
编译时多态还是运行时多态
00401054
  call        eax  
编译时多态还是运行时多态    pDerivedObjectBasePoint
->
VirtualFunction();
编译时多态还是运行时多态
00401056
  mov         edx,dword ptr [esi] 
编译时多态还是运行时多态
00401058
  mov         eax,dword ptr [edx] 
编译时多态还是运行时多态0040105A  mov         ecx,esi 
编译时多态还是运行时多态0040105C  call        eax  


栈上分配出来的那个对象,虽然是虚函数,但是编译期是确定的,所以没有虚表的开销,函数也成功展开了。
堆上new出来的那个对象,就不走运了。不管是基类指针还是派生类指针去操作,都有虚表的开销,同时函数也无法展开。

这是什么意思呢?就是说即时一开始的例子改成这样也一样性能很低下

编译时多态还是运行时多态//ServerBase* pServer = new HttpServer();    // 既然这样写性能低。。。
编译时多态还是运行时多态
HttpServer* pServer = new HttpServer();      // 那我换成这样写吧!!!
编译时多态还是运行时多态pServer->Open();                             // but,即使不用基类指针操作,效率也是一样低。


我们来总结一下吧

性能 栈上分配 堆分配 + 父类指针 堆分配 + 子类指针
普通成员函数
虚函数


下面切入正题。
要让用户使用起来性能得到保障,就只有用普通成员函数了。(你总不能强制用户必须使用栈对象吧?)

既然是普通成员函数,那也就没ServerBase什么事了。(既然不使用虚函数,那么一个基类的指针也就调用不到子类了,所以这里弄个基类没什么用了。)所以,代码修改如下了:

编译时多态还是运行时多态class  FtpServer
编译时多态还是运行时多态编译时多态还是运行时多态
{
编译时多态还是运行时多态public:
编译时多态还是运行时多态    
void
 Open()
编译时多态还是运行时多态编译时多态还是运行时多态    
{
编译时多态还是运行时多态        
//编译时多态还是运行时多态

编译时多态还是运行时多态
    }

编译时多态还是运行时多态}
;
编译时多态还是运行时多态
编译时多态还是运行时多态
class
 HttpServer
编译时多态还是运行时多态编译时多态还是运行时多态
{
编译时多态还是运行时多态public:
编译时多态还是运行时多态    
void
 Open()
编译时多态还是运行时多态编译时多态还是运行时多态    
{
编译时多态还是运行时多态        
//编译时多态还是运行时多态

编译时多态还是运行时多态
    }

编译时多态还是运行时多态}
;


所以使用的时候就可以这么写了:

编译时多态还是运行时多态 typedef HttpServer ServerBase;
编译时多态还是运行时多态typedef HttpServer Server;
编译时多态还是运行时多态
编译时多态还是运行时多态ServerBase
* pServer = new
 Server();
编译时多态还是运行时多态pServer->Open();
编译时多态还是运行时多态
//编译时多态还是运行时多态


这样,就可以了,什么时候需要一个Ftp的版本了,就可以简单修改头两行代码,别的都不用改,这样就是一个FtpServer了:

编译时多态还是运行时多态 typedef FtpServer ServerBase;
编译时多态还是运行时多态typedef FtpServer Server;
编译时多态还是运行时多态
编译时多态还是运行时多态ServerBase
* pServer = new
 Server();
编译时多态还是运行时多态pServer
->Open();
编译时多态还是运行时多态//编译时多态还是运行时多态


这里也可以用模板:

编译时多态还是运行时多态template<class T>
编译时多态还是运行时多态
class  App
编译时多态还是运行时多态编译时多态还是运行时多态
{
编译时多态还是运行时多态
public
:
编译时多态还是运行时多态    typedef T ServerBase;
编译时多态还是运行时多态    typedef T Server;
编译时多态还是运行时多态
编译时多态还是运行时多态    static
void
 main()
编译时多态还是运行时多态编译时多态还是运行时多态    
{
编译时多态还是运行时多态        ServerBase
* pServer = new
 Server();
编译时多态还是运行时多态        pServer->Open();
编译时多态还是运行时多态        //编译时多态还是运行时多态
编译时多态还是运行时多态    }

编译时多态还是运行时多态}
;

App<FtpServer>::main();        // 这就是Ftp的
//App<HttpServer>::main();     // 这就是Http的


这就是编译期的多态,都用普通成员函数,使用到多态对象的类可以使用模板来简化多态时的修改(都用模板,然后在外部整体一个typedef就可以把所有需要多态的类都提纯出来,一个修改就可以让全程序得到效果)。

当然,并不是到这就结束了。。。你的HttpServer和FtpServer都改好了。。。但是你的BOSS过来和你说:兄弟,新需求,需要允许运行时修改协议,允许热配置。然后你就发现:坏了,二了,咱已经改成编译期多态的了。。。

这该怎么办呢?怎么让你设计的类既能配置成编译期多态而且不损效率,又能配置成运行时多态呢?

这里可以用个Adapter来做。

编译时多态还是运行时多态template<class T>
编译时多态还是运行时多态
class Server_Adapter : public  ServerBase
编译时多态还是运行时多态编译时多态还是运行时多态
{
编译时多态还是运行时多态
public
:
编译时多态还是运行时多态    Server_Adapter()
编译时多态还是运行时多态编译时多态还是运行时多态    
{
编译时多态还是运行时多态    }

编译时多态还是运行时多态
编译时多态还是运行时多态    
virtual ~Server_Adapter()
编译时多态还是运行时多态编译时多态还是运行时多态    
{
编译时多态还是运行时多态    }

编译时多态还是运行时多态
编译时多态还是运行时多态
编译时多态还是运行时多态    
virtual void Open()
编译时多态还是运行时多态编译时多态还是运行时多态    
{
编译时多态还是运行时多态        m_server.Open();
编译时多态还是运行时多态    }

编译时多态还是运行时多态
编译时多态还是运行时多态
private:
编译时多态还是运行时多态    T m_server;
编译时多态还是运行时多态}
;


这样就OK了。使用者如果需要编译期多态,就像上一个例子那么用就可以了,性能没问题;如果需要运行期多态,那就这么写:

编译时多态还是运行时多态 typedef ServerBase ServerBaseObject;
编译时多态还是运行时多态typedef Server_Adapter
<HttpServer>
 HttpServerObject;
编译时多态还是运行时多态typedef Server_Adapter
<FtpServer>
  FtpServerObject;
编译时多态还是运行时多态
编译时多态还是运行时多态ServerBaseObject
* pServer =
 NULL;
编译时多态还是运行时多态
int nServerType = 编译时多态还是运行时多态// 这里可以动态得到,从配置文件里头读啊什么的,反正都行,运行时随便改。

编译时多态还是运行时多态
    
编译时多态还是运行时多态
switch
(nServerType)
编译时多态还是运行时多态编译时多态还是运行时多态
{
编译时多态还是运行时多态
case 0
:
编译时多态还是运行时多态编译时多态还是运行时多态    
{
编译时多态还是运行时多态        pServer 
= new
 HttpServerObject();
编译时多态还是运行时多态    }

编译时多态还是运行时多态    
break;
编译时多态还是运行时多态
case 1
:
编译时多态还是运行时多态编译时多态还是运行时多态    
{
编译时多态还是运行时多态        pServer 
= new
 FtpServerObject();
编译时多态还是运行时多态    }

编译时多态还是运行时多态    
break;
编译时多态还是运行时多态
default
:
编译时多态还是运行时多态    
//编译时多态还是运行时多态

编译时多态还是运行时多态
    break;
编译时多态还是运行时多态}

编译时多态还是运行时多态pServer
-> Open();
编译时多态还是运行时多态
//编译时多态还是运行时多态


这样就运行时多态了,一点毛病没有。和一开始的方案比,没有一丝性能损失。也许你要问了:这不是多包了一层么?调用Open的时候,要多一次函数调用呀?当然不会了。。。作为Adapter的T来说,编译期是确定的,所以内部的对象m_server作为一个栈对象,调用的函数是可以展开的。我们还是看一下具体的汇编代码:

编译时多态还是运行时多态     pServer->Open();
编译时多态还是运行时多态
00401030
  mov         eax,dword ptr [__imp_std::cout (402050h)] 
编译时多态还是运行时多态
00401035
  push        eax  
编译时多态还是运行时多态
00401036  call        std::operator<<<std::char_traits<char> >
 (401190h) 
编译时多态还是运行时多态0040103B  pop         ecx  


可见,这里的代码展开了,您可以试验一下在HttpServer::Open和FtpServer::Open那里下个断点,就会发现根本断不下来,因为那里的代码已经展开在Server_Adapter<HttpServer>和Server_Adapter<FtpServer>里头了,根本没有间接调用的问题。

这样,一个兼顾效率和灵活性的完美类库产生了。