现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

读《Windows编程启示录》有感

2013-09-24 10:45 工业·编程 ⁄ 共 2776字 ⁄ 字号 暂无评论

    最近拜读了Raymond Chen的The Old New Thing(中译名《Windows编程启示录》),作者Chen是微软Windows部门的资深软件工程师。总体感觉,这是本很有价值的书,它的价值体现在对Windows这个系统的细节剖析——关于Windows,我想很多windows开发人员很推崇的一本书是《Windows核心编程》,《核心编程》这本书某种程度上可以看成操作系统教科书的翻版,进程、线程、同步、内存、IO一章一章的写,确实是win编程的不错工具参考书。但《启示录》这本书就是另外一种风格了,作者关心的是win系统的一些有趣的细节,诸如win窗口管理及其内在机制、VC++编译器、向后兼容性、国际化编程、win安全机制等等,并以风趣的语言将其成文成章,给人另外一种别样的参考:"原来,Windows这样设计是有原因的!"因此对于看管了经典教科书的win开发人员来说,读一下这本书,应当会在细节方面有很好的收获。

    废话少说,步入正题:按照我的理解整本书可以分成两部分:windows设计问题和windows开发问题,前一部分属于比较浅显比较吸引人的,作者从win95开始娓娓道来,解释了很多win UI的设计原因——诸如为什么有个Start按钮,为什么关机选项要放在Start中等。后一部分则属于比较难懂的,也是适合开发人员好好阅读的一部分,牵扯到win系统很多的实现细节,其中我最欣赏第十一章——General Software Issues,如作者所言:Although these topics may take Windows as their starting point, they are nonetheless applicable to software development in general,有些体会想跟大家分享。

    很多程序员写程序时都会想到的一点就是拼命优化——时间上和空间上,但你有没有这种感觉,你绞尽脑汁想出来的一种优化策略,最终却花费了更大的时间成本?Chen就给我们举了一个很简单的例子——试想一个简单的小程序, 目的是获得当前函数的返回地址。这样的程序可以通过编译器内联函数_ReturnAddress很容易实现:

    上图左边代码是c++程序,右边是相应的汇编代码。这个时候有聪明者可能会想:为什么要这么麻烦?一条Pop指令不就搞定了嘛:

上面程序,pop指令将ESP直接送入currentInstruction,两条指令V.S.四条指令,显然两条指令胜出!然而,事实刚好相反。

实际上,x86的处理器隐藏了很多我们在处理器手册中也不一定能查阅到的东西——比如除了大家耳熟能详的程序调用栈,处理器为了优化还会为call/ret指令"量身定制"一个"返回预测"栈:当遇到call指令时,处理器将应当返回的地址(即ESP)压入"返回预测"栈(当然也会压入正常的调用栈),当遇到ret指令时,做相应的出栈(Pop)操作,同时处理器会做投机:它认为"返回预测"栈的栈顶即为应当返回的地址,因此首先将返回地址移入返回预测栈的栈顶地址,如果有问题再做相关的纠错操作。这样第二种方法的弊端就出来了:它额外添加了一条call指令而没有相应的ret指令,当然这样正确性肯定是没问题的,但导致的后果就是返回预测栈的栈顶是——L1! Pop指令对返回预测栈不起作用,这样当前程序返回时,处理器会简单的将返回地址设置为L1——之后就是不断的Page Fault 处理压栈的出错代码 Page Fault 处理压栈的出错代码…自然会花费更长的时间!

Return address
predictor stack:

L1

->

caller1

->

caller2

->

caller3

->

...

Actual stack:

caller1

->

caller2

->

caller3

->

caller4

->

...

    作者提到的另外一个"敏感问题"是内存泄露——malloc\calloc\relloc\free\delete…种种让人头大的东西,作者提到,导致一个server效能变低乃至崩溃的常见原因就是换页(Paging),为什么会换页?内存不够用了呗,为什么内存不够用?内存泄露了呗!

这里我们管理内存的时候经常有的一种思维方式就是:分配内存时,分配最大的,回收内存时,回收最大的(如果比当前保管的内存区域大),这样双管齐下,保证能有有效的内存供我使用,可这样的性能呢?想想OS课中讲过的Best Fit Allocation, Worst Fit Allocation? 作者给出了这种想法的一个简单实现:

上图左边可以认为是个简单的堆管理器,右边则是该管理器分配、回收内存的代码,GetBuffer是向管理器请求内存,ReturnBuffer是向管理器返回内存(即回收),顺带着提一下原书中的一个错误:GetBuffer的第二行代码 LocalSize(m_pCache)>=cb, 原书中印成了<=cb J

这种简单稳妥的想法,实际中却被证明内存大量被浪费,原因即在于实际中的内存请求多为较小块的请求,堆管理器回收的内存也多为小块内存。对于小块的内存请求,如果给其分配最大的可用内存块,会造成大量浪费。但当偶尔一个大块内存请求到来时,很可能的情况是没有新的内存可供使用——原来的大块内存都被分配一满足到更有概率出现的小块请求了,这样需要重新开辟新的大块内存!即使之前分配过的大块内存在某个时刻被返回了,之后的内存请求也很有可能是小块的,这样这一大块的内存区域又被浪费,如此周而复始,肯定会造成大量的内存泄露(其实照这么说,不能严格的算泄露,毕竟内存还是被稳妥的管理着,但因为这些也都发生在malloc calloc free这些函数的执行过程中,姑且这么叫了吧^_^)

解决办法呢?很简单,回收内存时,只回收较小的内存块就可以了,一个简单的修改:

注意到红色标注的部分:我们只回收比较小的内存,有大块的内存需要回收我们还不保留,只是简单的free掉。这样做的好处是什么呢?我们不需要很大块的内存,这样做只保证管理器当前拥有的空闲内存块不那么大,这样可以保证对于大部分小块的内存请求即能满足又不会有内存浪费,只有对于大块内存请求才需要进行重新分配,但重新分配的大块内存使用完毕被收集时,因为很大,按照当前方法就被简单的free掉了。这就是一种"对症下药"的方法,杀鸡焉用牛刀?对于大部分的小块内存请求,为什么非要分配最大块的内存来满足?小块的就行了J

总之,在《Windows编程启示录》中,类似这样的例子数不胜数,这本书确实给我们提供了很多全新的视角,让我们更清晰的了解到Windows系统的一些细节。遗憾的是我现在还没有看完,只有期待空闲时间比较多的时候再细细拜读,也希望有时间有兴趣的朋友能好好读一下,相信能给您带来很多启发~

给我留言

留言无头像?