« 学习从历史开始 | 返回首页 | 胡思乱想续 »

胡思乱想

这两周过的很混乱,主要是从程序部分脱出来,在写游戏的策划案。没怎么写代码,人有点空虚。策划案都是文字活,脑子里想是一回事,写出来又是回事。还有很多细节似乎是因为没想明白,所以表达不清。还得努力。

今天有个 blog 的读者在 gtalk 上督促俺,很长时间没更新了,不准偷懒,好多人看着呢。我从一开始就没打算为别人写 blog ,自己想到啥就写啥,没在意多少人在看。就这样有一茬没一茬的写着,为自己做一个记录。

今天写这么一篇,倒不全因为有美女鼓励。其实在下午百无聊赖的时候就想敲点什么了,一摸键盘又觉得没想清楚。在 blog 管理界面里已经有好几篇这样的稿子,写完了就那么放着而没有公开。生活若不是为了生存,那么就自然会充斥着胡思乱想,这些年我就这么个状态。偶尔想明白点什么,就写下来。而更多的,来也也快去的也快。

其实最开始想写的还是技术上的东西,大致有两点。

第一个是关于网络对时的,这个问题反反复复折磨我很多年了(去年写过一篇 blog,但相关纠缠着我们项目的问题不仅于此)。当然我不是要在这里向谁寻求答案,所以如果读完这篇 blog 想跟我讨论 NTP 协议或是相关技术细节的朋友,在下就不奉陪了。这也不是个什么复杂不能解决的难题,下面是想写一个衍生问题:

我们现在大多数的软件模型是不考虑时间因素的。我们关心的是输入和输出。各种编程语言也是如此,只见到语言的设计和实现去追求运行时的时间效率,不见从语言上严格定义一段代码的运行时间。当然,绝对时间是不能定义的,它会随着硬件发展而变化。但相对时间理论上可被定义的,可最终还是被人忽略了。我们研究算法,也只探讨时间复杂度而已。当然这跟现在计算机的模型有关系,在现有模型下,一段代码的严格运行时间甚至是不可能精确测度的。

可惜,很多软件却不得不考虑时间因素。我们把核电站之类先放在一边。那些对时间极其敏感,又危及人类性命的工业系统肯定对时间控制更为重要。还是轻松一点,只谈游戏。做这种实时交互系统,有人参于其中的时候,光是结果输出正确是不够的,必须是输出结果在数据数值上满足预期,又需要在规定的时刻输出,这才称作游戏软件的功能正确。

因为对于大多数互动游戏,人的感觉才最重要。

这几天在重读一本很早出版的科普读物《宇宙的琴弦》。这本书 02 年的时候在北京买过一本,黑皮的。当时是特地想看,可武汉没买到。正好那几天去北京游玩,一个网友陪我在海淀逛书店,走了几家才找到。花了不到一周时间读完后,被一个朋友借走。然后这本书就消失了。

前几天又是在北京,陪表妹逛书店,发现这本书再版了,顺手买了下来。时间过的真快呀,一晃就是五年。唉,想读第二遍的书还是不要轻易出借的好。

没事重读了几页,前面花了一大篇讲相对论。不同于我前段看的《相对论的意义》,这本书基本是通俗版的。不谈数学,但是说的很清楚。读着书,胡思乱想下去,时间到底是什么?没有运动,时间毫无意义。人理解的时间,恐怕更多的是映射为一种心理感觉(心理活动?)。你永远无法确定你的一秒和另一个人心里的一秒是不是一致,只能从人的生理结构上推断两个人作为两个系统基本相似。

我一直以来最喜欢用的软件调错手段就是录象。记录下软件的所有输入,在调试过程回放。采用白盒的方法,跟踪进入系统内部,找到每个错误的根源,然后修正。对于如今软件行业流行的单元测试方法,在我经历的软件项目中,反而是颇为不屑采用的。

和同行交流时,提到这点。太多朋友表示在实际项目进行时,录象机制难以实施。我的个人看法是,那完全是因为没有把录象机制放在最根本的位置去考虑软件的结构设计。以现在的计算机模型,录象机制总可以被建立起来。且即使系统复杂如人脑,任何问题都可以通过录象回放精确找到根源。(ps. 同时也要注意到,电脑现在的所用模型和人脑有相当大的差异。“系统复杂如人脑”只是一个比喻)

这就好比我们把一个复杂系统的时间无限拉伸(单步跟踪调试,或是添加更为详尽的追踪记录),用人脑去做细致分析。对于系统本身,时间是无关因素。(因为以现在的机器模型,时间是以数据的形式加在上层的,也可以作为输入的一部分。)无论实际运行还是被剖析,系统都可以严格保持表现一致。

当然,因为现在各种庞大的软件,都依赖着各种闭源的第三方库,以及诸多操作系统级的软件设施,而这些设施并没有建设在统一的录象模块的基础上。使得我们的工作变的复杂许多。如果需要让录象回放系统正确工作,还需要为它们各做一个中间层隔离。在中间层加入我们统一的录象部件。

在实现录象回放机制时,给交互系统加入心跳显得尤为重要。心跳是一种隔离交互系统中时间维度对软件影响的简洁方案。如果输入的数据是时间敏感的,那么通过心跳控制,可以把原本无法准确度量的时间信息限制在一个较粗的粒度上。从而使记录输入数据变的简单。当然心跳控制的作用绝不仅于此,我写过一篇游戏服务器中设置专门的 心跳控制服务器 的文章,可供一读。

在这类问题上,我也曾经跟公司别的项目组的同事有过争论。即使是录象,心跳控制也不是必须的。不过设置心跳控制模块,有它的内在哲学——隔离时间维度,让别的部分设计更简单。不光是服务器的设计,图形客户端也是如此。只是客户端需要更高的频率来满足人的手眼交互的舒适感。

btw, 我一直对 3d 游戏过分追求高帧速度是持反对意见的。我们只知道跑 100 帧的游戏就是比 50 帧时要更舒适(对于 fps 游戏,帧数的差别的确不是心理作用),但是却没有从根源上去找原因。或是不太在意其原因,只管把程序优化的可以跑的更快就够了。这个问题今天不展开写了。


胡乱写这么多,起因是我们前几天碰到一个跟时间有关的 bug 。因为涉及服务器组的多个进程和客户端,调试费了好大的劲。bug 的表现是几个数据包没有在预计的时间从客户端传到最终处理这个包的服务器进程,大约比预想的多了 0.2 秒左右。但一切数据逻辑都是正常的。

我们为找这个 bug 简直抓狂了。幸好我们的 client 可以跑在 X-Window 上,甚至可以用远程显示。结果我们让 server 组全部进程和 3d client 全部跑在一台 freebsd 的机器上,最终定位到了问题。


再写写第二个问题,关于对象的生命期管理。

还是缘起于上周项目里的一个 bug:我有一个注册到 timer 系统里的 closure 其实是跟连接有关的。Client 程序因为不需要考虑多个连接,我一直没在意做连接管理的工作。结果,在 Client 断线登出后,这个 closure 没有从 timer 系统中去掉,导致重新登陆后,系统中重复注册了这个时间事件处理函数。

Bug 定位到了以后,修正倒是简单。但这个问题让我想到,其实对象生命期管理其实一直都是个头痛的问题。虽然我们新版的 engine 从设计上已经帮后期开发开发人员解决了许多复杂度,但依然不够。我想说的是,还是不够 KISS 。可能我依旧忽略掉了什么,需要重新整理一下思路。

下面继续随便写写,想法还没成型,姑且看之。什么地方说的不对,多多包涵。

常见的对象生命期管理策略有两大类:手动管理和垃圾收集。它们各有优劣。单从性能上讲,无论从时间性能还是空间性能,都无法严格意义上的区分出那种更好。手动管理的策略更快,或是垃圾收集的策略会吃掉更多的内存都是一种偏见,我今天不想对次发表更多的评论。

从构建一个健壮的系统的角度上来看,无疑垃圾收集的策略更为有效。如果 C++ 的粉丝们蹦出来叫嚣正确的使用智能指针理论上可以获得跟 gc 系统同样的健壮性。这会让我想起另一个笑话:微软告诉我 Windows 系统其实提供了更细致复杂的安全机制和健全的 API ,它可以做的比 Unix 系统更安全。

相信程序员!多好的语言设计哲学啊。精确的控制每一行代码,每一个对象的构建和删除,我们总可以把事情做对,不是吗?

问题其实不在于我们没能力把每件事情做对,而在于在强大的工具(我指各种语言设施)的帮助下,我们在用正确的方法做错误的事情。

更多的时候,我们细致的控制每个对象的生命期,加大了系统结构的复杂度,直到复杂度蔓延到不在我们预期的犄角旮旯里。

请仔细考虑一下,为什么很多软件的退出速度这么慢?为什么 Windows 连系统关机这么简单的过程都容易出错(我的 Windows 系统经常不能正确的做完关机流程)?PC 不应该跟电视或是游戏机一样,不用了按一下电源就 OK 吗?

接下来说说 gc 的坏话。

当语言有了 gc 的支持后,并非高枕无忧。很多问题只是被隐藏起来了而已。

java 的应用软件那是出了名的吃内存,同样的功能比 C/C++ 实现的版本要多占用许多。前几年听 李维 讲过一节课,分析各个开源项目的代码。其间提了一段有关 TomCat 的某个恶评的版本。我不玩 java ,所以记不清当时提到的版本号了。

大体意思是说,其实代码实现和设计上都没啥逻辑错误,可是就是压力一上去系统就崩溃。最终发现问题跟内存管理有关系。大约是在一个内层循环用了字符串连接,导致了瞬间产生了大量的内存垃圾。

当然,这个可以算是 java 的错误使用了。换用 StringBuilder 就可以解决问题。不过我这次想谈的不是如何正确使用语言的问题。

再插一个例子,我们的 大话3 公测时,系统压力一上来,就老是因为内存耗尽而系统崩溃。整个服务器大部分都是 Lua 写的,这是一个支持 gc 的动态语言。按道理不会有 C/C++ 项目里的内存泄露问题。但是实际上,问题就是出在程序员不小心漏把一些数据解引用,导致垃圾数据堆积。

对于对象不能正确回收的问题, java 这样成熟的语言应该有许多好的工具去检查了。实际上,后来我们也为 Lua 做了类似的东西。这些工具和 C/C++ 项目中用的内存泄露和越界检查工具处于同等的地位。可我不想谈工具,工具只是用来解决已经存在的麻烦,却无法消除问题的根源。


如果说这两类对象生命期管理策略都存在问题的话,可是相比 C/C++ 或 java 这类命令式语言,函数式语言更容易处理好对象的生命期管理。函数式语言同样依赖垃圾收集,但可以避免 java 中的许多内存问题。

我不是想鼓吹函数式语言,本质上我还是习惯命令式语言的思维方式。它和我们目前用的计算机模型相同。如果继续采用命令式语言做软件,换个角度看,是否是我们在设计上有了缺陷?

或许我们对现实的问题模型映射到计算机的模型这个过程本身认识就远远不够。如何“正确”的表达要完成的事情的结构还是值得继续探讨的。所谓“正确”:简洁、层次分明。

问题出在哪里?

如果只考虑静态数据,其实无论是手工管理还是垃圾收集都无所谓,都可以做的很好。就好比打扫一张脏乱的桌面,手工管理其实就是有什么东西要用了放上面,不用了立刻收走;而垃圾收集则是在桌子堆满了后,把要用的挑出来,然后把剩下的东西全部扔掉。两个方式都能工作的很好。

可惜程序=数据+处理流程,数据并非静态存在,它往往跟处理它的流程绑定。这或许才是关键。


今天太晚了,没想到随手写来就写了四个小时,已经凌晨两点半了。明天再接着写。

点这里继续

Comments

client 的输入录象依靠心跳就可以做了。 所有输入都是暂存在队列按心跳做分发的。按类别记录起来,回放时按心跳模拟输入即可。 不用心跳也可以,只要保证次序就可以做到。这种方式我也实现过,没有问题。这时,时间可以单独看成一种输入。
关于录像带这个机制,记得云风好像曾经说过,有个统一的输入源头,然后再在这个源头记录,如游戏中的网络包。但这似乎是在服务器上描述的,对于客户端,除了网络数据的输入,还有其他数据的来源,如键盘、鼠标垫输入(而且部分输入还是不传给服务器的),要做到严格复现,这两部分的数据则需要严格意义上的时间同步,如某个包可能在某个按键后极短时间内到达。但这两个模块(按键、网络数据处理)显然是分开的而非耦合的,严格的同步又该如何执行呢?即使很精确的timer也办不到吧? 另外,这个数据量,究竟又有多大规模呢?
看云风这种用blog把自己思路也'录像'下来的文章真是受益颇多啊.
时间。。。我想起了数字电路中的“竞争与冒险” 从前写单片机,每个指令的周期数是固定的,所以一段代码的运行周期数是固定的,经常做的事情就是计算一段代码要运行多少个周期。。。
录象会不会影响性能
录像就是日志功能?那回放这个录像时候是自动的还是手动的? 另外有没有这方面的技术文章推荐一下?
看起来云风做的录像功能跟 StarCraft 的的 Replay 功能差不多的意思了。 把操作全部记录下来,下次重播就是了
每次都要记录,会不会影响到效率和反映速度???
录象就是把程序的所有输入按时间和次序记录下来。 以后可以让软件不从鼠标、键盘、网络、数据库等等跟环境有关的地方输入数据,而从事先的记录中模拟输入。 最终达到的效果是:整个软件重新运行不需要认为干涉,并保持跟前一次的运行流程完全一致。
录像,其实就是日志.不知道我这么理解对不对。 我们的每个软件,都有其日志,记录了它时时的运行状态和处理过程。我觉得非常不错的一个思维
我最近也在想与时间有关的交互协议问题。因为最近在作基于时间戳的组合事件检测的研究。要求能够检索出来自多个源的且与时间有关的事件组合。不知云老大有没有研究过这种组合事件的问题。
云风老大,你说的录像方法进行调试的那段能否说得详细一点?或者干脆另外专门写篇文章讲一下你的测试和调试方法?
来了许多次,今天终于更新了:)不知咋的,看不到你的博客我人就感到非常空虚
我觉得许多思想方法理论倾向于将一些概念、事物等分类划分清楚,为了所谓简洁或艺术美感,但这都只是受限于现实与大脑的,而事实上一切都是存于universal的,这种为了某种情形或层次的美丽必然会在其变迁中丧失。 所谓的“程序=数据+处理流程”,一直不喜欢,并不是否认,只是觉得数据与行为远远不是数据类型与表达式、操作符等就可以完好表现的。 行为操作着数据,其中有数据的诞生与消亡,也有行为的演变与消失。再加上一个时间,真是够复杂的。
读云风的文章每次都能thought-provoking:-) 感谢,一起胡思乱想~~
第一个:)

Post a comment

非这个主题相关的留言请到:留言本