这篇文章是讲alloca实现的原理。 alloca我感觉是个很强大的函数,帮了我一个很大的忙。突然觉得我就是个天才,我们知道,构造一个对象,无非就二种方式,一种形式就是动态构造:
class A;
A* a=new A();
对于这种形式,编译器底层可能是这么实现的:
size_t nClassSize=sizeof(A);
void* pClassAdress=malloc(nClassSize);
return new(pClassAdress)A();
一种就是静态构造,
class A;
A a;
同样对于这种方式,编译器底层可能是这么实现的:
size_t nClassSize=sizeof(A);
void* pClassAdress=alloca(nClassSize);
return new(pClassAdress)A();
这里用到new的一种新用法,new(void* pAddress)Construct();
这句话的作用其实是在pAddress所指向的内存上调用类的构造函数来初始化pAddress指向的内存,构造出指定对象。很多内存池技术会用这种方式来在内存池缓存中构造对象。
上面二种方式中,唯一不同的就是内存分配函数不同。
malloc是在堆上分配内存,其分配,回收都会跟操作系统打交道,一来速度会很慢,二来频繁的调用会导致很多内存碎片,更加导分配性能的下降。对于这种方式分配的内存需要显示调用free函数来回收内存,以防造成内存泄露。
而alloca这个函数的不同之处在于:该函数是在调用堆栈上来分配内存:在调用堆栈上分配内存有什么不一样呢?其实调用堆栈的分配,回收只是CPU寄存器的ESP的移动,基本上只需要几十条机器指令就能实现一次分配:而且不需要人为的释放,不会造成内存泄露:
_alloca_probe_16 proc ; 16 byte aligned alloca
push ecx
lea ecx, [esp] + 8 ; TOS before entering this function
sub ecx, eax ; New TOS
and ecx, (16 - 1) ; Distance from 16 bit align (align down)
add eax, ecx ; Increase allocation size
sbb ecx, ecx ; ecx = 0xFFFFFFFF if size wrapped around
or eax, ecx ; cap allocation size on wraparound
pop ecx ; Restore ecx
jmp _chkstk
更重要的是,函数的调用堆栈是程序启动时,就已经分配好的内存,在调用堆栈上分配内存是不需要再跟操作系统打交道的,性能非常高,而且不会造成内存碎片。
这个函数简直帮了我的大忙,帮我优化了不少性能。我之前设计了一套网络通信库,希望实现拾取到网络消息后,根据包ID,构造出逻辑包,初始化逻辑包的数据,调用逻辑包的处理函数;
class Packet
{
private:
uint32_t m_peerIP;
int32_t m_SocketID;
public:
void SetPeerIP(uint32_t IP){m_peerIP=IP;}
void SetSocketID(int32_t Id){m_SocketID=Id;}
uint32_t GetPeerIP(){return m_peerIP;}
int32_t GetSocketID(){return m_SocketID;}
Packet():m_peerIP(0),m_SocketID(0){}
virtual ~Packet(){CleanUp();}//这里基类的析构函数一定要用虚函数
virtual void CleanUp(){};
virtual bool Read(CMessage& Ref)=0;
virtual bool Write(CMessage& Ref)=0;
virtual int Execute()=0;
virtual PacketID GetPacketID()=0;
};
class PacketFactory
{
public :
PacketFactory(){}
virtual ~PacketFactory(){}
virtual Packet* CreatePacket(void* pAddress)=0;
virtual PacketID GetPacketID()const=0;
virtual int GetPacketSize()=0;
};
上面二个类Packet是逻辑包基类,PacketFactory是逻辑包工程类,通过这个工厂能够构造出逻辑包,CMessage是个消息包其底层是基于二进制流的。
class PacketMgr :public Singleton<PacketMgr>
{
friend class CMessage;
private:
PacketFactory** m_Factories;
unsigned int m_Size;
bool b_Inited;//是否已经初始化
public:
PacketMgr():b_Inited(false),m_Size(0),m_Factories(NULL){}
virtual ~PacketMgr()
{
for(unsigned int i=0;i<m_Size;i++)
{
SAFE_DELETE(m_Factories[i]);
}
SAFE_DELETE_ARRAY(m_Factories);
b_Inited=false;
m_Size=0;
}
bool AddFactory(PacketFactory* pFactory)
{
if(b_Inited==false)
{
assert(false);
return false;
}
if(pFactory==NULL)
{
return false;
}
int nPackIdx=pFactory->GetPacketID();
if(nPackIdx<0||nPackIdx>=m_Size)
{
return false;
}
if( m_Factories[nPackIdx]!=NULL)
{
assert(false);
return false;
}
m_Factories[nPackIdx]=pFactory;
return true;
}
private://这两个函数仅提供给CMessage对象来用
Packet* CreatePacket(PacketID nId,void* pAddress)
{
if(nId>=m_Size ||nId<0 || m_Factories[nId]==NULL||pAddress==NULL)
{
assert(false);
return NULL;
}
Packet* pPacket=m_Factories[nId]->CreatePacket(pAddress);
return pPacket;
}
void RemovePacket(Packet* pPacket)
{
//pPacket所指向的内存由栈管理,无需delete,仅需调用对象的析构函数
if(pPacket==NULL)
{
assert(false);
return;
}
pPacket->~Packet();//此处析构函数为虚函数,会先调用子类对象的析构函数,然后再调用父类函数的析构函数,防止内存泄露
}
public:
int GetPacketSize(PacketID nId);
bool Init(int nMaxPacket)
{
assert(nMaxPacket>0);
m_Size=nMaxPacket;
m_Factories=new PacketFactory*[m_Size];
for(unsigned int i=0;i<m_Size;i++)
{
m_Factories[i]=NULL;
}
b_Inited=true;
return true;
}
};
}
#define S_PACKET_MGR (PacketMgr::Instance())
PacketMgr是一个逻辑包管理类,系统初始化时,会调用AddFactory以逻辑包ID为数组下标,将逻辑包工厂类添加到m_Factories数组。
S_PACKET_MGR- AddFactory(new CGLoginFactory() );
一个简单的逻辑包实现:
class CGLogin : public Packet
{
private:
char m_Account[MAX_ACCOUNT_LEN];//账号
char m_Pwd[MAX_PWD_LEN];//密码
uint32_t m_ClientVersion;//客户端版本
public:
CGLogin();
~CGLogin();
void SetAccount(const char* szAccout);
const char* GetAccount(){return m_Account;}
void SetPwd(const char* szPwd);
const char* GetPwd(){return m_Pwd;}
uint32_t GetClientVersion(){return m_ClientVersion;}
void SetClientVersion(uint32_t version){m_ClientVersion=version;}
public:
virtual void CleanUp();
virtual bool Read(CMessage& Ref)
{
Ref.getEx((void*)m_Account,sizeof(m_Account));
Ref.getEx((void*)m_Pwd,sizeof(m_Pwd));
m_ClientVersion=Ref.getUInt32();
}
virtual bool Write(CMessage& Ref)
{
Ref.addEx((void*)m_Account,sizeof(m_Account));
Ref.addEx((void*)m_Pwd,sizeof(m_Pwd));
Ref.addUInt32(m_ClientVersion);
}
virtual int Execute()
{
return CGLoginHandler::Execute(this);
}
virtual PacketID GetPacketID(){return CG_Login;}
};
class CGLoginFactory : public PacketFactory
{
//注意这里new的用法 new(void* p)fun();这里new的作用是在p所指向的内存调用对象的构造函数,不会再申请内存
Packet* CreatePacket(void* pAddress){ return new(pAddress)CGLogin();}
PacketID GetPacketID()const{ return CG_Login;}
virtual int GetPacketSize(){return sizeof(CGLogin);}
};
class CGLoginHandler
{
public:
static int Execute(CGLogin* pPacket) ;
};
//网络底层拾取屌网络消息,回来调用消息的run方法。
int32_t CMessage::run()
{
uint32_t packet_id = getMsgID();
size_t PacketSize=S_PACKET_MGR->GetPacketSize(packet_id);
if(PacketSize<=0)
{
return 0;
}
//alloca函数用于在栈上申请指定大小的内存空间,其申请的内存空间由栈负责回收
void* pAddress=NULL;
#if _WIN32
__try
#else
try
#endif
{
pAddress=alloca(PacketSize);//在栈上申请对象大小的空间,用于构造对象
}
#if _WIN32
__except(EXCEPTION_EXECUTE_HANDLER)
{
pAddress=NULL;
}
#else
catch(...)
{
pAddress=NULL;
}
#endif
bool bAllocInStack=false;//是否在栈上分配的内存
if(pAddress==NULL)
{
//如果在栈上分配失败则在堆栈上分配
pAddress=malloc(PacketSize);
bAllocInStack=false;
}
else
{
bAllocInStack=true;
}
if(pAddress==NULL)
{
assert(false);
return 0;
}
memset(pAddress,0,PacketSize);//初始化这块内存
Packet* pPacket=S_PACKET_MGR->CreatePacket(packet_id,pAddress);//在指定的地址上构造包对象
//这里pPacket的地址实际上是pAddress的地址,上面已经判断过pAddress的值,这里无需再判断pPacket是否为空了
pPacket->SetSocketID(m_socketID);
pPacket->SetPeerIP(m_dwIP);
pPacket->Read(*this);//初始化逻辑包
int nRet=pPacket->Execute();//调用逻辑包处理函数
S_PACKET_MGR->RemovePacket(pPacket);//该函数仅会调用类的析构函数,并不会delete pPacket指向的内存,该内存分配在栈上,由系统负责回收
if(bAllocInStack==false)
{
//内存是分配在堆栈上,需要手动释放内存
free(pAddress);
}
return nRet;
}
}
bool SendPacket(Packet* pPacket)
{ if(pPacket==NULL)
{
return false;
}
int nPacketId=pPacket->GetPacketID();
CMessage Msg;
Msg.init(nPacketId);
pPacket->Write(Msg);//将逻辑包的数据写入消息流
//调用网络底层将CMessage发送出去
}