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

本文谢绝转载 http://www.weibo.com/0x2b


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



    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(N^4)!就这样的东西,我真无法想象是如何还有勇气推销给别的团队的。明明就是个本地变量,栈分配就够了啊,非要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:关于术语、内容晦涩难懂的问题,我是知道有这个问题的。我想从技术层面去反省过去的种种失败,就不免会多谈些我认为的技术方面的失误,而不仅仅是像怨妇般地诉苦,那绝非我所愿。然而,正如我在前面说的,和技术人员沟通尚且困难,何况是外人?不是我不想写得更明白,而实在是我缺乏这个能力。如果指出具体的问题,我会适当修改,泛泛而论我就无所适从了。能看懂,固然欣喜,看不懂,也只好随他去吧。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 18
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值