服务器程序为何要进行内存管理,管中窥豹,让我们从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;
}

【超值分享】为何写服务器程序需要自己管理内存,从改造std::string字符串操作说起。。。的更多相关文章

  1. IM服务器:编写一个健壮的服务器程序需要考虑哪些问题

    如果是编写一个服务器demo,比较简单,只要会socket编程就能实现一个简单C/S程序,但如果是实现一个健壮可靠的服务器则需要考虑很多问题.下面我们看看需要考虑哪些问题. 一.维持心跳 为何要维持心 ...

  2. 《用Java写一个通用的服务器程序》02 监听器

    在一个服务器程序中,监听器的作用类似于公司前台,起引导作用,因此监听器花在每个新连接上的时间应该尽可能短,这样才能保证最快响应. 回到编程本身来说: 1. 监听器最好由单独的线程运行 2. 监听器在接 ...

  3. 《用Java写一个通用的服务器程序》01 综述

    最近一两年用C++写了好几个基于TCP通信类型程序,都是写一个小型的服务器,监听请求,解析自定义的协议,处理请求,返回结果.每次写新程序时都把老代码拿来,修改一下协议解析部分和业务处理部分,然后就一个 ...

  4. 以技术面试官的经验分享毕业生和初级程序员通过面试的技巧(Java后端方向)

    本来想分享毕业生和初级程序员如何进大公司的经验,但后来一想,人各有志,有程序员或许想进成长型或创业型公司或其它类型的公司,所以就干脆来分享些提升技能和通过面试的技巧,技巧我讲,公司你选,两厢便利. 毕 ...

  5. 腾讯技术分享:微信小程序音视频与WebRTC互通的技术思路和实践

    1.概述 本文来自腾讯视频云终端技术总监rexchang(常青)技术分享,内容分别介绍了微信小程序视音视频和WebRTC的技术特征.差异等,并针对两者的技术差异分享和总结了微信小程序视音视频和WebR ...

  6. 腾讯技术分享:微信小程序音视频技术背后的故事

    1.引言 微信小程序自2017年1月9日正式对外公布以来,越来越受到关注和重视,小程序上的各种技术体验也越来越丰富.而音视频作为高速移动网络时代下增长最快的应用形式之一,在微信小程序中也当然不能错过. ...

  7. 性能追击:万字长文30+图揭秘8大主流服务器程序线程模型 | Node.js,Apache,Nginx,Netty,Redis,Tomcat,MySQL,Zuul

    本文为<高性能网络编程游记>的第六篇"性能追击:万字长文30+图揭秘8大主流服务器程序线程模型". 最近拍的照片比较少,不知道配什么图好,于是自己画了一个,凑合着用,让 ...

  8. PIC12F629帮我用C语言写个程序,控制三个LED亮灭

    http://power.baidu.com/question/240873584599025684.html?entry=browse_difficult PIC12F629帮我用C语言写个程序,控 ...

  9. 分享:linux下apache服务器的配置和管理

    linux下apache服务器的配置和管理. 一.两个重要目录: Apache有两个重要的目录:1.配置目录/etc/httpd/conf:2.文档目录/var/www: 二.两种配置模式: Apac ...

随机推荐

  1. 【NX二次开发】Block UI 截面构建器

    属性说明 属性   类型   描述   常规           BlockID    String    控件ID    Enable    Logical    是否可操作    Group    ...

  2. 6.10考试总结(NOIP模拟6)

    前言 就这题考的不咋样果然还挺难改的.. T1 辣鸡 前言 我做梦都没想到这题正解是模拟,打模拟赛的时候看错题面以为是\(n\times n\)的矩阵,喜提0pts. 解题思路 氢键的数量计算起来无非 ...

  3. FreeRTOS移植EasyFlash

    1. EasyFlash Easyflash可以让 Flash 成为小型 KV 数据库(Key-Value) GitHub: https://github.com/armink/SFUD Gitee: ...

  4. Kubernetes Pod中容器的Liveness、Readiness和Startup探针

    我最新最全的文章都在南瓜慢说 www.pkslow.com,欢迎大家来喝茶! 1 探针的作用 在Kubernetes的容器生命周期管理中,有三种探针,首先要知道,这探针是属于容器的,而不是Pod: 存 ...

  5. 一文说透 Go 语言 HTTP 标准库

    本篇文章来分析一下 Go 语言 HTTP 标准库是如何实现的. 转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com/archives/561 ...

  6. 理解vertical-align

    vertical-align 支持的属性值及组成 inherit 线类baseline, top, middle, bottom 文本类text-top, text-bottom 上标下标类sub, ...

  7. Java并发之Semaphore源码解析(一)

    Semaphore 前情提要:在学习本章前,需要先了解笔者先前讲解过的ReentrantLock源码解析,ReentrantLock源码解析里介绍的方法有很多是本章的铺垫.下面,我们进入本章正题Sem ...

  8. 关于Excel中表格转Markdown格式的技巧

    背景介绍 Excel文件转Markdown格式的Table是经常会遇到的场景. Visual Studio Code插件 - Excel to Markdown table Excel to Mark ...

  9. 7、解决windows10家庭版无法远程连接服务器的问题

    (1)方法一: 升级windows10为专业版,因为win10家庭版没有组策略: (2)方法二:通过远程命令: 同时按住"win+r"键调出"运行",在方框内输 ...

  10. 教你几招HASH表查找的方法

    摘要:根据设定的哈希函数 H(key) 和所选中的处理冲突的方法,将一组关键字映象到一个有限的.地址连续的地址集 (区间) 上,并以关键字在地址集中的"象"作为相应记录在表中的存储 ...