您当前位置: 首页 »

编码技巧

分类目录归档: 编码技巧 - 第3页

【windows】关于频繁创建和关闭进线程开销大的原因理解之一(1)

首先从进程角度去看,内部其实是由一个或者若干个线程组成。因此在理解进程频繁创建和关闭导致的开销问题,只需要理解一个单一线程的频繁创建和关闭即可。

CreateThread、TerminateThread

这两个函数一个用于创建线程,另一个用于关闭线程。

1,先说一下线程创建的过程。对于CreateThread来说,创建的均为用户态线程。尽管创建的是用户态线程,但对于函数被调用者来说,本身还是需要切换到内核态或等待内核态服务线程将线程创建起来。

仅从这一点去看,有了态切换。显然就存在各种开销。

2,在内核创建好线程以后,这个线程将会被纳入windows的线程调度中,同时该线程所占有的内存资源也会被纳入内存调度中。仅从这里去看,又可以看到两个调度器的任务增加,且内存资源在减小。

3,在线程退出后,内核需要对其内存进行回收。在回收过程中,需要对线程的堆栈等系统分配给的内存资源进行校验(退出时的crash生成也在这一步)。检查完成以后,又要将回收回来的内存插回到windows的堆栈管理器里面(不管是crt堆,还是win32堆或者系统堆,都要插回去)。在回插内存的时候,可能还会引发堆内存的合并和子堆往父堆进行归还的过程。

 

通过以上描述,基本可得知线程的创建和关闭的开销是相当大的事情。

如果再深入一点看的话,可能会更清楚一些。

由于在windows系统以后,系统会“虚拟”出一个system进程,该进程内部其实是一堆辅助线程(有时候创建IO句柄时,也会出现这个进程里面的线程数增加,或者做调试的时候也有可能会出现类似的现象)。

system里面的线程,主要是用于服务在不同IRQL和内核用户态之间。换句话说,相当于一个桥(个人猜测,不负责哈!)。

当一个线程要进行关闭时,该system里面的辅助线程(15级以上),会对线程回收工作进行处理。换句话说,如果一个线程自己是正常退出的,那么自己会抛一个系统请求给system,让system把自己的内存回收掉。这个过程又要走一堆windows服务程序(服务端口),从这一点上看可得知。

一个线程的关闭,势必会激活一个“紧急”任务的进行,且会涉及到一个较长的流程。应此无论是创建还是关闭线程,如果执行频度太过于高,显然是不行的。

2015-12-15 | | win, win api, 编码技巧

【windows】关于频繁创建和关闭进线程开销大的原因理解之一(1)已关闭评论

关于windows是一个非实时系统的验证和讨论

再看《深入理解windows》的时候,书中提到windows并非总是一个实时系统。主要原因是由于系统本身的设计,以及驱动,应用软件各方面一同构成的;然后就偶然了解到RTX这个辅助工具。

看了一下RTX的介绍,实际上RTX就是一个和windows内核相仿的一个系统。然后RTX会工作在一个或者几个独立的vcpu/cpu上,使得该vcpu/cpu变成一个实时系统。进而进行部分接管windows的一些中断响应、驱动操作。

然后就看到这个RTX的关于如何配置的视频时,听到对方介绍到用到RTX和没有用到RTX的系统。在用到了RTX的windows里面,对于一个1us采样的5s beep音频来说,能够流畅的播放出来。而对于一个没有使用RTX的是系统来说,就会出现断断续续的问题。

接着因为感兴趣对方是如何测试,就随手写了一段代码:


#include "stdafx.h"
#include <Windows.h>

int _tmain(int argc, _TCHAR* argv[])
{
for (int i =0; i<5000; ++i)
{
DWORD dwstart = GetTickCount();
Beep(750, 60);
DWORD dwend = GetTickCount();
printf("now cur %d, cost time %d\n", i, dwend - dwstart);
}

Sleep(1000*3600);
return 0;
}

 

由于gettickcount本身也是个严重不准的时间函数,所以也只能间接看一看;且整个声音均为断断续续。

通过测试发现,每个循环周期都有可能不是稳定的,有时候会存在2-5ms的误差,也或许和gettickcount的分辨率有关,或许确实和beep函数有关。

 

后来通过代码修改为如下,整个音频则变为流畅:


#include "stdafx.h"
#include &lt;Windows.h&gt;

int _tmain(int argc, _TCHAR* argv[])
{
Beep(750, 5000);
Sleep(1000*3600);
return 0;
}

通过上述观察然后跟踪了一下beep的汇编,汇编代码如下:
无标题

无标题

在msdn在NtDeviceIoControlFile中的描述:
无标题

参考beep函数的说明以后,在通过上述的描述;可见每调用一次beep始终是等待硬件处理完成以后,才会返回beep。然后又因为windows为非实时系统,所以就会出现上诉的代码1会出现发声会出现断断续续的问题。因为各种中断什么时候能够被处理,或者处理结束都是没有很强的约束性的。

当然,sleepex本身也是一个要走软中断的模块,显然也会影响到代码的发声情况。至于sleepex的精度情况,我自己没找到msdn官方说明,但参考waitforsingleobject和sleep,那sleepex精度也不会很高。

2015-10-28 | | win, win api, 编码技巧, 音视频_图像相关, 音视频编解码

关于windows是一个非实时系统的验证和讨论已关闭评论

如何做好程序的性能优化

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

最近一年一直在单线程框架的工程上写代码,阅读框架代码以后,不禁感叹道:“这个框架的设计者不仅是个高手,而且对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构架支持的“断点”方式已关闭评论

windbg 批量分析dump

最近参与的一个项目中,由于重复的dump太多,为了简单处理问题。因此将dump放到同一个目录内进行集中解dump,下面是dump批量分析脚本

 

for %%i in (*.crash) do ( echo %%i
my_windbg_a.lnk -y "SRV*D:\windbg_symbol*http://msdl.microsoft.com/download/symbols;E:\tmp\pdb" -z E:\tmp\crash1\%%i -WX -Q -logo E:\tmp\crash1\%%i.log -c "!analyze -v;q"

my_windbg_a.lnk是给windbg做的一个快捷方式
-y代表符号表位置
-z是 dump的位置
-Q代表退出的时候不显示对话框
-logo输出日志
-c发命令到windbg中

2015-01-08 | | win, windbg

windbg 批量分析dump已关闭评论

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

class A

{

public:

}

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

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

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

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

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

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 | | 编码技巧

++重载符已关闭评论

[chromium]MessageLoop控制所谓的“消息嵌套”问题

所谓的消息嵌套,说白了,就类似函数递归。下面存在这样一种场景,会导致所谓的嵌套发生。

需要解决一种使用场景,例如主线程post一个任务到MessageLoop的队列中,然后调用MessageLoop::Run
这是进入消息处理,而这个抛入的任务实际上做的也是调用MessageLoop::Run
那这就陷入了2个问题
1,要么出现多次“嵌套”,直到最后一个待处理的任务被执行完,然后再逐级返回,以至于最后一个任务被处理完。
2,要么直接死循环或者说是饿死。因为有可能抛入的任务都是调用同一个线程的MessageLoop::Run,然后最后被处理的任务被停在了事件等待上面,这个事件是用于等待有新MessageLoop任务抛入的。

因此nestable_tasks_allowed_ 标记就成了关键,这个标记控制着不允许任务会被嵌套的情况发生。

2013-06-04 | | win, 编码技巧

[chromium]MessageLoop控制所谓的“消息嵌套”问题已关闭评论

[chromium]MessageLoopProxyTest单元测试中的一个隐蔽问题

代码如下

  MessageLoop* task_run_on = NULL;
  MessageLoop* task_deleted_on = NULL;
  int task_delete_order = -1;
  MessageLoop* reply_run_on = NULL;
  MessageLoop* reply_deleted_on = NULL;
  int reply_delete_order = -1;

  scoped_refptr task_recoder =
      new LoopRecorder(&task_run_on, &task_deleted_on, &task_delete_order);
  scoped_refptr reply_recoder =
      new LoopRecorder(&reply_run_on, &reply_deleted_on, &reply_delete_order);

  ASSERT_TRUE(task_thread_.message_loop_proxy()->PostTaskAndReply(
      FROM_HERE,
      Bind(&RecordLoop, task_recoder),
      Bind(&RecordLoopAndQuit, reply_recoder)));

  // Die if base::Bind doesn't retain a reference to the recorders.
  task_recoder = NULL;
  reply_recoder = NULL;
  ASSERT_FALSE(task_deleted_on);
  ASSERT_FALSE(reply_deleted_on);

  //--- 这里将是比较关键的一个问题 begin ---
  UnblockTaskThread();
  current_loop_->Run();
  //--- 这里将是比较关键的一个问题 end ---

  EXPECT_EQ(task_thread_.message_loop(), task_run_on);
  EXPECT_EQ(current_loop_.get(), task_deleted_on);
  EXPECT_EQ(current_loop_.get(), reply_run_on);
  EXPECT_EQ(current_loop_.get(), reply_deleted_on);
  EXPECT_LT(task_delete_order, reply_delete_order);

readmore

2013-06-04 | | win, 编码技巧

[chromium]MessageLoopProxyTest单元测试中的一个隐蔽问题已关闭评论

【编码技巧-win】关于字节编码长度在windows里面的处理

win 2k时代开始,微软用的中文系统编码均是unicode,而unicode编码被微软使用固定的2个字节来存放(也许这就是和unicode字符集的编码长度有关)。

因此,在中文windows平台中可以直接等同这么看

unicode便编译的代码,并且和unicode版本的微软库打交道的代码。在判断字符串时是否含有中文时,只要循环遍历字符串中的每一个字节,判断对应的 uint8 是否 大于 0x7F。如果大于,说明有中文一类的,非ascii字符。将对应的字符串buff,转换成wchar即可。否则直接按照char方式来用即可

 

2013-04-08 | | win, 编码技巧

【编码技巧-win】关于字节编码长度在windows里面的处理已关闭评论