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

本文谢绝转载

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


   一个周末过去,对于我所回复的,如何在运行时存储字符串的问题的解决方案,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,Traslation team,我真的不是在影射你们,你们不会告诉用户任何错误的,还会毁尸灭迹。

 

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值