页面

分类

Posts in category ‘其他’.

梦断代码--一个程序员的自白(五)

2014年4月25日星期五, by wingfire ; 分类: 其他; 0 comments

O同事给我的回复中,除了强调控制内存使用外,原来还指望通过扩展string的实现来支持数组。他真是太高估我的结果了。但是同时,他又提出了一些让我难以理解和接受的内容:不喜欢暴露新的接口,数据必须保持为char形态,模板不能跨边界分配和销毁。站在他预设的,string必须是char的立场上,对string提出了许多质疑,在我看来是颇为可笑的质疑:

  1. 是否意味着在加载数据时,如果有那么多string的话,是否会创建大批string对象?
  2. 如果有1000个只含义个字符的字符串,是否要创建1000个string?言下之意,string太浪费了。
  3. 我为什么将string的内容部分分离出来,而不是和string对象本身在同一个内存块上?
  4. 任何程序都将会有自己的string类型,并且会只想访问ADP的char* buffer。
  5. 坚持用户使用他们自己的string,这样string将会在我们的DLL外面。用户修改了就应该存储回ADP的runtime。

质疑1毫无意义。质疑2不符合实际情况,况且我们有许多优化的空间。质疑3属于毫无头绪,透过合适的allocator当然能做到3,但是这么做意义何在?好处是什么呢?质疑4则是妄作假设。质疑5则只能说是基本功不足了。说什么string在dll外面的这种话,我很怀疑他明白自己在说什么吗?

对于对象跨DLL边界,后来我发现不是O一个人的问题,许多人的理解都有问题。他们其实不明白为什么有时候一个对象不能跨边界,那些有问题的做法又是怎么导致问题的。甚至在许久以后,好像是在实现StringPointer的时候,M还指导过一位中国的W同事这个问题,然后我回信说他弄错了。他还和我争论了,并写个代码片段证明他是对的。其实跨边界行不行很容易判断,只要看边界两侧的代码对同一个对象的内存布局的假设(或者说约定,但是实际上没有约过)是否一致。如果是一致的,就能够跨,不一致,就跨不了。而对象方法的代码是否一致根本不重要,一个debug build的DLL有时候可以链接另一个release build的DLL就是例子。曾经容易出问题的一个地方是堆内存,原因就是边界两边使用的就不是同一个堆管理对象啊(其实只是些数据结构),但是现在堆在许多情况下已经不是问题了,比如VC就运行库。事实上,C++标准库早就跨边界传递对象--不是对象指针--好多年了。

O重做了string的支持。决定采用定长的buffer来存放string!32,64,...512等几种。我被彻底打败了。更烦人的是,O总是把他的runtime的设计和XML存储下来的效果放在一起讨论。你一个runtime的结构和最终存下来是什么样子有一毛钱关系吗?把runtime是什么样子的描述清楚就够了,我们看100遍存出来的XML也还是不能理解你的runtime啊。当时被折腾的最惨的是TD,要通过读示例XML,来给runtime写测试用例。TD读不懂来问我,我也读不懂啊!

多年以后,我也渐渐有点明白O的思路,到底错在了哪里。O当时一心想弄一个非常“Low level”的runtime,然后再加一层好用的wrapper,给用户用,底层负责解决高性能问题,wrapper负责解决易用性。貌似我刚开始写程序的时候也有过类似的想法。软件要分层是没错,但不是这么个分法。如果分层是个简单的活,那软件也太好做了。而且,这种分层法注定要很难逃过抽象惩罚的,因为不同层不但概念分层了,运行时也分层了。层次越多惩罚越重。写到这里,我忽然想,是不是很多的抽象惩罚其实都是这种无意义的自虐呢?不过我没有兴趣再去看这样的代码了。

但是老实说,O的邮件中的态度在某种意义上激怒了我。我们可以加班讨论他那些晦涩难明的设计,但是对于我们发出的设计、解释,O实际上并没有认真理解。仅仅因为我通过placement new去构造对象,他就能以一句“the code logic is not clear to me”否决我们几天的工作 --第一次知道,placement new的逻辑原来是好混乱的啊。

那个时候,ADP的其他同事也没闲着,我印象深刻的事情是研究Zip格式,要支持两件事,Zip64和inplace editing。这两件事其实是个信号,Zip64意味着ADP想处理大型数据,Inplace Editing则意味着ADP想提供程序运行时的高性能支持。

从ADP启动开始,过了只是半年,就败象初成了。我认为ADP的两大主要目标全告失败了。目标之一是可交换的文件格式,之二是公司范围的统一对象模型。对于目标一我写过一个长篇邮件,指出要做哪些事情,同时也是对O同事用memcpy来保存数据的否定,可惜美国那边根本不当一回事 -- 即使我当时的经理重发了一遍我的邮件以期望那边能重视也没用。按照我的快速失败的观点来看,ADP此时就可以停止并且反省了。要么改目标,要么纠正方向。而次要目标,运行时的高性能支持,已经注定是不可能做到的了。

虽然当时我已经对ADP失望,但并未绝望,认为有的是时间纠正错误。我因为之前引入了DBC,在一连串误会中,又扯出了错误报告机制,实际上就是个Log。我个人是不大喜欢Log的,也不认同很多人对Log的使用方式。我认为Log是用来记录工作流的检查点,而不是用来核对程序正确性的。程序正确性需要靠UT来保证。Log虽然也能起到错误诊断的作用,但是那只是副产品,就好比某人爱拍视频,但显然不是为了作为破案的证据的,虽然它确实有那作用。我写了个Log的设计给O,那个设计中提供了三个接口,Logger,Formatter和Device。分别用于过滤Log等级,格式化,指定输出设备。设计成接口的目的当然是为了可以定制和替换。另外,免不了的,还会有个总成的地方。并且,我也强调了,初始化这个日志系统的决定权应该交给最终用户。这次还不错,O觉得很好。然而正是这个东西,最后让我下决心远离ADP的主要工作,以免自己的声誉受损。

那时候G同事忽然插进来,说生产者/消费者可能更好,然后logger的实现就可以非常简单,只要把数据打包然后发到一个队列里去就行了。这样,生产者都不需要考虑同步,消费者处理负责同步就行了。他所谓的生产者是用户代码,消费者是将数据打包发送到一个内部队列,这个队列被一个工作线程维护,然后那个工作线程负责格式化并把数据写出到设备。好处是Log调用处不会被阻塞。这样,我们只需要一个logger的实现就够了。

当时正是多核狂热的时候,多线程和并行计算也被热切地讨论。G有这个想法很自然。而且,总的想法也不坏,可惜太不完善。首先让人难受的是术语。在讨论多线程的时候,我没见过把不需要同步的东西叫生产者/消费者的,只能理解为之前几天他被多线程的话题轰炸太多了。其次,说一个logger实现就够了,只是自大的妄想。今天回头再看当时的邮件,可明显地看出我和G在设计软件上的差异:我考虑的是Logger要做什么,应该让用户怎么用,那些地方用户会需要扩展,怎么扩展;G则是着眼Logger应该怎么实现,怎么可以一下全部完工,怎么用上先进的技术。我认为我们两者的一个根本差异在于,我相信程序库和产品是一个共存的关系,程序库要遵循Open-close原则,而G眼中的组件都是封闭的。

其实,G所谓的那些更好的选择,其实在我的设计里面已经都解决了。我的那个Device接口只有一个write函数,非常容易实现,也就意味着非常容易扩展,这是故意的。要同时输出到多个设备,只要实现一个伪Device类,转发给多个其他Device对象就完了,根本不是个事。至于输出阻塞不阻塞,是不是放到队列里去,是不是起一个线程,有必要在Logger设计中考虑吗?留给扩展就行了。其次,Logger还负有过滤日志等级的职责,第一时间过滤当然是代价最小的。因为Log一般还要采集当时的一些执行数据,这就意味着要格式化以后才能打包进队列,这是开销很大的事,如果日志很多的话,这么做性能会很成问题。即使将格式化后移到工作线程中去做,前面打包数据进队列也是开销很大的,这还限制了所能采集的数据类型,数据得能打包才行啊。所以显然的,日志等级过滤必须第一时间做,甚至还要优化--理论上可以优化到只有一次标志检查的开销。

本来这个Log是实现在一个DLL中的,G曾经要求将它放到叫core的静态库中去。O没有理会G的意见重新设计,但还是把代码提交到了Core中。结果,没多久就遇到了一个Bug,有两个DLL都link了Core,出现了两份Log管理器的实例。实际上我的原始设计中已经提供了解决方案,只要显式地初始化Log系统,就可以使两个Log系统使用相同的Logger,Formatter和Device对象。G对此很不满,认为我们如果按照他的意见实现就不会有这个问题。其实按他的要求实现只会问题更大,怎么启动的那个worker线程就会惹上大堆的麻烦,事实上也确实如此。不管怎么说,G后来还是给出了自己的设计,然后让另一个同事实现了。从此这个东西就开始一直折磨我们。一会儿性能出问题了,一会儿加载DLL锁死了,一会儿又程序退出锁死了。有产品要求我们不能默认启动,有产品又要求启动。还有产品一会儿要求自动启动,然后又要求不自动启动。程序崩溃时锁死,不能退出这个特性为我们招揽来了许多的崩溃报告。当然,修这样的Bug,还是可以彰显ADP的存在感的,多酷啊。从那件事以后,我就再也没有动力去思考ADP的方向了,我只冷眼旁观,只是多少有些不甘心。

接下来的日子里,我发现我就没做成过什么事,ADP也没什么事能让我看得上眼。有人写个Iterator(不是STL那种风格,first/next/valid风格),能在构造函数里把对应容器的元素指针全部复制到Iterator的一个容器成员变量中,所得的好处是遍历的时候是线程安全的,全然不顾复杂度从O(1)降到O(N).至于我当时被迫要求用Design By Policy去糅合一堆的Iterator就不要提了,那代码写完了就没人能改动,我也不行。不懂Memory Model,一样敢用Lock-Free。为容器中的每个元素创建锁--当然要用Lock-Free的技术,然后锁还要是按需创建的。用原子的Flag代替Mutex,用Yeild让出CPU代替Wait锁。真是恣意奔放,激情燃烧啊。只可惜烈火中没有重生的凤凰,只有灰烬。

我那时其实已经有数年的多线程项目经验,并行计算,Lock-Free什么的也是接触了好几年,至少也是从正儿八经的教材和文章开始学起的。CSP虽然到今天我也没啃下来,但好歹是知道点方向的。可是有什么用呢?G同学拿着Lock-Free的大锤满世界砸的时候,我只能重复Andrei Alexandrescu的话,告诫说:“General Programming很难,异常安全代码也很难,可是和多线程比起来,那俩不过是小娃吃奶”。我想,那时候我没告诉他们有种东西叫MPI,大概是我在ADP项目中作出的唯一正确的选择。渐渐地,当有别的team人问我ADP的问题的时候,我开始不愿意去解释那些垃圾的设计。因为当别人一脸谦逊地问我为什么要这么做,有什么好处时,我觉得无地自容。我实在是没法解释,只能为自己开脱,说:这代码不是我写的,那个不是我设计的。这是怎样的一种悲凉啊。

ADP还以一种病态的方式追求性能。比如坚持使用memcpy,坚持不用传统的Mutex,用TBB替换掉Boost.Thread等等,却在真正需要性能的地方挥霍。为了从ADP的runtime复制一个整数,首先要通过字符串名字查map来确定偏移量,然后再new两个串接起来的对象以shared_ptr返回。仅仅为了读一个整数,整个过程要做一个LgN的查询和3次内存分配!Runtime相关的测试中,内存分配释放一度耗费70%执行时间。就这样的结果还好意思提什么高效内存管理?G同学也不甘落后,实现了一个可局部排他锁定的图的实现,本意是想多个线程访问同一个图时,如果操作的部分不重叠,就可以避免锁等待。我自始至终就没看懂那个图的实现,但是我会测试啊。结果测试数据拟合下来的复杂度是O(N4)!就这样的东西,我真无法想象是如何还有勇气推销给别的团队的。明明就是个本地变量,栈分配就够了啊,非要new出来,觉得指针就是高效的?莫名其妙的做法不胜枚举,可以写本C++傻事儿大全了。

渐渐地,我发现自己离开了开发的核心位置。我开始做各种杂乱的事务,比如在Linux上用scons,用SWIG导出API(必须抱怨一下,SWIG不支持嵌套类,害死我了),去写Python/C#的sample,修Memory Leak,Performance Tuning,编译器升级。甚至还做了一个诡异的API调用序列的回放工具(做这个被dynamic_cast折磨死了,实在是太多了,而且不能查找替换,因为有些是不需要处理的)。要么就去做一些看上去有点难度的事情,比如写一个内存池--因为boost那个太慢了,heap优化。还有那让我抓狂的,取代boost的,shared_ptr/weak_ptr,只有一个额外要求:同时能支持intrusive的counter。shared_from_this是不满足这个条件的,它仍然需要一个分离的counter。谁有兴趣的话可以挑战一下这个任务。我反正是做不出来,实在没办法,只好不管怎样都会创建counter,但是review代码的美国同事无人发表任何意见。

然而也有高兴的事,08年春节前,我的宝宝出生了。那一年的上海下了很大的雪。然而,项目却越来越糟糕了。我不确定是否还有人有和我一样的感觉,或许,有些人感觉到了也不会承认吧。整个08年,我做的工作很少。我该是赶紧推ADP一把,好让它早点毁灭,还是无论如何,都要让它多撑些时日?和我的悲观相反,ADP此刻仍然卷入大量的资源。我的情绪却越来越坏,以至于我想把ADP干掉。我又想起我曾经做过的那个prototype,是不是重写一个ADP呢?即使我只有一个人?然而工作之余的时间几乎没有了,有时候写了几行,又找不出我这么做有什么意义,就又放弃了。

到了08年年底,有三位ADP的同事转去做Protein了,我还继续在ADP打杂。Protein主要部分也是一个程序库,用来一个处理3D材质包的。到了09年年初,ADP开始要集成到Protein中去了。这时,公司开始裁员。我等着被裁,拿一笔钱走人了,不想再在ADP无谓地耗下去。然而很奇怪,没有裁到我头上。难道是因为我还算物美价廉吗?不管怎么说,既然没有被裁,工作还要继续。

ps:关于术语、内容晦涩难懂的问题,我是知道有这个问题的。我想从技术层面去反省过去的种种失败,就不免会多谈些我认为的技术方面的失误,而不仅仅是像怨妇般地诉苦,那绝非我所愿。然而,正如我在前面说的,和技术人员沟通尚且困难,何况是外人?不是我不想写得更明白,而实在是我缺乏这个能力。如果指出具体的问题,我会适当修改,泛泛而论我就无所适从了。能看懂,固然欣喜,看不懂,也只好随他去吧。

梦断代码--一个程序员的自白 (四)

2014年4月25日星期五, by wingfire ; 分类: 其他; 0 comments

一个周末过去,对于我所回复的,如何在运行时存储字符串的问题的解决方案,O也有了回应。O对我的方案非常不满,认为我的方案是他之前就考虑过的,我完全没有解决他所担心的问题。应该说,这次合作不愉快,我也是有很大责任的,主要是在沟通方面。在之前的一周时间内,我除了问他问题,他回答,就没有真正双向的交流。他所担心的问题之一是内存碎片化,而我对此则根本没放在心上。他认为,对一个数据对象,存放在连续内存中是必须的,而在我看来完全不必要。而我则低估了他对这一点的看重。在我看来,因为这一层是紧靠IO层之上的。相对于IO的时间来说,内存操作的开销就不算什么了。另外,内存排布问题基本上可以通过一个定制的内存管理器,比如allocator之类的东西去控制。也就是说程序不直接关心内存从哪里来,反而给予allocator以机会去充分利用操作系统的各种机制去优化。从上层来说,使用方便也比一点点性能损失来的重要。何况运行时对象管理实现得越简单,从长远来看,反而有利于性能。悲剧的是,我错误地理解了对方心中主要问题。

我当时主要的关注点是三个方面的问题。第一,这个runtime不能依赖于一个物理的IO接口,必须将IO操作集合先抽象出来。我们既不能假设IO输出到文件,也不能假设其输出成XML。一个条件反射式的方案就是adaptor模式。更进一步,在adaptor中支持compose模式。那么IO的骨干就完工了。我们很容易扩展adaptor,为输入/输出到本地文件系统,zip文件,http,ftp写专门的adaptor,这是一个刚毕业的程序员也能够胜任的工作。运用compose模式,我们就很容易实现加密、签名,支持文本、XML、二进制格式数据。更重要的是,这样做的好处是,工作被充分分解了,每一项工作对人员的水平要求都降低了,还可以多人并行工作。容易测试,也容易保证质量。这么做还有一个理由就是ADP只是个程序库,最终用户(产品)有各种理由要求调整和扩展。因此,ADP根本就不能和具体的IO adaptor绑死,而是必须提供多个小的组件供用户选择并装配后使用。当然,ADP可以提供一些预置的装配,方便使用,但最终 决定权必须交给产品。也就是说,可扩展性,可配置性(可装配性),简单性三者必须都要照顾到。后来的事实也证明,这种预见是必须的。否则,就是噩梦般的折磨。

当然,这么做IO adaptor的设计风险也增加了。但是这些风险实际都是微不足道的。对于C++来说,可能的抽象惩罚也是几乎不存在--事实上,只要在设计和实现中避免冗余的数据复制就差不多了。毕竟,我们只是在和一个低速的IO系统打交道。当然,如果要较真,adaptor模式显然不适合大规模、重度的IO场景,比如,同时处理成千上万的文件、网络连接。如果是那样,就需要慎重对待了。显然ADP不是这样的项目。

我关心的第二个方面是用户代码如何去访问这样一个内存中的数据?O给出的接口是非常原始和不安全的。例如,为了读一个int数据,用户需要准备一个int变量,然后调用接口,把数据复制回用户的int变量中。这种内存复制的操作是非常低效和不安全的,因为用户还要告诉接口int变量的字节数。接口实际只相当于memcpy,只是知道从哪里开始复制给用户而已。所以,我认为我们需要给用户一个高效、安全的数据访问接口。这不难做到。如果要访问一个int,那API就返回那个数据的int引用给用户即可。至于string,也是如此。当然,这就要求ADP的对象管理内部真的放了一个string对象才可以这么做。这也就是我为什么的解决方案中用了个string类型来放字符串的原因。

第三个问题是真正的核心问题。至少我是这样认为的。O设计的数据类型,除了C++中的基础类型(Fundamental type)外,就只有个未实现的字符串类型。还有其他几个长度固定数据类型,比如二维向量Vecotor2,2×2矩阵e2X2Matrix之类。这就导致一个严重的问题,数据类型不是可扩展的,那个runtime的实现也支持不了内容变长的数据类型,如数组。这也是为什么O同事认为字符串非常困难的一个原因。不能支持数组也是个说不过去的事情,而支持数组本质上说就比支持字符串困难。数组的困难在于,若要真正支持,就必须允许元素的类型是可变的。这就意味着数组本身不是一种具体类型,指定了元素类型的数组才是个、具体类型。这也意味着需要有一种简单的类型复合机制。另一方面,用户实际使用的数据类型是很多的,比如Point,Vector。仅仅是一个点,就存在Point2F,Point2D,Point3F,Point3D,甚至是Point4D。这多出来的一D是时间。同样叫Light,不同产品、同一产品的不同场合中各个Light数据成员可以完全不同。如果ADP不能够提供一种机制来定义和产品中一致的数据类型,那么产品就只好使用基础数据类型来生搬硬套。这将在事实上导致产品不可能直接用ADP的runtime数据来建模,而只是将ADP当作一个数据序列化的目的地或来源,把数据再次翻译到他们已有的数据模型中去。这个推理结果是必然的,事实上也正是如此。不可能有任何理智的程序员会将数据模型直接架构在ADP的runtime上。

这样,ADP的一个主要目标,统一公司的数据模型对象的企图就必然要破产了。然而,也并不是说ADP支持了自定义类型和类型组合就能成功。这个机制的效率,空间效率和时间效率都必须非常高,要和C++相当,至少差别不大。此外,还必须有非常健壮、易用的API,才有可能让产品接受。要知道产品都是给公司挣钱的,都非常强势。幸运的是,这几个要求都是能够做到的。不幸的是,ADP一条也没做到。

关于这个类型的扩展、组合机制,我后来又在另外两个程序库项目中见到,都是采取蛮干的办法,例如通过硬编码支持int,double,float,和string的数组。既不能允许用户自己扩展,也不允许组合新类型。唯一的办法就是修改库代码来支持新类型。因为修改程序库不方便,所以,产品么,只好将就吧。

对于这三个问题的认识并非事后诸葛亮,而且,我也估计O同时并没有想那么多。所以,虽然O对我的方案很有意见,但是我没特别放在心上,而是认为事实会让他清醒的。后来的事实表明,我再次幼稚了。

因为之前在DWF项目上时就profile过其中string类型的效率,指出了其中一些实现上的严重问题,也包括一些接口设计的问题。结果关于性能的结果得到了重视,而接口上的毛病则毫无音信,这颇让我怀疑美国同事在这方面的水准和品位--Linus 所谓的那种品位。因为DWF string的性能有问题,所以,我们应该避免这些问题,然后实作一个更高质量的string了吧?我还写了封信给G,说我打算实现一个更适合ADP的string,因为那也算是补足O的方案中未完成的部分。G的答复让我先是极为诧异,然后就凌乱了。G同事决定,我们不能使用string数据类型,因为string类型性能低下,所以,我们使用char,哦,因为要支持本地化,所以,我们要统一使用wchar_t!我理解不了这理由和决定之间的逻辑是什么。WTF,这是一种什么样的精神?至于我原来打算怎么设计那个string,G自然也是毫无兴趣过问了。

这件事让我对自己也起了很大的怀疑,因为这样的事情不是第一次了。为什么我屡次想做得更好,却总是得到了一个更糟糕的结果?我究竟错在哪里?怎么做才是对的?是我根本就不适合在一个从属地位上做团队合作吗?短时间内一系列出人意料的结果,从UT,DBC,到这个string,第一次让我怀疑起自己和别人的沟通能力。我已经把对自己的要求降低到不指望和外行人交流了,可是现实却告诉我,我和内行人交流一样失败。我不知道为什么会这样,我开始对自己的能力感到强烈的不自信和怀疑。

这个string的故事还没完。wchar_t*显然是很难使用的,可ADP还是就这样坚持一两年。最后实在受不了性能问题--实际上是必然的一个后果,过度扫描和复制字符串--要用引用计数。好像是美国的M同事,写了个叫StringPointer的东西。看名字就能知道,根本就不会有什么像样的API,事实上也确实如此。StringPointer一直用到ADP快结束时,为了将ADP集成到另一个叫Protein的项目中去,又弄出一大堆string类型来。这一次出来的东西更加让人受不了了,然而寿命却注定了出奇的长。留待后继再吐槽吧。

继续第三个问题。类型复合和自定义用户类型是可能的,因为我们项目所使用的语言,C++就是一个现成的例子,JSON则是另一个例子。C++中可以定义任意类型的数组,这一点在std::vector表现的更清楚,通过变更模板参数就可以做到,当然如何在运行时做到还需要一点小技巧。而C的struct就是自定义数据类型的典范。如果ADP能够做成这两件事,那么就能够具有和C一样的用类型建模的能力--当然,离抽象数据类型(ADT)还差点儿。

在那两三个月之后吧,我花了一个周末的时间做了个原型,尝试了一下想法是否可行。对于基础数据类型,我们可以知道,也只需关心其对象大小,对齐属性。我把要自定义的复合数据类型用一个表格来描述,就像struct所作的那样。这样,我就可以算出每个成员的大小和在对象中的偏移量。最后,得到的复合数据类型也具有一定的大小和对齐要求。这就和基础属性所需关心的特性一样了,换句话说,可以和基础类型一样被处理了。复杂的基础类型,如string,还需要关心构造和析构,这是为了和C++对象交互所必须的,其他某些语言则可以不考虑这个问题。而复合类型的构造和析构也很容易自动生成出来:按照恰当的顺序遍历成员进行析构和构造就行了。

对于数组的支持稍稍复杂一点。我需要实现一个特别的vector类,元素类型不是模板参数,而是在构造时告诉vector的一个描述数据,即Type。vector内部就医根据Type提供的size,alignment等信息,计算出给定元素的偏移量,也可以用Type给出的构造析构方法初始化元素。

这样定制出来的新数据类型可以和C++的struct描述的结果内存布局完全一致(当然你不能在C++中放任何virtual的东西),内存布局一致,就意味着用户可将之cast成他们喜欢的数据对象并直接操作。当然,直接cast是危险的,也是高效的。我们还可以提供一套带检查的cast机制,作为常规的使用方式。

但是我当时在做这个的时候也还是犯了点小错误。出于简单起见,那个原型中作为输入的复合数据类型的描述表是一个数据结构,而不是文本,这样可以避免写一个解析器。但是,那个数据结构中的描述可能是有错误和矛盾的啊。检测错误是容易的,可是如何报告错误,并且尽可能精确定位错误呢?我不想,也不应该在将来解析一个有错误的文本时,扔出一个失败的结果,却不告诉用户错在哪儿了。用户不是小网站站长,我也不能做真理部不是?哦,sorry,Translation team,我真的不是在影射你们,你们不会告诉用户任何错误的,还会毁尸灭迹。

我搞混了那个输入数据结构和文本的本质区别。以至于我后来为了实现一个即是异常安全的,又能精确报告所发生错误的解析器花了不少时间。要么难看之极,要么没有达成所有目标。最后才意识到自己理解上的错误,这大概就是一个人写代码所必须付出的代价吧,没人会指出你再明显不过的愚蠢之处。以后会有对这一蠢行更具体的记叙。当然,最后还是完满解决了。那是一个正面的教训:绝不轻言放弃,绝不轻易妥协。

梦断代码--一个程序员的自白 (三)

2014年4月25日星期五, by wingfire ; 分类: 其他; 0 comments

"程序员是工程师吗"?这是我在大约10年前初次接触敏捷方法时,让我有醍醐灌顶之感的一个问题。那个时候,称呼程序员为码农,IT民工,吃青春饭的,周围充斥着三十岁后能不能做技术的质疑。这个问题让我真正想明白自己从事的是怎样的一个职业。已经忘了最初在哪里看到这篇文章的,很幸运,我今天在Robert Martin的那本《敏捷软件开发》的附录D《源代码就是设计》中又找到了。它回答回答了两个问题,一,程序员是工程师,二,源代码就是设计。工程师的本质在于产生文档,代码就是程序员的文档。工程师只需要产生一次文档,然后其他人在此文档上反复工作,比如技术员、生产线上的工人。甚至,程序员的某些工作应该被看作是科研工作,因为“分类是科研的最初级形式”。我想,这一点未来在开源社区会变得更为明显的,一个优秀程序员做出来的突破性的工作,会被全社会共享。我曾经和一位朋友说,如果说工业革命依靠的是工人,那信息革命依靠的则是程序员。程序员不该看轻自己。

我是以工程师的心态看待自己的工作的,也是以文档的心态看待自己的代码的。说到文档,我必须对那些不愿付出努力,就想理解系统的人表示鄙视。那种微软让扫地大妈来测试可用性的故事就是个骗局,太多程序员不还是用不好Word?那些认为自己可以让一个“untrained eye”也能轻易理解自己工作的人,不是狂人,妄人,就是所作的事情不值一提。和其他行业一样,程序员的文档只对专业人士负责。所谓简单设计,简洁代码,也只能是对合格程序员说的,而不是让一个未入门的菜鸟觉得简单。菜鸟的唯一出路就是变成专业人士。我对自己写的代码的态度也是如此,只有代码表达不出来的东西,比如系统的高层设计,如问题分析,动机,原理这些无法在代码中体现的东西才需要文档。最多再有个别技术难点需要点解释文档。此外的部分,所有文档都在代码内,用用doxygen这一类工具抽取出来方便阅读就够了。要求给vec.erase(unique(vec.begin(),vec.end()), vec.end())这样的C++语句写注释的人,一概鄙视之。

当时的ADP也是很多琐碎的工作。QA的团队刚刚设立起来,如何做测试其实并没有什么经验。虽然我不认同测试、开发分离的组织模式,但这不是我能改变的。我对软件质量的态度也许偏执,但我坚持认为:逻辑,绝不会因为你写一个要求不高的程序就心慈手软,网开一面。在我看来,软件缺陷就是你的债务,而高质量、高可靠就是软件的不动产。当软件投向市场的那一刻起,你就必然,也必须不停地偿还债务和兑现资产。那种认为要求软件高质量就意味着的更多资源,更多投入的观点根本就是错误的。那种拿航空器、生命支持系统软件来说明高质量软件代价高昂的人,根本就是在说外行人的胡话。高质量软件开发过程非但不会增加投入,反而提高开发效率。只有高质量的代码,你才能,才敢于写完后忘记。只有忘记实现的细节,程序员才能将自己的智力解放出来,真正投入到分析问题,设计问题解决方案上去。那些总是纠缠与Debug,修Bug,各种Special Case的程序员,我无法想象怎么才能有精力去分析问题?怎么才能有勇气说,写下的所有不完善的代码,全都可以随时丢掉重新来过?在公司,我往往是作为一名C++专家,或者是能解决棘手问题的人被大家知道。其实这对我来说,不是什么褒奖而是诋毁。因为这无异于说我是“Language Lawyer”,而语言律师向来是我最为鄙视的。同样,对于Debug也是我深恶痛绝的,甚至我刻意地不去学习debug的技巧,事实上我也没有这样的技巧。我充分理解当年Linus为什么反对kgdb,而我也持有类似的观点。 。Debug必须被视为可耻的,是坠入地狱前最后的救赎,但却是不可靠的。所以,那些请我帮忙调试程序的同学们,每次你们都该请我吃饭以抚慰受伤的心 :P。

言归正传。

当时公司迅速扩张,我的另一件事情是面试,每周都有好几个。直到那一年年末,我大概面了近百人,虽然不是面试最多的人,但也名列前茅了。很多人不喜欢干这事,但是对我来说非但可以放松一下,还可以从来面试的人那里学习到许多有意思的东西。我是抱着找比自己优秀的人的心态去面试的,也是代表公司的,所以反复提醒自己,必须给予面试者足够的尊重。我后来听说有来面试的人因为对面试官很恼火,回去后在网上骂人的。然而,我面过的人当中居然有两三个发邮件来感谢我,有一个还是通过别的部门同事辗转联系到我的。这让我觉得非常意外,但是也很开心的事。我好像当时还做着给刚毕业的同事做些培训,可惜老板的要求太急功近利了,实际没什么效果。

在我的极力主张下,把单元测试搞起来了,用的是boost.test作为测试框架。QA最初也使用boost.test。但是很不幸,首先是美国同事在UT中输出性能测试数据,后来又更过分地输出许多的log信息,这让我极为崩溃。即使多年以后,我仍不免要说:他妈的!中国的同事年轻,没有经验,算是情有可原的话,那么美国资深的,经验丰富的同事们呢?居然也连UT都不知道怎么做!难道还要我来给他们讲如何写UT?更何况这事儿是他们开的头!我给中国的同事解释为什么不能这么写UT,他们还是能虚心接受的,可是美国同事根本不理这茬。相反,后来代码出了Bug,或是Build break了,美国同事指责我们说为啥不写UT,或者是为啥不检查UT的结果。我勒个去!不客气地说,绝大多数开发和QA,对什么是软件测试,有那些类型,分别是干什么的和怎么干完全是一脑袋浆糊(包括但不限于ADP)。这是我第一次觉得自己给自己上了个套。除此外,还有另一个更让我抓狂和沮丧的事情就是引入DBC(design by contract)。

在没有正确实践过DBC之前,我不确定程序员是否能理解DBC的美妙。当我在过去的项目中同时运用DBC和异常安全时,我得到了如同编程初学者所写的那种简单直白的代码。所不同的是,我不露痕迹地完成了错误处理。整个代码几乎完全由Happy Path构成,逻辑主干突出,分支语句比例大幅度下降(if语句比例可下降2/3。一个极端的例子是1400多行代码缩减到不足200行)。那些代码看上去简单质朴,但实际上都是深思熟虑的。反璞归真,我认为我的代码是有能力达到这个境界的。对许多程序员,甚至是资深程序员来说,如何设计软件是两眼一抹黑的。“设计之道,不在增无可增,而在减无可减”,不理解这话的人都还在纠结加什么功能特性上,要处理那些错误和特例呢。然而DBC虽好,还必须让运用的程序员能立刻从中收益,才能被愉快地接受。

我为ADP写了一个contract的库,提供了PRE_CONDITION,POST_CONDITION,和INVARIANT三个宏,而且说明,只有一个PRE_CONDITION是必须的,提供三个只是为了概念的完整。可以等同于assert来使用,搞不懂的时候,就只用PRE_CONDITION,用错了也出不了大事儿。唯一要牢记的是PRE_CONDITION(expr)中的expr不能有副作用,这对于理解assert的人来说,应该完全没有记忆负担。本来,我还打算继续在此基础上,讲述如何设计API,如何划分API职责,要注意些什么。最好,能推行异常安全代码。我把Contract库,一些资料的链接,以及解释写了封长邮件给美国的架构师G同事。他先是回了封邮件说很好,说质量很重要。但是,过了个周末,他又回了一封信,这是让我彻底抓狂的信。

在我看来,一个技术,如果带给程序员很大的心智负担,那也不能算好。按照Brooks,并且我也认同的观点,软件开发自身的困难未来都是可以解决的,而要解决的问题域所含的困难是本质性的,永恒的。一切软件技术的进步都要以解放程序员的智力、精力,使之可以更多、更好地投入到分析问题中去。这就是为什么我只推荐一个PRE_CONDITION的原因,也是我不喜欢繁琐而严格的命名规范的原因。G同事的回信中,他把contract库重写了,然后提供了大约几十个宏。这些宏和assert完全不同。有类似ADP_CONTRACT_PRECONDITION_RETURN_VOID, ADP_CONTRACT_SIDE_EFFECT, ADP_CONTRACT_POSTCONDITION_EX_MSG_ENFORCE这样的名字超长,不知所云的东西。特别是XXX_SIDE_EFFECT,XXX_RETURN这种的,摆明了是逻辑混乱的产物啊,怎么能这么干呢?SIDE_EFFECT,哼哼,成功了给个副作用吗,还是失败了给个副作用?然后信里面还给了两个DBC的链接,一个是Wiki的,还给我解释了一下什么才是DBC。我实在无语的很。那时,我已经接触和使用DBC好几年了,相关的文章也读了不少,他一个周末就可以在这方面指导我,然后弄出这么个奇葩的东西而不自知。这种自信是怎么来的呢?不管怎么说,G同事的回信就是决定。他写的那些宏,直到ADP结束我也没记住。我唯一死心的是,我不需要说怎么写异常安全代码了,甚至都不需要去提。事实上,ADP早期还用着异常规范呢,这可是2007年了。我后来还是忍不住,从《Exceptional C++》里面扣出几条,说明别用那悲催玩意儿了,这才把异常规范去掉。TMD,又做了一次语言律师!

--回头看一下这篇文章,成了炫耀贴了。炫就炫吧,对上这些人,我还真有炫耀的资本呢。

梦断代码--一个程序员的自白 (二)

2014年3月13日星期四, by wingfire ; 分类: 其他; 0 comments

AIRMax是一个庞大的项目计划,需要3~5年的时间来完成。这个缩写是取公司的主打产品首字母和特征单词组成的,也意味着这个项目要影响到所有这些支柱产品。公司的这些产品都是不同领域的设计>软件。这个项目计划在三个方面做资源整合,即为所有产品提供统一的程序库或者框架:程序外观(GUI),渲染(主要是3D引擎),和文件格式(保存设计成果)。这些产品都是公司的现金牛,都很强势。这 就意味着AIRMax是一个极富挑战性的项目,而且需要高超的组织协调艺术才能成功。

AIRData项目就是这三架马车之一,目标是打算统一文件格式和相关程序库。项目名称中的“Data”表明了项目的性质。后来又简称为ADP,是取Axxx Data Package的意思。但这个简称实际上要到一年后才被普遍使用。ADP项目美国那边有一位架构师总负责,和另外三位程序员。我们这边最初好像也是四个,有一个同事来了没几天就因为没兴趣而去了别的项目组。

查看更多...

梦断代码--一个程序员的自白 (一)

2014年3月13日星期四, by wingfire ; 分类: 其他; 0 comments

--当一个有价值的人或事物逝去,缅怀他的人便为之立碑,写下墓志铭。

谨以此文献给Protein。

看过一本同样叫做《梦断代码》的书,英文名叫《dream in code》 我总觉得翻译得不准确。 在那本书中,讲述了一个软件项目Chandler失败的前后经历,让人读来扼腕叹息。 然而今天,这四个字不断地在我的脑海里盘旋,仿佛是为我这六年来的经历下一结语。

在AIRMax启动时,我来到公司尚不足一年。经理让我参加一个叫AirData(后来也叫ADP)的项目,我对此也是跃跃欲试。然而一开始什么都没有准备好,除了让我们看一些资料外,还让我们看了一个叫DWF的项目代码。没想到,这一看就让我工作在上面好几个月。

因为当时已经知道ADP中将有一个很重要的部分就是管理property,因此,我也就重点看了DWF的property部分。当时,美国那边提出ADP的性能和内存占用是很重要的,还特别提到要考虑如何缓存处理property。从我知道的信息来看,已经知道ADP是读写一个本地的文件包,并管理一系列property map的对象。对于这样的系统,数据规模方面的信息也是知道的,我觉得性能低下是难以想象的问题,或者说是不理解。不仅是我不理解,我认为美国同事也没想明白问题出在那里。而对一个不理解的问题,设定了一个解决方案则是更不可思议的。在我看来,这根本就是软件设计中的大忌。可是就是这样的错误,在后来的日子里一犯再犯。

查看更多...

下一页

下一页