【超值分享】為何寫服務器程序需要自己管理內存,從改造std::string字符串操作說起。。。


服務器程序為何要進行內存管理,管中窺豹,讓我們從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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM