c语言部分的开销测试

最近在写c代码时底气越来越弱,原因在于某些调用的开销,我心里并不是十分明确。写起代码来由于纠结也就变的畏畏缩缩。

今天终于抽时间测了一下,仅测试了最近常遇到的一些调用的开销。

测试环境如下:

CentOS release 6.7(Final)

CPU:Intel(R)Xeon(R)CPU E3-1230 V2 @ 3.30GHz

采用gcc(glibc)编译,未开任何优化。
在测试时,大部分操作cache均会命中,因此如果cache经常命中失效,还需要另外考虑。

测试结果如下:

可以看出for循环是最廉价的。

malloc是开销最大的,这还是在单线程的情况下,如果再多线程由于锁的开销malloc的代价将会更甚。

在栈上分配1M次内存的代价几乎等同于for了1M次,在栈上分配内存开销最低。

而copy了64M内存也才花费了5ms而已。

lua gc的使用

主流的垃圾回收一般都是基于引用计数和标记清除算法.

从内存占用量上来讲, 引用计数无疑是有优势的, 当引用计数为0时, 直接就会将相应的对象清除, 典型的应用就是C++的智能指针. 但是基于引用计数的gc有一个坏处, 它无法解决循环引用问题. 如果A引用B会导致B的引用计数+1, B引用A也会导致A的引用计数+1, 这样A,B对象永远也不会删除. 记得在OC中似乎使用了弱引用来解决这个问题, 但总感觉这样会给代码中埋下坑.

标记清楚算法可以完美的解决循环引用问题, 其做法是从root遍历所有可达的指针, 然后将不可达的区域回收. 这样即使A和B循环引用, 只是没有其他对象来引用A和B, 那么A和B对象即是不可达. 此时A和B对象即可删除. 但是标记清除算法有一个致命缺陷, 就是在gc时需要停顿整个程序来进行mark-sweep.

最近抽了点时间研究了下go语言, 果然可能是由于太年轻, go也有这个坑. 在搜索资料时发现很多人都在吐槽go的gc部分, go的主要应用领域就是服务器部分,然而据说go的gc会造成不可预知的停顿,这对于服务器来讲是非常致命的.

看到这, 我突然非常担心lua也有此问题, 而在silly中也是以lua来写上层逻辑的, 很担心lua中的gc是否也会造成stop-the-world. 于是找来手册重新认真的读了一下, 发现以前对于lua的gc的工作方式太想当然了, 以致于造成了对lua的错误使用.

lua中的gc算法是采用增量标记清除算法, 通过一次执行一小步来将一次完整的gc时间分摊在每一步上, 提高了整个程序的实时性.(具体如何做的,还要等看完源码才知道:D)

在lua中是分为手动gc和自动gc的.

通过设置垃圾收集器间歇率和垃圾收集器步进倍率来控制自动gc的运行方式.

有某些特殊的条件下, 如果我们需要手动控制gc的时机, 这时就可以LUA_GCSTOP来停止自动垃圾回收器. 然后在恰当的时机手动调用LUA_GCSTEP来发起一次增量垃圾回收.

这样看来, 至少在gc的策略上, lua应该做的比go要好. 当然具体情况还是要等看完实现才能更确切的比较.

btw, 作为一门现代语言, gc已经是一个不可缺少的部件. 然而gc的问题在于, 在平时开发时完全体会不到问题, 一旦在大量数据面前就有可能会出现各种情况. 因此熟悉一门语言绝不仅仅是熟悉其语法就好了, 还必须要深刻了解该语言的Gc机制, 才能做到心中有数.

2015

眨眼间2015年已经过去了, 也许是我最近记忆力变差了, 总感觉好像昨天才辞职的样子。

在2015年年初辞职时, 其实我的内心非常的纠结,一方面是安逸,一方面是未知, 很难下定决定到底何去何从。

终于还是求知欲战胜了懒惰。因为我发现如果再干着同样的工作,我的技术不太可能提高太多。所以最终我还是选择了辞职,由于上一份工作的原因,相比前端程序来讲,我对于后端的兴趣更大。因此在找工作方面,我更着重于找服务器编程方面的工作。因此后来就找了一份游戏服务器工作.

现在想想刚入职时闹的一个笑话都还脸红。才入职时,基于对新人的保守使用,都是先编写UI逻辑。而客户端的UI逻辑是采用lua+异步的方式来实现的。而我当时并不知道什么是异步,甚至还想在lua中使用sleep来保证代码的时序逻辑。直到后来我在silly中集成luaVM时,我才终于知道客户端lua+CPP中异步到底是如何工作的。

虽然当时我很想直接去写服务器代码,但是即然写了客户端,也就花了大约2周时间去学了一下《3D数学基础》。再说依我的性格虽然准备写服务器, 但一点都不了解客户端,那也不是我的性格 :), 不过在学习过程中,我发现游戏客户端其实就是在用各种自然学科的知识去模拟整个世界,也就是在这时候,我却突然对客户端代码也产生了兴趣。

这时分给我的UI工作刚好做完, 手头上暂时没什么事, 就硬着头皮研究了一下客户端的引擎代码部分。物理部分和声音部分都是不开源的,因此能研究的也就只有动画部分了。在研究了几W行动画相关代码之后,终于大致知道了一些动画渲染的原理。比如骨骼动画,动画融合等。

粗略研究了一下动画原理之后, 就顺便研究了一下客户端多国语言的实现。上一份工作中,我对于自己多国语言的设计非常不满意。看完这个多国语言的实现之后,我才明白原来当是自己是过度设计了,在简化了设计之后,我只花了几十行就重新实现了一个多国语言模块。

下一阶段的开发任务来了,我终于能够编写服务器代码了(虽然只是服务器中很简单的逻辑部分), 不过却见到了一些我从没想过的C++的用法, 由此又顺便学习了一下C++的一些高级用法(比如模板推导等)。与此同时我的个人服务器框架也正基本上正式开始编写了。

在接触到服务器编程之后, 由于是TPS游戏都是开房间的,因此我一直都是思考如何才能让所有玩家都可以在同一张地图上进行游戏。在此之前一直都想不通要怎么做。在网上搜了很多文章也都没有找到什么好的办法,我只好暂时暂停了此问题的思考,转而去研究了《redis的设计与实现》来换换脑筋。在《redis的设计与实现》的最后部分,作者讲了一下redis对于集群的设计,一下令我慌然大悟

在下一个开发阶段,由于业务需求,我终于要单独写一个服务器,想想都令人激动。在摆脱了以往框架限制的同时,我也需要从头来实现一些机制。 正是在实现这些机制的同时却让我对TCP协议有了更深的了解,比如tcp的端口绑定, TIME_WAIT等。就这样基本上都是上班写代码,然后发现自己有什么理解错误就下班回来改silly.

终于开发任务完成了, 又有一段时间可以搞自己的东西了.由于很多UI上的bug都不是100%必现的,而lua的调试手段一般就是加print来打印一些变量的值。当出现bug时我们需要打印一下当时一些变量的值就非常的不方便,于是就琢磨着写了一个简单的lua调试器.这样在出现bug时就可以拿lua调试器直接attach上去来打印当时的call stack以及以些变量的值等需要的调试信息,可以大大减少了bug重现的需要。

在很早以前就知道google protobuffer这个东西,而且也对编译原理有所眼馋。在silly中上层逻辑都是使用lua来编写,在socket传输序列化时很不方便,因此老早就想实现一个类似google protobuffer的东西了。趁还有时间就花了几周看了一下编译原理的词法和语法部分,然后实现了zproto, 用作silly的配套lua序列化库。这个库直到上周才终于完成了。


总得来说,如果2014年我得到的是新的技能的话,那么2015年我得到的其实就是经验。而获取经验的最大途径并不是来自于工作,而是对于silly的一次次的重构甚至重写。

所谓的经验其实就是各种取舍,在编码的过程中, 总会遇到各种情况,在这时就需要靠经验来进行取舍。

比如linux中的seqlock就是估算了index的递增速度以及linux调度时间来大大降低了实现的复杂度。

在silly中关于连接号的管理也同样借鉴了类似的方式大大降低了实现的复杂度。

因此在编码过程中,我们需要会的不仅仅是技能,还有对于问题的取舍,怎样做到功能和复杂度的平横,才是一个程序员迫切需要解决的问题。

在2016年,我打算阅读完luaVM的源码,一个脚本语言的设计,一个栈式虚拟机的实现,仅此而已。