您当前位置: 首页 »

c++

标签归档: c++

如何做好程序的性能优化

本来标题想取名成 《如何做好代码的性能优化》,但发现如果仅仅说代码性能优化的话就太狭义了。

最近一年一直在单线程框架的工程上写代码,阅读框架代码以后,不禁感叹道:“这个框架的设计者不仅是个高手,而且对windows相当了解,甚至借鉴了windows内核里面的一些设计元素。”

首先介绍一下我手里的这个框架:

1,总体来说,整个程序几乎就是单线程+异步(对于DNS解析、IO操作等一些耗时或难以异步的模块,会另启一个线程去执行和管理)。

2,在这个单线程框架中,所有的业务、网络协议栈、数据处理都在挂在这个单线程中去运行,并且绝大对数情况和业务下面都能够很好的运行。

3,模块间通讯采取模拟异步/同步事件、异步/同步消息的方式来完成

4,数据通讯很简单,直接用堆内存传递,用完立即释放。

5,定时器是最简单的“轮询”方式实现(并非真正轮询)

 

现在遇到一个比较严重的问题,就是在这个单线程模型中,由于开发的时候存在编码人技能层次不齐,以及任务力度控制不均匀。

导致开发一些功能在获得执行时间时,执行时间太长或没有及时的将任务中断并将执行时间让给其他任务执行。

这样的现象导致最严重的问题,自身流程过长,效率不一定高(如果还依赖其他模块的执行结果)。并且其他模块不能及时的拿到执行时间,并将执行结果及时反馈。进而引起血崩效应,导致部分要求时效性高的功能出现问题。

 

为了解决这类问题,就要做程序性能上的优化。这个问题上,一般采取的优化策略有几种:

1,代码级优化,就是通过各种技巧,将本来执行效率低的代码进行一点一点的修改并加快。这种优化方式周期长,效果不见得很明显,但对于长久来说是具备一定好处的;可以让优化者能够熟悉和了解整个代码的运行情况和流程等。

2,业务/功能优化,通过将长流程或者耗时的流程将功能和业务优化掉。然长流程变成短流程,然后进而增加执行时间在任务中的切换频率。可以促进轻量级任务的提早执行和结束,也间接能够提高大部分任务的及时性。但对于原本又长又臭的任务来说,此类优化可能带来的改善并不大,同时还需要优化者对程序整体有一定度的把握。

3,任务分拆,这样优化方式是将任务的关联性和时效性做一个定性分析。将相关任务集中起来,不相关任务分拆开;并将任务流中的上下游进行松耦,接着再对相关的业务进行松耦。这样做的好处在于一切以任务执行为视角,进行分拆,可以有效的区分开重任务和轻任务。同样也可以定性的了解到即使性要求高的任务和及时性要求低的任务。缺点在于,优化者同样也要对整个程序有一定的了解。

4,框架优化。这个难度大,没有做太大分析。

 

因为1和2都是夹杂着代码上技巧性的优化,这种优化如果考虑不当很有可能事倍功半,反而降低代码的可阅读性和可维护性。之前,我就遇到过一些成天嚷嚷着“算法”的人在用“算法”的思维去优化代码,结果代码优化下来性能是有一定的改善,但可维护性和可阅读性就差到极点了。甚至优化者本人自己去维度代码也是满天飞的bug。

介于4这种都是构架师水平,我最终选择3。

从任务的关联性出发,将一些重任务,以及及时性要求高的任务进行分类,并把这些任务的旁路任务进行一同整理。最终得到几类任务:

1,一般任务

2,及时性高任务

3,重任务

 

其中一般任务继续保留在原先的单线程框架中。及时性高的任务会从中剥离出来,并挂在一个新的单线程框架中;后期随着这个单线程任务量的增加,最终线程会逐步调整代码中的线程数。

重任务,其中重任务也被挂在一个新的单线程框架中,处理方式与及时性任务的处理方式一致。只不过对于及时性高的任务,可能还需要做一些代码层面上的执行优化,不过应该不多。

 

通过上面的方案进行优化了以后,发现整个客户端任务执行的拖沓、任务切换的不及时得到了较大的改善。看来改善任务安排有时候比起用一些代码技巧更为重要。

 

当然,我在这里说说是很容易的,实际写起代码来未必那么容易。因为涉及到多线程,就需要留意任务的关联性。因为关联性的存在,就会出现线程资源竞争,资源出现竞争时,就会很容易出现数据不同步,死锁,野指针等问题。

这是就需要经验和一定的代码技巧来解决这类问题。因此合理调整程序结构与代码技巧同样重要,如果一味的追求代码技巧和所谓的“算法”,那最终会失去对整个程序的可持续维护和开发的可能。这就如我之前所呆的一家公司一样,软件在国内某行业里还是有点小名气,但是真的要去看代码的话…………………bug、可阅读性不是在人类可理解范围内。反而这个团队内总有人一味的强调“算法”、“二叉树”什么的,数据结构与算法确实是程序的核心之一的东西,但现在国内的公司并非科研机构,顶多只能算一个做的工程。多数时候其实以工程的思维就能把问题解决好的,根本没有必要上升到“算法”层面,再说了,代码都没写好装什么逼呢?

2015-08-01 | | win, windbg, 数据结构 & 算法, 算法导论, 编码技巧

如何做好程序的性能优化已关闭评论

防御性编程是种好习惯,但控制不好也是个问题

最近在看一些同时的代码,发现有些代码确实烂的可以。不仅代码风格很差,就连最基本的逻辑也是不严谨的。接着就是在解析一段数据时,没有对数据中对应的字段进行有效值范围约束。如:

 

struct A
{
   int   msg_id;
   char  msg_body[100];
   short msg_param;
}

在实际中:
1,msg_id是有范围的,因为并不是所有的msg_id都被实现了。
2,msg_body里面的内容有可能是具有一定特征的,对于部分msg_id是可能不存在部分种类的msg_id
3.msg_param也有可能是有范围的。

当在一些习惯不好的程序员手里写这段数据的解析往往是


void parse(const char* buff)
{
   struct A a;
   memcpy(a.msg_id, buff, 4);
   memcpy(a.msg_body, buff+1, 100);
   memcpy(a.msg_param, buff+101, 2);

   .......... //do something
}

由于这里没有判断几个关键字段的有效值范围,很可能在dosomething的这个代码块部分会出现执行错误的问题。

2015-07-13 | | 编码技巧

防御性编程是种好习惯,但控制不好也是个问题已关闭评论

x86-p6构架支持的“断点”方式

了解了一下调试相关的内容,x86下的p6平台,大约支持的几种基本断点方式:

1,断点指令

2,向特定地址写数据

3,向特定地址读数据

4,操作特定IO地址(读写)

对于断点指令:int 3(0xcc),当cpu执行到该指令时,会检查中断向量表,转为去执行中断服务“函数”

对于想特定指令写数据,同样也会检查中断向量表(实际上很可能不是中断向量表,因为引发的是一个寄存器检查,检查异常处理函数的地址),执行对应的代码块。

对于3,4与2的方式一样。

在VS里面,对于监控某个变量是否被修改成某个固定的值,很有可能是采用了上述的2或3这两个机制。当然,我这边的vs的反汇编代码其实只是指令上的判断而已,当修改成了我指定的条件时,最终会引发DebugBreak这个函数(这个函数最终还是执行int 3(0xcc))

在VS里面还有没有发现那一种中断方式是对应着4的。

2015-07-12 | | win, 编码技巧

x86-p6构架支持的“断点”方式已关闭评论

C++中类型转换的危险用法

C++中,出了基本类型以外,就是类类型。对一个类类型的实例,均叫做一个对象。实际上C++上面,只要是一个变量都可以称作对象。

但这里狗屎的问题就有了。对象的类型是有可能存在继承关系的。

以前在C语言里面,可以有这样的变量转换。

 int a = 1000;
 long b = 0;
 b = (long)a; //这里做了强制转换

但如果在C++里面还是用C风格方式的类型转换就会存在很严重的问题。例如有这么一段代码

class IInterface_Me
{
protected:
   int a;
}

class CInterface_1 : public IInterface_Me
{
public:
 int b;
}

class CInterface_2 : public IInterface_Me
{
public:
  int c;
}

IInterface_Me* pInterface = new CInterface_1;
CInterface_2* pInterface_2 = (CInterface_1*)pInterface;

pInterface_2->b = 100;   ///这里就要出错。

对于这个问题主要原因是由于c语言风格的类型强转,主要是由于编译器不做检查,因此在C++里面应该使用安全的类型转换修饰符(static_cast之类)。

2014-04-10 | | object

C++中类型转换的危险用法已关闭评论

满街尽是地雷阵,不要随便乱用“&”引用

我们公司有这个一段代码,后来看了高效c++之后就总感觉有问题。不试不知道,一试全是地雷阵。

 

一般会写这么一个函数,返回的是引用。这样也符合C++里面的一些思想。但如果这样的函数没有用好的话,留下的就是一个地雷


std::vector<int>& Return_null_reference()
{

.....

}

例如有如下代码


std::vector<int>& Return_null_reference()
{
 std::vector<int> *pTmp = NULL;

return *pTmp;
}

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{

std::vector<int> *pTmp1 = NULL;
 pTmp1 = &Return_null_reference();

std::vector<int> &pTmp2 = Return_null_reference();

Sleep(1);
 }

我在vs 2008 sp1中编译不但能够通过,而且不会报错。执行的结果就是,ptmp1指向了一个空指针,ptmp2引用了一个空指针。

这里实际上看上去ptmp2会有问题,因为在引用的变量定义时,需要在编译器就要确定引用的对象是一个非NULL地址。但事实上这里因为引用了一个函数的返回,由于函数的返回值是属于运行时问题,所以编译器不做检查。于是就留下了一个坑。

高效C++中说过,不要随便引用一个指针指向的对象,因为有可能那个指针是指向NULL。

同时也隐约讲过引用不的不当反而比指针会更危险。

要利用编译器检查引用非NULL的特性,需要显示的指明被引用的对象是编译时就可以确定内存地址的。

2013-10-31 | | 未分类

满街尽是地雷阵,不要随便乱用“&”引用已关闭评论

一个类在被编译器处理以后包含的数据结构

class A

{

public:

}

一般对于使用者来说这个类只有1个数据结构,那就是类本身。

实际上,一个类包含了3个数据结构。

除了类本身以外,还包含了一个函数表、和指向函数表的指针。

函数表是用来保存虚函数在本类中的中的具体实现地址的。

而指向函数表的指针,值用于表示某个虚函数在函数表的地址。

2013-10-12 | | 编码技巧

一个类在被编译器处理以后包含的数据结构已关闭评论

【编译器优化】临时变量的消除

之前简单看过一点c++11的特性,里面讲了一堆左值和右值。说来说去有一部分还是在谈论关于临时变量的问题。

目前有这么一个函数:


const Rational operator*(const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.numerator() * rhs.numerator(),  lhs.denominator() * rhs.denominator());
return result;
}

上面这个函数在调用的时候因为返回的是局部变量,因此返回的时候编译器会插入代码会产生一个临时变量,然后将result拷贝到临时变量中返回出去。但让这里谈论的前提是,一些编译器的优化关掉。

而对于下面的代码来说编译器的行为就会不一样。因为这段代码已经显式的告诉编译器,这个这里会构建一个匿名的临时变量并返回,因此编译器在这里不会再次构建临时变量,而是直接将代码中创建的匿名对象直接返回出去给外面。这样做可以节约一个对象的创建和销毁时间复杂度。


const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

2013-10-12 | | 未分类

【编译器优化】临时变量的消除已关闭评论

不给函数做对象的值传递另一个重要原因

在调用函数时,一般都不建议采用值传递,原因是因为要建立一个临时的拷贝;这样会带来空间和时间复杂度的提升。

一般除非很特殊情况,很少把对象以值的方式进行传递到函数里面。

实际上,不讲对象做值传递还有一个比较重要的原因。

在C++的多态中,如果父类的指针指向了一个子类对象,并对该对象做值传递,会导致子类数据丢失。大致代码如下:


class CA

{

};

class CB : public CA

{

};

&nbsp;

void func(CA src);

&nbsp;

int main()

{

CA* p1 = new CB;

func(*p1);

....

}

在这个代码里,实际上调用func时,使用了CA的默认拷贝构造函数,因为对于CB的所有数据就丢失了,在func里面的临时对象也仅仅是CA类型。

2013-10-11 | | 编码技巧

不给函数做对象的值传递另一个重要原因已关闭评论

++重载符

首先说一下++重载符,++分为前后两种方式的调用。因此就有了两种的符号的调用。大致如下(对于后置++的做法采用了不严谨的重载,返回的应该是 const对象)

namespace class_cplusplus10
{
class CBase
{
public:
CBase&amp; operator++()        //这里是前置++调用
{
printf(_T("class_cplusplus10::CBase&amp; operator++()\n"));
return *this;
}
CBase&amp; operator++(int)     //这里是后置++调用
{
printf(_T("class_cplusplus10::CBase&amp; operator++(int)\n"));
return *this;
}

CBase&amp; operator--()
{
printf(_T("class_cplusplus10::CBase&amp; operator--()\n"));
return *this;
}
CBase&amp; operator--(int)
{
printf(_T("class_cplusplus10::CBase&amp; operator--(int)\n"));
return *this;
}
};

}

上面的代码已经很好的说明了哪一些是前置调用重载,哪一些是后置调用的重载。

然后通过这些函数可以来看看一些哗众取宠的笔试题。

class_cplusplus10::CBase base;
++base++;

问++base++的调用是怎样的,通过调试发现,实际上后置++是先被调用,然后前置++。

不过这里是c++,而这种笔试题往往考的是操作符的优先级。也许一个对象和一个内置类型的变量存在一些不同

2013-10-10 | | 编码技巧

++重载符已关闭评论