【超值分享】為何寫伺服器程式需要自己管理記憶體,從改造std::string字串操作說起。。。

一隻會鏟史的貓發表於2021-07-27

伺服器程式為何要進行記憶體管理,管中窺豹,讓我們從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;
}

相關文章