服務器程序為何要進行內存管理,管中窺豹,讓我們從string字符串的操作說起。。。。。。
new/delete是用於c++中的動態內存管理函數,而malloc/free在c++和c中都可以使用,本質上new/delete底層封裝了malloc/free。無論是上面的哪種內存管理方式,都存在以下兩個問題:
1、效率問題:頻繁的在堆上申請和釋放內存必然需要大量時間,降低了程序的運行效率。對於一個需要頻繁申請和釋放內存的程序由於是服務器程序來說,大量的調用new/malloc申請內存和delete/free釋放內存都需要花費系統時間,這就必然會降低程序的運行效率。
2、內存碎片:經常申請小塊內存,會將物理內存“切”得很碎,導致內存碎片。申請內存的順序並不是釋放內存的順序,因此頻繁申請小塊內存必然會導致內存碎片,可能造成“有內存但是申請不到大塊內存”的現象。
對於客戶端軟件,內存管理不是很重要,起碼你可以重啟機器。但對於需要24小時長期不間斷運行的服務器程序來說就顯得特別的重要了!比如無處不在的web服務器,它采用的是HTTP協議,基於請求—應答的超文本傳輸方式,這種一問一答的協議非常簡單,請求頭和響應頭都是非二進制的字符串。當服務端收到客戶端的GET或POST請求時,服務器程序要先構造一個響應頭並拼接響應體,如下:
// 構造響應頭
string strHttpResponse;
strHttpResponse += "HTTP/1.1 200 OK\r\n";
strHttpResponse += "Server: HttpServer \r\n";
strHttpResponse += "Content-Type: text/html; charset=utf-8\r\n";
strHttpResponse += "Content-Length: 9527\r\n";
strHttpResponse += "Last-Modified: Sat, 13 Apr 2019 14:27:06 GMT\r\n";
strHttpResponse += "\r\n"; // 空行,空行后就是真正的響應體
// 構造響應體
strHttpResponse += "<html><head><title>Hello,我是9527!</title>"
"</head><body>Hello,我是9527的body,假裝我有9527那么長!</body></html>";
對於動態網頁或者后台應用來說,通常需要查詢數據庫以及各種業務上的操作,然后將結果拼接為json或xml這種半結構化數據返回給客戶端。
當然這篇文章並不是要介紹什么是HTTP協議,關於HTTP協議介紹的文章已經非常多了。我們是想通過一次正常的HTTP會話,來看看字符串操作是如何應用的?是否有優化提升的可能?
字符串操作能有多大事啊!
對於客戶端來說,問題確實不大,但對於每天24小時不關機長期運行的web服務器程序來說可能就會產生性能問題。字符串在累加賦值時,可能導致內存的不斷開辟和銷毀,也就是上面我們說的產生了內存碎片。
產生內存碎片能有多大事啊!
如果在高並發的情況下,性能就可能會有影響,頻繁的malloc/free本身就會大量的占用CPU時間,過多的碎片將會讓物理內存過於碎片化,從而導致無法申請更大的連續的內存塊。
無論是標准庫中的string還是微軟MFC庫中的CString,內部都會維護一個字符串緩存。當拼接后的字符串長度小於內部緩存時,直接將兩個字符串連接即可;當拼接后的字符串長度大於內部緩存時,就需要重新開辟一個新的更大的緩存,然后將字符串重新拼接起來。為了直觀的進行比較,我們編寫一個自己的字符串封裝類CFastString(文末有CFastString的全部實現)。並重載操作符“+=”。
const CFastString& CFastString::operator+=(const char *pszSrc)
{
assert(pszSrc);
int iLenSrc = _tcslen(pszSrc);
int iNewSize = iLenSrc + length() + 1; // 0結尾,所以+1
// 當內部緩存足夠時,直接進行拼接,不足時則需要開辟新的內存
if(m_iBuffSize >= iNewSize)
{
memcpy(m_pszStr+m_iStrLen, pszSrc, iLenSrc);
*(m_pszStr+iNewSize-1) = 0;
}
else
{
// 分配一塊新的內存
char* pszNew = AllocBuffer(iNewSize);
// 將字符串拷貝拼接到新開辟的內存中
// 方法一:strcpy+strcat
strcpy(pszNew, m_pszStr);
strcat(pszNew, pszSrc);
// 方法二:直接使用內存拷貝
// memcpy(pszNew, m_pszStr, m_iStrLen);
// memcpy(pszNew+m_iStrLen, pszSrc, iLenSrc);
free(m_pszStr);
m_pszStr = pszNew;
}
m_iStrLen = iNewSize-1;
return *this;
}
通過上面的代碼可以看到,如果內部緩存不足時,將會重新申請新的緩存,字符串在不斷累加過程中,可能會導致內存的反復申請和銷毀,那么如何提升性能呢?
我們寫個測試函數比較CFastString和string的累加函數(+=)的性能,測試代碼如下:
void TestFastString()
{
int i = 0;
int iTimes = 5000;
// 測試CFastString
printf("CFastString 測試:\r\n");
CFastString fstr = "Hello";
DWORD dwStart = ::GetTickCount();
for(i = 0; i < iTimes; i++)
{
fstr += "10000000000000000000000000000000";
fstr += "20000000000000000000000000000000";
fstr += "30000000000000000000000000000000";
fstr += "40000000000000000000000000000000";
}
DWORD dwSpan1 = ::GetTickCount()-dwStart;
printf("CFastString Span = %d\n", dwSpan1);
// 測試string
printf("std::string 測試:\r\n");
string str = "Hello";
dwStart = ::GetTickCount();
for(i = 0; i < iTimes; i++)
{
str += "10000000000000000000000000000000";
str += "20000000000000000000000000000000";
str += "30000000000000000000000000000000";
str += "40000000000000000000000000000000";
}
DWORD dwSpan2 = ::GetTickCount()-dwStart;
printf("std::string Span = %d\n", dwSpan2);
printf("測試結束!\r\n");
}
運行一下,結果如下:

我們發現CFastString並不fast,反而相當的slow。重新封裝的字符串操作類還不如不封裝,會不會是strcpy和strcat比較慢?
改進一:
我們修改CFastString::operator+=(const char *pszSrc)函數代碼,將如下拼接語句:
// 方法一:strcpy+strcat
strcpy(pszNew, m_pszStr);
strcat(pszNew, pszSrc);
改為:
// 方法二:直接使用內存拷貝
memcpy(pszNew, m_pszStr, m_iStrLen);
memcpy(pszNew+m_iStrLen, pszSrc, iLenSrc);
再次運行看下結果:

還不錯,比string快了一點,但好像並不顯著。重載的+=函數中,每次內存分配的大小為前一個字符串加后一個字符串的大小,這就導致了一旦字符串的內部緩存已滿時,后面每次的累加操作都會觸發一次內存的重新申請和釋放。舉個極端的例子,假設str在累加操作前內部緩存已滿:
str += "0";
str += "1";
str += "2";
str += "3";
str += "4";
str += "5";
str += "6";
str += "7";
str += "8";
str += "9";
和
str += "0123456789";
兩者雖然結果一樣,但第一種寫法會觸發10次內存的申請和釋放,而后者只觸發了一次。
如果我們每次申請內存時多分配一點,效果如何呢?
改進二:
我們將:
char* pszNew = AllocBuffer(iNewSize);
改為:
// 分配一塊新的內存,將之前的按原尺寸分配改為增加1.5
char* pszNew = AllocBuffer(iNewSize, 1.5);
累加字符串時,我們並不是按照實際需要的尺寸來分配內存,而是在此基礎上多分50%。運行結果如下:

CFastString快的仿佛飛了起來。如果上面測試函數中的iTimes不是循環次數而是並發數,也就是服務器同時處理了5000個HTTP請求,那么可以看到,CPU的處理速度得到了極大提升,也就說讓CPU避免了頻繁的malloc和free操作,在處理速度提升的同時,內存碎片也得到了降低。
當然你可能會說,內存多分配了50%,但這個50%換來了性能上的極大提升,服務器編程中以空間換時間非常正常,內存閑着也是閑着,又不是不還。回到AllocBuffer(int iAllocSize, double dScaleOut)這個函數上,我們只是增加了一個控制參數dScaleOut而已。
上面並不是嚴格意義上的內存管理,只能說是內存分配的技巧。真正的內存管理是需要預先分配N多連續的內存塊(也就是內存池),當String需要內存時從內存池中申請一塊,釋放時再還給內存池,內存池的實現很多,已經寫的太多了,就下次再介紹吧。
回到主題,如果想寫好一個高性能的服務器程序,很多細節問題都要考慮,哪怕是不起眼的字符串操作,哪怕是字符串中不起眼的累加操作。
我的HttpServer就是使用了自定義CFastString同時結合了真正的內存管理,IOCP只是保證高並發的前提,真正的把內存管理起來才能確保服務器發揮最佳的性能。
下面是CFastString案例簡單源碼,拿走不謝!
頭文件
#include <TCHAR.h>
#define DEFAULT_BUFFER_SIZE 256
class CFastString
{
public:
CFastString();
CFastString(const CFastString& cstrSrc);
CFastString(const char* pszSrc);
virtual ~CFastString();
public:
int length() const{
return m_iStrLen;
}
// 這種方式獲取字符串的長度要慢於length()函數
int GetLength() {
return m_pszStr ? strlen(m_pszStr) : -1;
}
char* c_str() const{
return m_pszStr;
}
// =============運算符重載=============
const CFastString& operator=(const CFastString& cstrSrc);
const CFastString& operator=(const char* pszSrc);
const CFastString& operator+=(const CFastString& cstrSrc);
const CFastString& operator+=(const char *pszSrc);
// =============友元函數=============
friend CFastString operator+(const CFastString& cstr1, const CFastString& cstr2);
friend CFastString operator+(const CFastString& cstr, const char* psz);
friend CFastString operator+(const char* psz, const CFastString& cstr);
// 類型轉換重載
operator char*() const{
return m_pszStr;
}
operator const char*() const{
return m_pszStr;
}
protected:
// =============連接兩個字符串=============
void Concat(const char* psz1, const char* psz2);
protected:
char* AllocBuffer(int iAllocSize, double dScaleOut = 1.0);
void ReAllocBuff(int iNewSize);
protected:
char* m_pszStr; // 字符串Buffer
int m_iStrLen; // 字符串長度
int m_iBuffSize; // 字符串所在Buffer長度
};
實現文件
#include "stdafx.h"
#include "FastString.h"
#include <stdlib.h>
#include <assert.h>
#include <TCHAR.h>
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
CFastString::CFastString()
{
m_iBuffSize = DEFAULT_BUFFER_SIZE;
m_pszStr = (char*)malloc(m_iBuffSize);
memset(m_pszStr, 0, m_iBuffSize);
m_iStrLen = 0;
}
CFastString::CFastString(const CFastString& cstrSrc)
{
int iSrcSize = cstrSrc.length()+1;
m_pszStr = AllocBuffer(iSrcSize);
m_iStrLen = 0;
//_tcscpy(m_pszStr, cstrSrc);
memcpy(m_pszStr, cstrSrc.c_str(), iSrcSize);
m_iStrLen = iSrcSize-1;
}
CFastString::CFastString(const char* pszSrc)
{
assert(pszSrc);
int iSrcSize = _tcslen(pszSrc) + 1;
m_pszStr = AllocBuffer(iSrcSize);
m_iStrLen = 0;
//_tcscpy(m_pszStr, pszSrc);
memcpy(m_pszStr, pszSrc, iSrcSize);
m_iStrLen = iSrcSize-1;
}
CFastString::~CFastString()
{
free(m_pszStr);
m_pszStr = NULL;
m_iStrLen = 0;
m_iBuffSize = 0;
}
char* CFastString::AllocBuffer(int iAllocSize, double dScaleOut)
{
if(dScaleOut < 1.0)
dScaleOut = 1.0;
int iNewBuffSize = int(iAllocSize*dScaleOut);
if(iNewBuffSize > m_iBuffSize)
m_iBuffSize = iNewBuffSize;
char* pszNew = (char*)malloc(m_iBuffSize);
return pszNew;
}
void CFastString::ReAllocBuff(int iNewSize)
{
if(iNewSize <= 0)
{
assert(0);
return ;
}
if(iNewSize <= m_iBuffSize)
return ;
m_iStrLen = 0;
// 重新分配一塊內存
free(m_pszStr);
m_pszStr = (char*)malloc(iNewSize);
m_iBuffSize = iNewSize;
}
void CFastString::Concat(const char* psz1, const char* psz2)
{
assert(psz1);
assert(psz2);
if(NULL == psz1 || NULL == psz2)
return;
int iLen1 = _tcslen(psz1);
int iLen2 = _tcslen(psz2);
int iNewSize = iLen1 + iLen2 + 1;
if(m_iBuffSize < iNewSize)
ReAllocBuff(iNewSize);
// 拷貝字符串1
memcpy(m_pszStr, psz1, iLen1);
// 拷貝字符串2
memcpy(m_pszStr+iLen1, psz2, iLen2);
m_iStrLen = iNewSize-1;
*(m_pszStr+m_iStrLen) = 0;
}
const CFastString& CFastString::operator=(const char* pszSrc)
{
assert(pszSrc);
int iSrcSize = _tcslen(pszSrc)+1;
if(m_iBuffSize < iSrcSize)
ReAllocBuff(iSrcSize);
//strcpy(m_pszStr, pszSrc);
memcpy(m_pszStr, pszSrc, iSrcSize);
m_iStrLen = iSrcSize - 1;
return *this;
}
const CFastString& CFastString::operator+=(const CFastString& cstrSrc)
{
cstrSrc.length();
int iNewSize = cstrSrc.length() + length() + 1;
if(m_iBuffSize >= iNewSize)
{
memcpy(m_pszStr+m_iStrLen, cstrSrc.c_str(), cstrSrc.length());
*(m_pszStr+iNewSize-1) = 0;
}
else
{
char* pszNew = AllocBuffer(iNewSize, 1.5);
memcpy(pszNew, m_pszStr, m_iStrLen);
memcpy(pszNew+m_iStrLen, cstrSrc.c_str(), cstrSrc.length());
free(m_pszStr);
m_pszStr = pszNew;
}
m_iStrLen = iNewSize-1;
return *this;
}
const CFastString& CFastString::operator+=(const char *pszSrc)
{
assert(pszSrc);
int iLenSrc = _tcslen(pszSrc);
int iNewSize = iLenSrc + length() + 1;
// 當內部緩存足夠時,直接進行拼接,不足時則需要開辟新的內存
if(m_iBuffSize >= iNewSize)
{
memcpy(m_pszStr+m_iStrLen, pszSrc, iLenSrc);
*(m_pszStr+iNewSize-1) = 0;
}
else
{
// 分配一塊新的內存,將之前的按原尺寸分配改為增加1.5
// char* pszNew = AllocBuffer(iNewSize);
char* pszNew = AllocBuffer(iNewSize, 1.5);
// 將字符串拷貝拼接到新開辟的內存中
// 方法一:strcpy+strcat
// strcpy(pszNew, m_pszStr);
// strcat(pszNew, pszSrc);
// 方法二:直接使用內存拷貝
memcpy(pszNew, m_pszStr, m_iStrLen);
memcpy(pszNew+m_iStrLen, pszSrc, iLenSrc);
free(m_pszStr);
m_pszStr = pszNew;
}
m_iStrLen = iNewSize-1;
return *this;
}
// ===============friend函數===================
CFastString operator+(const CFastString& cstr1, const CFastString& cstr2)
{
CFastString cstrNew;
cstrNew.Concat(cstr1, cstr2);
return cstrNew;
}
CFastString operator+(const CFastString& cstr, const char* psz)
{
CFastString cstrNew;
cstrNew.Concat(cstr, psz);
return cstrNew;
}
CFastString operator+(const char* psz, const CFastString& cstr)
{
CFastString cstrNew;
cstrNew.Concat(psz, cstr);
return cstrNew;
}
