不亦快斋 :: 其他 http://icerote.net/blog/ 程序人生 How to Read an Academic Article http://icerote.net/blog/post/106 http://icerote.net/blog/post/106 <div style="direction:ltr"> <p>Author: | Peter Klein |</p> <p>This fall I’m teaching “Economics of Institutions and Organizations” to first-year graduate students. The reading list is rather heavy, compared to what most students are used to from their undergraduate courses and their first-year courses in microeconomics, econometrics, etc. I explain that they need to become not only avid readers, but also efficient readers, able to extract the maximum information from an academic article with the least effort. They need to learn, in other words, the art of the skim.</p> <p>When I’ve explained this in the past, students have responded that they don’t know how to skim. So a couple years back I put together a little handout, “How to Read an Academic Article,” with a few tips and tricks. I emphasize that I don’t mean to be patronizing, and that they should ignore the handout if its contents seem painfully obvious. But students have told me they really appreciate having this information. So, I reproduce the handout below. Any comments and suggestions for improvement?</p> <center>How to Read an Academic Article</center> <ol> <li><p>Caveat: no single style works for everyone!</p></li> <li><p>Klein’s basic steps for skimming, scanning, processing…</p> <ol> <li>Read the abstract (if provided)</li> <li>Read the introduction.</li> <li>Read the conclusion.</li> <li><p>Skim the middle, looking at section titles, tables, figures, etc.—try to get a feel for the style and flow of the article.</p> <ol> <li>Is it methodological, conceptual, theoretical (verbal or mathematical), empirical, or something else?</li> <li>Is it primarily a survey, a novel theoretical contribution, an empirical application of an existing theory or technique, a critique, or something else?</li> </ol> </li> <li><p>Go back and read the whole thing quickly, skipping equations, most figures and tables.</p></li> <li><p>Go back and read the whole thing carefully, focusing on the sections or areas that seem most important.</p></li> </ol> </li> <li><p>Once you’ve grasped the basic argument the author is trying to make, critique it!</p> <ol> <li>Ask if the argument makes sense. Is it internally consistent? Well supported by argument or evidence? (This skill takes some experience to develop!)</li> <li>Compare the article to others you’ve read on the same or a closely related subject. (If this is the first paper you’ve read in a particular subject area, find some more and skim them. Introductions and conclusions are key.) Compare and contrast. Are the arguments consistent, contradictory, orthogonal?</li> <li>Use Google Scholar, the Social Sciences Citation Index, publisher web pages, and other resources to find articles that cite the article you’re reading. See what they say about it. See if it’s mentioned on blogs, groups, etc.</li> <li>Check out a reference work, e.g. a survey article from the Journal of Economic Literature, a Handbook or Encyclopedia article, or a similar source, to see how this article fits in the broader context of its subject area.</li> </ol> </li> </ol> </div> 梦断代码--一个程序员的自白(九 完) http://icerote.net/blog/post/72 http://icerote.net/blog/post/72 <div style="direction:ltr"> <p>Protein的性能瓶颈主要是在runtime,而runtime是基于FBX来写的。FBX已经停止开发,而且改动代价太高,因此,美国那边打算把把FBX从runtime中去掉。这是一个大工程。当时Protein的新经理M也刚来不久,我们都有点担心资源不够。这时,美国的架构师也忙于新的项目,也没时间给出Protein的新设计,中国这边就只好先做起来。在同事W的一再劝说和鼓动下,我决定努力一下,用息壤来替换FBX。下决心的根本原因是当时就觉得息壤说不定有机会替换ADP,那才一直是我最想做的事情。</p> <p>从11月份开始,我就在息壤中做准备工作。为息壤写document comments,写设计文档,sample文档,写一些Protein--版本4.0--的高层设计。这个新设计有一个让人非常痛苦的约束,就是文件格式和API不能做任何改变。后来在我们的争取之下,API放松一点点,只要不要求修改用户代码就行--实际上,我们后来还是顺便纠正了一点点细小但很恶心的API问题,比如列表的元素个数至少是1这样诡异的设计。我把这种限制下的设计叫做戴着镣铐跳舞。写文档和设计花费了大量的时间,后来还是W全部整理了之后给美国的架构师S看(当时美国那边只剩S一个人了)。不可能设计全部做完了才开始编码,美国那边也需要一个严肃的原型,以便可以测量出可能的性能改进,进而评估项目是不是要做。因为当时转向到云计算的氛围已经很浓了,要不要继续做Protein已经打上了问号。从11月中下旬开始,我和另外两个同事开始为Protein写基本的runtime的支持部分。差不多到元旦,运行时管理的部分就已经写完了。这时TD做了几个美国那边最关心的case的性能测试,最好的情况快了两个数量级。除了一个预期不会有改进的部分(实际上也是改进巨大,只是在当时的case里无法反映),其他的case也基本上快了一个数量级,这让美国那边非常兴奋。虽然当时公司要把大量的资源抽去做Cloud相关的项目,但是因为这个极好的性能报告,Protein 4.0还是立项了。这是我们第一次争取到一个本来很可能取消的项目,也是第一次,在我们这个部门内,由中国这边全权来做全部设计的项目。这对我们当然是个巨大的鼓舞。现在想来,之所以能拿到主动权,是因为我们做的早,美国那边还忙的不可开交的时候,我们已经为第二年的事情开好了头。如果没有息壤,也很难在这么短的时间里做出可以提供这么多性能测试的结果。</p> <p>接下来的就是写文件包管理的部分,到春节前就基本上完工了。考虑到我们并非整个团队全部投入到4.0上,新的4.0能工作,并全部跑通,只花了整个团队1个半月的工作时间。这是我在公司所有项目中从未有过的速度。高峰时期,我们平均每个人一天可以提交四五次代码。虽然我们仍然会骂Protein API的设计很烂,会批评以前的实现代码为什么那么糟糕,但是我们还是尽力在允许的范围内做大最好。接下来的任务是通过回归测试,这花费了我们相当多的时间和资源。这是以前的设计中各种特例的恶果,还有就是针对实现和现状测试,而不是针对预期和设计来测试带来大量的虚假错误。为了通过那几百个回归测试,我们还是付出了相当多的努力的。到回归测试差不多都通过时,我发现因为前期为了能够表现给美国那边看,和M一味地赶进度,代码的质量很让人不满意。在兼容以前各种奇怪的逻辑和行为的时候,我们已经不得不引入了许多混乱和复杂性,如果我们不能把这些混乱和复杂性控制住的话,就会伤害到软件的质量和阻碍我们进一步的改进。代码改进越困难,我们就越容易丧失对代码质量的警惕性。我批评的第一个目标是单元测试。</p> <p>在xirang中,我写了比较完整的UT,我想这足以作为如何写UT的范本了。但是,同事们完全没有正确地去写UT,即对每一个API,都要根据给定的输入,检测期望的输出。仍然按照以前的路子,以写功能测试和集成测试的方法写UT,既不在意测试用例的独立性和隔离性,也不在乎是否做了IO和执行时间。还是使用了我一再强调并反对的一杆子到底的测试方法,那会遇到执行路径的组合爆炸问题。测试数据也不是那种精心制作的,而是随便抓来的掺杂大量噪声的数据。当时的UT时间已经100多秒,而且还有快速上升的势头。我当时和M提出来,质量有问题,必须停一下,补上质量的欠债,否则后面的工作可能会遇到很多麻烦。但是M强烈反对,他打算投入更多资源去完善回归测试。这在我看来是及其低效和不可行的。当时还有一个开发X,也支持M,这让我很意外,也很难理解--我们过去的质量声誉并非很好,也为此饱受折磨。我们在之前的Protein开发中,单元测试最快的也要40分钟。我不能理解,为什么程序员能忍受?为什么不把这个看作是一种致命的错误?我是每次必抱怨的,从不掩饰。</p> <p>4.0的UT执行时间必须被降下来,而且要降到1秒之内,这是可以做到的,也是我们必将做到的。我提出停止其他任务的要求其实是在漫天要价,也得到其余同事的全力支持,但我想得到的不过是制定质量改进的计划和任务,并且,至关重要的一点是,制定任务完成的验收标准。最后,妥协的结果是削减了一点其他任务,增加了质量改进的任务。但是关键的,为质量改进任务设定验收标准,M死活不肯。结果是一些人确实改UT了,但是换汤不换药。我又不得不自在完成自己的任务之余,再去改进那些有问题的部分。这个过程中因为触及了很多X写的部分代码,他可能因此对我很有意见。但是,我从把xirang带进Protein的时候开始,就多次说过,我们是集体代码所有权,xirang不再是我个人的代码,是所有人的,任何人都可以改动代码,无需先告知我。在改进过程中,我重写了X实现的AdpFs,和相关的archive。X的实现还是用ADP的api来操作的,不但慢,还有其他可靠性的问题,我直接用xirang里已经有的ZipFs来实现了。最后,我们终于把UT的执行时间降到了100多毫秒。</p> <p>除了AdpFs,我还花了两周,把ADP中的DataStore类重新实现了。这时,除了残存在接口上的一些微不足道的类型(主要是String)外,ADP已经被彻底地从Protein中驱逐出去了,连带的,一个叫AdpPlugin的project也被我们彻底删除掉了。ADP花了三年时间,20多万行代码(不含TD的代码),数十个人年;xirang 2万3千行代码,数个人月,交付的功能比ADP还多。</p> <p>UT的改进结果其实我是很不满意的,以改进UT为契机,反向推动4.0的设计和实现改进的目的也落空了。因为受不了恼人的复杂性和频繁的错误,以至于我们在后一个月又做了两个大范围的代码改进。效果不错,但是离我的期望还差距很远。其实我改进质量的终极目标是为Protein的下一版腾出空间。Protein的局限和危机是客观存在的,如果我们只是完成了预期的目标,那么来年被抛弃的命运是可以预知的。但是,我们又受到了很大的限制,不能放手施为。因此,在我的计划里,就是我们通过远远超前的进度,留下足够的时间,然后在今年多做一个版本。我设想的新版本是不改变文件结构,但是提供全新的模型和API。按照往年发布周期的惯例,我们有充足的时间来完成新版本。当今年正式集成时,我们将发布包含两套独立的API的Protein。我们不用刻意去宣传新版API,只需引导用户去选择即可,让用户的选择来为我们投票。一旦新API发布,我们就可以很容易做一个特别适合云存储的版本来。这样,就算抱上了公司云计算的大腿了。我们就有极大的机会和资本将xirang的技术推介出去。</p> <p>然而经理M并没有这样的野心。虽然他也预感到Protein将来的地位岌岌可危,却缺乏为长远谋划的行动,而是安排了大量的资源和任务去做产品的预集成。这让我和一些同事颇感失望。ADP被彻底删除只是我个人的胜利,不是Protein的。Protein必须在API和存储格式两个方面取得根本性的变革,并拥抱Cloud才能生存。可是,因为我们的弱势地位,导致我们没法先先向公司抛出设想,然后争取立项和资源,再进行开发。我们成功的唯一途径是压榨自己的产能,先期做出成果,展示给美国那边看,然后才能获得完善的机会。一个可能的机会,正是我们前期高速运转的动力所在。本来,到6月份,我们就完成了所有的工作,只需一两个人处理修Bug,合并和集成的事情就足够。可惜,各种在我看来没什么价值的任务夺去了我们所有的时间。我认为M应该为此负责,并且他也为自己挖了一个坑。</p> <p>接下来的时间,我们去做各种组件和产品的预集成--除非产品发布,否则这是个无止境的工作。给外界的映像我不得而知,但是我猜多半是认为Protein的今年的开发全部结束了,所余不过是修修零碎的Bug而已。在我和W的努力下,到7月底才开始讨论新API原型的事,然而一切都太晚了。到了8月,我,W和测试K都被调到新成立的A360项目,这其实意味着Protein已经没有机会了。但是更大的打击接踵而至,8月24号,公司裁员,M,X和TD的lead都被裁了。Protein受到致命的冲击,一下子只剩下三个人。我们这些留下的人不但有兔死狐悲的感慨,也有卸磨杀驴的愤怒--那些进度慢的项目反而没受到什么冲击。接下来,Protein合并到另一个叫CM的项目中去了。到十月份,Protein的Lead,Olivia主动去了另一个部门。在十月底的部门重组中,Protein只剩下一个人在做维护和修Bug的工作了。Protein团队至此彻底烟消云散。这让我们觉得,过去大半年的努力不过是一群幼稚的程序员闹出的一出笑话。</p> <p>尽管Protein已经结束,但是回顾过去一年的时间,我觉得需要特别感谢三个人。第一个是Jessica Cao,没有她的努力,我们中国这边可能不会有机会尝试做新版的Protein。其次是Scott Morrison,美国那边的架构师,他有出色的能力理解我们的设计和意图,也充分地信任和放权给我们。最后是Olivia Wei,没有她的一再鼓舞和劝说,xirang只会是我个人的业余作品。没有她做的大量文档化和解释工作,xirang也不会那么快被接受。Protein团队的其他人对我的支持和肯定,我也会永远铭记在心。无论如何,就我个人而言,做Protein 4.0的过程,是我在这家公司最难忘的一段经历,虽然中间并不轻松,结果也是个悲剧。</p> <p>R.I.P, Protein!</p> </div> 梦断代码--一个程序员的自白(八) http://icerote.net/blog/post/71 http://icerote.net/blog/post/71 <div style="direction:ltr"> <p>因为最初我只是想证明我的那个可扩展的类型系统,因此我给ADPLite改了个名字,叫“息壤”,喻意类型系统会像息壤一样,能够生长。到11年夏天的时候,息壤已经有了一些东西:</p> <pre><code>1.DBC库--这个是以前的工作。除了pre-condition,post-condition,invariant三个宏之外,允许用户自定义和设置错误处理句柄。从而支持测试和特殊需求。 3.buffer,和vector类似除了a)只接受pod类型,b)resize的时候不会初始化对象。我用它来完成各种基于内存块的操作。 4.iterator,一个擦除了真实迭代器类型,但保留元素类型的模板类。 5.string,三个不同用途的string的实现。 6.exception,定义异常的辅助宏,和辅助收集异常抛出点的上下文。 8.archive,一组相当于iostream的接口,但是简化许多。两组实现,一组是内存的(5个),另一组是文件(一个,mmap实现). 9.一个类型系统的抽象实现,也是息壤名称的来源。 10.vfs.一个virtual file system的接口。这个当然不是用在OS上的,也是相当简化。有一个基于内存的实现,一个本地文件的,和一个能用,但是未完工的zip的实现。 11.range,一容器的方式提供迭代器对,避免不必要的容器复制。 ...... </code></pre> <p>虽然这些东西只是业余写写的,且断断续续,但还是严格地遵守了这些原则:正交分解,接口最小化,依赖倒置,区分方法和查询,Liskov替换,Open-close,异常安全等等。就这么丢了有点可惜,我何不花点时间把它写完呢?第一步,先清理这些代码。我删掉了一些练习和不成功的尝试,如一个STL风格的tree,可是最后却发现并不好用,关键是不能定制children的管理方式,这变得很鸡肋。一个奇怪的,编译期解析的配置文件读取类等等。一个完善现有STL迭代器分类的尝试。也保留了一些不完善,但是有用的东西,如range,tuple。写tuple完全是因为不想在接口中暴露boost,当然最后还是放弃tuple了,这其实是个很不情愿的决定。</p> <p>在写息壤的过程中,有些决定很明智,有些则显得愚蠢。但无论如何,没有放松对质量和性能的要求。无论对错,我希望把这些内容记录下来,供借鉴或是批评。</p> <ul> <li><p>DBC库的三个宏,最初的实现是支持搜集任意数量的错误点上下文的,但是因为对文本转换的支持不够理想而去掉了(在异常支持组件仍然保留了这一设计,但是实现手法并不相同)。用户可以通过一个额外的宏来控制是否生效而不仅仅是依赖Debug/Release编译宏。</p></li> <li><p>Iterator本来有两个目标,一是擦除迭代器类型,二是实现新的迭代器分类,把遍历和读写访问分解。曾经有文章批评STL迭代器把遍历方式和访问性这两个正交的概念混在一起,导致一些棘手的问题,理想的做法是将之分离。因此读、写构成可访问性概念,遍历概念包括:可递增的,单遍,前向,双向,随即。我写出了将任意一个STL迭代器映射到新概念的实现,但是无论如何,都显得非常累赘和低效。要做的更好必须重度使用TMP(template meta programming),实现代码不但晦涩而且工作量也不小。这也和我TMP不够纯熟和缺乏支持库有关--boost不能用在这里,C++0x当时我还没打算用。最致命的是,如果我用这样的一个新概念STL,我就在STL和boost中步履维艰,许多必要的算法都不能用了。所以我最后放弃了,仍旧回到STL的iterator,但是优化了类型擦除时的性能。只要迭代器不是太大,就可以避免内存分配操作--这可以单独写一篇技术文章。</p></li> <li><p>STL一大让人诟病的地方是,只有迭代器对,而没有提供封装(最多用pair,但没有与之关联的算法支持),要知道C++可没法返回一对迭代器的。所以,我用range包裹一对迭代器,这在返回一个范围时很有用。但是现在想起来,我对range的设计仍然不够好,这在配合内存读写时特别明显。我的range就是定位成一个半开半闭的迭代区间,这个有时过于抽象。当我看到GoLang的slice是,我才知道我错在哪里,并且应该如何弥补。go的slice还有个额外的属性capacity,这在复制数据到一个buffer时非常有用,slice的capacity可以允许安全地越尾写出数据。我应该写一个slice,用于内存操作相关接口的,而不是直接用range。</p></li> <li><p>String是非常重要的一种数据类型。STL的string虽然够强大,但是问题相当的多。借鉴Java,我设计了三个class: range_string,string,和string_builder,这是三个模板类。range_string实际上是一个迭代器对,但是保证其指向内存连续的区间。string则是一个immutable的实现,因此使用了引用计数的优化。string_builder的内部则完全类似STL string了。然而这里还是有两个重要的优化没有做。一是string pool。对于string,启用string pool有可能极大地降低内存使用,和优化字符串比较性能。但是启用这种pool机制必须是用户可控的和安全的。另一个是string_builder赋值给string,如果两者的内部数据结构是一样的,就可以在某些情况下move而不是copy。但是,我想不出在move时如何解决string_builder额外浪费的内存,或者如何评估这种浪费是划算的。range_string的本意是避免无谓的字符串复制,但是我没能做好这一点,在一些应该使用range_string的地方使用了string,至今没有纠正。完成string后,又写了utf8和uri编解码的算法。为了支持本地化,ascii字符串显然是不行的,但是我并没有打算使用传统的宽字符。我以前就注意到,如果程序使用utf8作为字符串编码,几乎是完全够用的--除了下标访问。而我因为已经习惯于使用迭代器风格,发现极少需要使用下标。因此,用utf8作为字符串编码是合适的,而且不需要两套字符串类型。当然,支持从utf8到ucs的转换是必要的,这就解决了偶尔需要的下标访问问题--以性能为代价。当我看到Golang也采用同样的策略时,更坚定了我使用utf8的信心。后来的经验证明,这是明智的选择,几乎所有的STL算法都可照常使用,和第三方库的交互性也更好了。</p></li> <li><p>Archive是用来做io的。延续iterator的经验和正交分解原则,我把读写访问和seek分开了。数据访问有reader、writer。seek的接口有sequence(single pass),forward,和random。sequence实际意味着不能seek到一个新位置,比如标准输出。forward可以用于管道或socket,random则可用于文件。一方面,我让seek相关三个接口支持必要的查询,如当前offset,size信息(可能返回表示未知的值)。另一方面,尽可能简化,比如seek方法的定义:</p> <pre><code> virtual long_size_t seek(long_size_t offset) = 0 </code></pre></li> </ul> <p>在我看来,传统的fseek提供起点位置完全是罗嗦的。</p> <p>接口的正交分解也不是没有代价的。C++的多重继承固然很容易组合多个接口,但是缺乏从多个接口中选择子集的能力。例如,我可能有一个实现了reader,writer,和random的archive类,还有一个函数接受一个指针,指针指向一个具有reader和forward接口的对象。C++中没有一种简易的机制让我描述该指针的类型(archive&lt;reader, forward>* p),并容易地从archive类的对象转换过来--我需要的是golang中的interface机制--和C++中曾经有人提议的dynamic concept map类似。最近,我终于在C++11中模拟了这个机制,工作得还不错。但是当时还是决定采取保守做法,利用dynamic_cast做了个blob的接口设计(blob接口是C++所反对的。STL的char_traits, iterator_traits都是blob设计。C++11中的type_traits则不是blob的,是C++鼓励的设计方式。)来做接口查询。这个设计的好处是简单,可行。坏处是缺乏弹性,僵化,丑陋。现在看来,我当时的决策太保守,应该更勇敢一些。这种设计也带来一些恶果。</p> <p>Archive最典型的实现当然是读写文件。为了性能,实现采用的是mmap方式。reader/writer的数据读写接口都和传统的read/write本质上是一样的,都是复制数据到buffer或相反,不过是形式稍有不同。既然采用mmap方式,那就可以直接返回一个地址指针了。因此,我又实现了const_view和view, reader/writer可以返回要求的const_view/view。这就避免了额外的内存复制。但是,不是所有的archive实现都能支持view的,所以又提供了一个viewable的方法去查询archive的能力。我后来发现,view应该被看作是一种数据访问方式,而不是reader/writer的另一个方法。如果我把view分离出来,reader/writer就不必提供viewable,并且几乎减少一半的方法,而且,很多不能支持view的实现就不必写好几个空的实现--只要不支持view接口就行了。可是如果分离view,这就要在那个blob的接口中增加新方法。受blob设计的阻碍,我做了错误的判断和决定,以致我在不同的派生类中重复空的readable和view_rd方法。</p> <pre><code>struct AIO_INTERFACE reader { typedef buffer&lt;byte&gt;::iterator iterator; virtual iterator read(const range&lt;iterator&gt;&amp; buf) = 0; virtual bool readable() const = 0; virtual const_view view_rd(ext_heap::handle h) const = 0; protected: virtual ~reader() = 0; }; </code></pre> <p>Archive的实现类包括:基于文件,基于一个可交换内存到磁盘的heap,基于一个给定的buffer对象,共三组。还有一些adaptor。虽然数量众多,但是因为接口简单,每个实现也都很简单,总共也就800行代码。为了避免跨平台的麻烦,我是用boost.interprocess.file_map来实现文件archive的,这个file_map对windows平台的路径支持有问题,不支持wchar_t,只有mbcs。我不得不给它打了个patch,我看了最近的boost,好像到1.51也还有这个问题。还好,这个patch很容易打。</p> <ul> <li><p>VFS包含一个RootFs类和IVfs接口。之所以区分RootFs和IVfs是受Linux的启发,Linux也是区分两者的。RootFs只用来mount(unmount) IVfs,以及相关的查询功能。IVfs提供常见的目录和文件操作--文件操作不包含操作文件内部数据,那是archive干的事。包含三个实现,一个基于本地文件系统,一个基于内存的,另一个是基于zip格式的archive的。在ADP和Protein中,我受够了在不同平台上对文件路径的处理。受boost.filesystem的启发(进了TR2,但是很奇怪,居然没进C++11,这是我认为非常漂亮的一个库,特别是把路径操作抽象成纯粹的字符串算法非常出色。只是要编译这一点比较糟糕),我决定在整个息壤中,采用同一的文件路径格式,也就是unix风格的路径。通过契约约定,息壤只会接受和产生息壤风格的路径(除了两个专用于转换路径格式的函数),在所有和平台API交互处做路径格式转换。VFS也遵循这一约定。后来证明这是明智的做法,消除了所有ADP和Protein中出现的此类混乱和罗嗦的代码。</p></li> <li><p>对于采用utf8和单一路径格式这样的决定,并非都是模仿其他的范本作出的决定。软件设计通常应该要尽量推迟决策,这样会比较有灵活性。但是Ken Thompson说过:你总是要在某个地方hardcode。这在golang中很容易感受到这种设计哲学。一味地推迟决策也容易带来额外的复杂性。当然,后来在一些优秀的系统中看到相同的设计决策,就会额外地得到鼓舞。</p></li> <li><p>类型系统基本上是模仿C++的struct。因为要考虑到效率,空间布局和时间性能都是必须要非常紧凑。类型系统有基础类型和一个组合扩展机制构成。对基础类型定义了size,alignment,还有一个pod属性用来配合做对象初始化和销毁。对所有的类型,都定义了公共的方法,包括构造,析构,赋值,序列化,和一个typeinfo的handle,用来辅助检查绑定到C++对象时的类型是否正确。有了size和alignment,组合机制就可以像C++的struct一样,计算出每个数据成员的偏移量,也就可以直接访问了。组合机制还支持了多继承,这个纯粹是不想引起C++程序员的惊讶,所以没有坚定去掉的决心,后来实际上根本也没有用到过。从内存布局来说,继承和成员变量的方式并无区别。</p></li> </ul> <p>类型系统还支持参数化类型机制,有点像运行时的C++template。这样,对于数组,就可以定义为一个带参数的基础类型,用到的时候只要和具体类型绑定就可以了。如果需要一个新的复杂数据结构,完全可以用C++写一个,然后作为基础类型加到类型系统中。这是很有用的,因为类型系统并不是ADT,不能随意增加方法,C++则很方便加方法。如果不需要增加复杂的算法,也可以直接在类型系统中进行扩展。两种方式在息壤都有使用。</p> <p>类型系统处理的不好的一个地方是,其上有哪些方法是预定义好的,很难扩展。除了构造,赋值这种基本操作外,也可能需要比较,hash这样的方法。Java中的做法是在Object中一股脑加进去,这是我不喜欢的,因为不是所有的数据类型都一定可比,即使可比语义也未必也一样。我希望的是有一种自由的方式给某个类型附加任意多的方法。方法表似乎是出路,但是hash表的代价太高,我认为不可接受,数组的排序问题怎么解决以及查询机制又是个问题。怎么做最好我到现在也没想到,只能留着慢慢来。但不管怎么说,即使扩展方法的能力欠缺,但是扩展数据类型的能力也是意义重大的。在可以预见的未来,类型扩展机制够用了。这种用基础数据类型装配出新的类型,新的类型有和基础类型一样成为装配更新的类型的材料,仿佛可以自我增殖一样。正是这种特性让我联想到息壤,传说中可以生长的泥土,我也因此用息壤来作为项目的名称。</p> <p>考虑到类型定义可能是一段手写的文本描述,那么手写的简洁性是必须考虑的,另外,还要解决类型名称冲突。为此引入了别名机制(alias),和命名空间(namespace)。有了namespace,就可以用完全限定名称来唯一标识一个类型。这个类型系统在我看来还有一大缺陷,是参数化机制引起的。在C++中,Array<int>这样一个类型的名字是用某种拼接机制产生的字符串,参数很多或这嵌套很深时会导致很长的类型名字,这是极其让人讨厌的。另外,给两次Array<int>产生两个类型也很让人蛋疼。如果每个类型都有一个确定唯一的id那就好了,但如何产生这个唯一的id很是头疼。我想过对类型的成员、属性做sha1或md5来产生id,但是如何处理碰撞呢?虽然碰撞的概率接近于0,但毕竟不是0啊。这个问题也是到现在也没有去解决的。</p> <p>还有另一个和实现有关的问题也很让我闹心。息壤有一个运行时系统管理所有的类型,类型之间显然会存在依赖关系。那么我需要在很大程度上保证用户不会建出一个错误的类型来,也不能让一个建到一半、未完工的类型保留在系统中。当时我还希望把类型的描述通过一个数据结构提供给息壤,然后息壤一次性处理这个数据结构,如果失败,就要指出数据结构中错在哪里,并且对系统状态不能产生影响。看上去这个设计非常简单,但是实现起来却变得困难重重。我反复实现了三次,每次都不能让我满意,而且使用的时候,设置那个数据结构也很罗嗦繁琐。没办法,我决定先放弃那个数据结构和错误报告机制,先以最简单的方式完成功能再说。但异常安全是不能放弃的,因此,我写了一个transaction类,负责完成所有的数据building的工作,并在失败时回滚撤销。这样一来,transaction类又变成了一个大杂烩式的肥大类,充满了各种操作。但是transaction也还是有价值的,它把复杂的构建充分分解成了一系列充分小的操作步骤,这样每个步骤的调用点就只会依赖类型描述的一个数据点--不管这个描述是数据结构还是一种语言文本。这种推卸是可行的。我后来在写第一个demo时,很容易就在解析器中做了错误报告,根本无需依赖息壤。</p> <p>之后,transaction改进的方案似乎一下子就自动浮出水面了。第一个transaction依赖息壤运行时的问题。在实现transaction的时候发现,transaction其实只在最后提交的时候才需要和运行时打交道,也就是说,我很容易创建一个完整的,但是没有登记到运行系统的类型。这是测试需要的,意味着我可以把一个类型对象隔离到更小的范围了,而无需像最初那样,为了得到一个测试对象,满世界都要被惊醒。另一个是transaction的那些方法很容分组归类,自然地,应该被拆分成多个class。自然地,先用builder模式创建对象,再在事务中提交类型对象到运行时就水到渠成了。transaction被分解成了若干个互不相干的builder,彻底解决了长期让我难受的问题。</p> <p>虽然只能在工作和陪孩子之余才能有时间写代码,也想尽快地把息壤弄完撒手,但我还是给代码定下了--或者说坚持--质量优先的原则。在设计方面,主要坚持如下原则:</p> <pre><code>1.Design By Contract 2.基于组件 3.方法/接口正交分解 4.接口最小化 5.区分“方法”和“查询” 6.“方法”的效果是可观察的(通过“查询”观察) 7.异常安全 8.支持设计演化 </code></pre> <p>当然,还有一些广为人知的原则是必然坚持的:KISS,DIP(依赖倒置),DRP(不要重复自己,Raymond称为SPOT,真理的单点性原则),SRP(单一职责)。MVC也是也别注意的。</p> <p>对于质量实际上体现在两方面。一是设计质量,二是实现质量,当然两者也是相互依存的。特别是设计质量,很大程度上依赖于那些设计原则坚持了多少,另外单元测试也非常重要。成熟的程序员应该主要把写单元测试看作改进设计的手段,检测实现那只是副产品。对于实现质量,我认为单元测试是最经济高效的办法--当然,设计必须支持测试。很多开发人员对测试没有清晰的认识,分不清各种测试的职责和价值,更不要提写容易测试的软件了。测什么?怎么测?以及什么样的系统才是容易测试的?这不仅是测试需要关心的,开发一样要关心。对于开发来说:1.测的是接口,接口除了函数原型外,就是行为。而行为可以通过输入、输出的契约来描述--虽然可能有些契约无法用编程语言描述,甚至很难形式化。把对行为的关注转移到对契约和参数的关注上来,就是从面向实现编程转移到面向接口上来。这种转变实际上关乎到测试的能行性。2.给定被测试对象一个输入数据,然后观测系统的状态,检查是否符合预期。如果遵循设计原则中的5和6两条,那么这种测试方法就是可行的。3.符合2、3、4设计原则的才是容易测的。测试的一个基本要求就是能将测试对象孤立甚至是隔绝出来。如果给测试一个对象一个输入,牵一发而动全身,整个系统,无数的状态改变,又如何用有限的资源完成测试呢?一个理想的系统,测试代码量应该和接口函数的数量之间保持线性关系。另外,创建或初始化测试对象,或者说测试的数据准备工作应该非常便捷且廉价。我对息壤中比较基础的,重用很多的部分测得比较完整,因此也几乎没出过什么问题。</p> <p>另一个重要的问题是性能。我认为很多人没分清性能和优化。软件的整体性能是很难通过优化解决的(老实说,我从未见过成功案例),优化只能改善局部性能问题--通常以复杂性为代价。性能必须是从初始设计开始就要关注并且持之以恒地维持的。我认为通常有两类性能问题,一是追求单位时间的最大吞吐量,另一个是追求响应速度。两者都很常见,是截然不同的两类问题。大部分情况下,性能问题的解决方案能同时改善这两类问题,典型的例子是算法改进。但是有时也会冲突,例如,为了解决一个响应时间的性能问题,有时可以采用并行计算来加速,或者把计算的某些部分延迟到响应之后。这些措施都存在额外的开销,这将降低系统的最大吞吐量。</p> <p>我非常厌恶ADP中那种错误的性能观点-比如喜欢用memcpy,原生数组等等-但仍然注重性能。首先是在接口设计上就要充分考虑,避免抽象惩罚,archive中view的设计就是一例。借助于C++的能力,让息壤的抽象惩罚主要表现在编译期,而非运行期。解决性能问题要靠设计,要落实到数据结构和算法上。Brooks曾经说过大意是这样的话:你藏起数据表,给我看流程图,我还是不知道你要说什么;给我看你的数据表,啊,我不再需要你的流程图了。另一个忘了出处的话是:算法是流动的数据结构,数据结构是凝固的算法。还是在ADP项目的时候--当时有人在鼓吹算法的力量,这当然也没错--我就曾经对两个同事说过:如果你不曾纠结于数据结构的设计和选择,那你还算不上是真正的程序员。我说这话是因为看到太多的人,太多的代码,肆无忌惮地往class里面塞成员变量,有时候,仅仅是用来代替传参数给某个成员函数,还有的时候则仅仅是当作局部变量用。另外,一个状态或者属性,反复地出现在多个class中,而对可能造成的不一致性视而不见。怪不得有人说学过数据库设计的人,做出来的OO要好的多。我开始赞成所有使用数据抽象技术的程序员都应该接受范式的思想。Protein在这方面很糟糕--虽然还算不上最糟糕的。</p> <p>接口设计也会关乎性能问题。不止一个项目中看到链表类提供下标访问,全然不顾这是个O(N)算法,用户用这样的链表,很容易就会不经意间写出O(N<sup>2</sup>)的算法来。我亲眼目睹过许多这样O(N<sup>2</sup>)的代码。少就是多,多反而会坏事。任何时候,当不得不选一个降阶的算法时,都不能轻易屈服,你今天对数据规模所作的种种假设,明天就会过时。任何时候都不要以“不成熟的优化是万恶之源”来反驳算法复杂度阶的变化(常数项不予考虑)。我在写息壤的过程中持续关注性能的方式就是关注复杂度。我的实践也表明,关注复杂度可以解决绝大多数性能问题,项目规模越大,复杂度就越重要。我认为,任何时候,改进复杂度的阶都不能算是优化,而是修Bug--设计或实现的Bug。把具体的实现问题正确归类到已知的算法的能力也是重要的,而不是随手写。比如我见到很多合并两个已序的序列(例如合并两个std::map到一个数组中)实现为O(NLgN)而不是O(n)的,在已序序列上遍历而不是二分查找的,不一而足--这种做法真的该打屁股。不要说C++算法库已经提供了可以直接调用的库函数,就是没有现成的函数,也应该自己按照算法写一遍。也许我孤陋寡闻,网上很多人喜欢谈论和关注的算法、数据结构,大多是特定于某个领域的,在某个点用过了,也就用过了。正经是排序,查找,二分,归并这些基本的算法像吃饭喝水一样,每天都要用到。数据结构也是,数组,链表,二叉树(C++主要就是map,set),hash表这些才是每天要用的。就连数组这样基本的东西,有些程序员仍然对其性能,内存,错误处理等等不甚了了,真是让人大跌眼镜。</p> <p>2011年,在Protein为了改进性能和支持新特性,我主要参与两个部分。一个是文件存储格式做了改变,虽然还是使用ADP保存到OPC格式,但是主要的数据不再保存为XML,而是二进制。另一个是引入了Schema的概念。这两部分都带来了大量的不稳定性。Protein的新格式保存出去的数据格式是按照Schema的描述来的,并没有自描述部分。Schema因为没有版本,因此遇到版本升级、修bug、同名合并等问题,其内容并不是稳定的。一旦Schema和二进制数据不精确匹配,就会导致在加载数据是崩溃。这个问题我们也是在一开始就提出来过,也是一样被告知不会发生,接下来的故事就是历史的重复,而且因为一旦出问题问题的后果特别严重,特别难定位问题原因。幸好,大部分时候不是我来修这样的bug。</p> <p>到6月底,当年的主要开发工作结束,然后就是集成。7,8月份,在和一款产品集成时(不是全新集成,只是程度更深),出了严重的性能问题。产品那边给出的接受标准非常高,只能是两边共同做优化。有段时间每天就是profiling,找热点,调整,等产品的反馈,然后继续。我做了两个优化,一个是修改FBX内部实现,对性能改善效果非常显著,但是读代码和修改时相当痛苦。另一个是改进Protein的引用计数机制,读代码那就不是痛苦了,而是想死的心都有了。改完了引用计数,还要改许多hack了引用计数的地方。总体上,我认为产品的优化对最后性能的改善作用更大些,但Protein确实也对性能做了巨大改进。</p> <p>到5,6月份,我把息壤也整理的差不多了,代码有2万多行,仅仅是ADP的十分之一。我没有精力也没有兴趣以息壤为基础重新实现一个Protein,但是又不甘心就此让息壤消失。于是写了一个demo,和一个很简陋的PPT,作为知识共享,介绍给了公司的同事,想以此作为一个了结。当时Olivia同学一再鼓动我,让我把息壤的文档弄好,推广出去,但我已经毫无动力了。</p> <p>当时公司决定全面拥抱云计算,中国这边的研发当然也是想方设法做一些原型,来被美国那边看重。我当时被要求做一个材质共享的原型。所谓共享,就是在一个程序可以从服务器上在线获取材质的数据和图像,如果程序新建或修改材质,保存结果到服务器上,另一台机器上的程序就可以浏览到最新的数据,直接使用。这个过程要求不需要显式的下载过程,必须是和使用本地材质库差不多的方式。服务器则使用公司自己做的一个云存储的服务。最后给了我们大概两三个星期的时间。</p> <p>显然Protein的数据包格式不能满足要求,而两三周内在原来的ADP上改几乎是个不可能的任务。因为是原型,我们只想尽快把东西做出来。如果不考虑ADP,Protein只需在IO部分,把存出去的数据重定向到网络服务器就可以了。息壤的VFS来做这个任务是非常简单的。于是,我决定用息壤来做。首先是花了一周把息壤从Linux移植到windows。另一个同时包装了一下云存储的几个功能给VFS用,然后我就给这个云服务实现了一个VFS,最后修改了一下界面。结果,那个云存储的速度慢到无法忍受。于是决定自己搭一个本地服务器,不用云存储。这时,息壤的表现非常棒,虽然加了几个小时班,还是在一天之内就弄完了。性能不是问题,用来演示是足够了。</p> <p>我本以为息壤的命运也就到此为止了,但是没想到,事情又有了转机。</p> </div> 梦断代码--一个程序员的自白(七) http://icerote.net/blog/post/70 http://icerote.net/blog/post/70 <div style="direction:ltr"> <p>大约是各方面对ADP的反响都不大好,ADP要想推销自己,必须找到新出路。最终,目标锁定到Protein,让Protein通过ADP来存储材质包。对于Protein项目,我至今也不知道最初的立项缘由。Protein后来被定位成一个内容(材质)管理库,为公司的所有产品提供统一的材质管理。最早,Protein是基于FBX(一个3D文件格式和SDK)来实现的。ADP同事加入的是Protein 2.0。由加拿大的一组同事设计了一套新的API,表面上隔离了FBX。O和G做了将ADP集成到Protein的所有关键工作。除了用一个单独的DLL用来作为胶合层外,ADP也为此大作修改,几乎重写了runtime部分。此时,大部分中国的同事,也包括我,是对这些工作一无所知的,或者说是漠不关心。我只是做一些常规性的工作,解决各种编译、移植、测试问题。随着ADP集成结束,我也转到了Protein项目。合作者也换成了加拿大的同事。</p> <p>在产品集成Protein 2.0的过程中,遇到了许多早就预见到的问题。我称之为,明知前面有坑,被推着眼睁睁往下跳。我早在ADP时就说过,即使把数据存成文本,依然需要制定存储和转换协议,并预言中的问题却一一变成现实。一个是本地化的问题。例如在德语中,千分位和小数点符号不同于英语,而是相反。因此,“3.142”这样的数,到了德语里面就被读取成3142,这显然是不能接受的。这个问题在项目早期很好解决,就是要规定外部文本的解析方式,比如统一使用C locale保存就很容易搞定。另外一个是精度问题。比如0.3这样的数。用户输入的0.3,下次再打开,显示的却可能是0.2999999999。其实对于明白原因的人来说,这再正常不过了,可是对某些人却是难以接受的。由于Protein内部同时使用float和double类型,double还好一点,float出现前面问题的机会非常大。于是一个凑合的方案出台了,就是全部改成使用double,显示的时候,截短显示位数,利用平台API的能力来达到显示为0.3的效果。这种做法是非常脆弱和幼稚的。我们需要通过制定数据规格,让用户理解,什么样的显示差异是有道理的,也是符合预期的,而不是凑一个让用户满意的结果。如果我们明确了这种差异和差异的原因,用户代码自然就有据可凭,去填补数据表示和显示之间的空隙,而不是一团糊涂地代劳。事实上,提高精度和利用平台API的方法,在用户做了一个float到double再到float的转换后,因为精度丢失,就失效了。精度问题在后来Protein 3.0引入限制数据范围的特性后变得尤为严重。从字面上看是合法的数据,但是代码执行中却可能是非法的。这是因为double转成float会四舍五入,所以舍入后方向并不确定的缘故。这种问题对某些软件来说完全不重要,对另一些软件来说,则是至关重要,因为它影响到数据正确性。我们公司的软件正是后者,多是用来做各种设计--也就是创造数据的。</p> <p>另一个我认为很重要的数据完整性(包括正确性)问题,则从未得到重视,或者说从未被理解过。在我看来一个很显然的事实是,对于ADP、Protein要处理的任何外部数据,都要持恶意假定,即可能是任何形式的坏数据。在此情形下,在处理到任何坏数据时,都不能出现崩溃或其他的致命错误(如内存耗尽,死循环等等)。即便是安全性错误也是要竭力避免的,如临时文件漏洞,缓冲区溢出等等。对于保存数据,ADP采用了创建--改名的机制,虽然很慢,很笨拙,却也有效地避免了数据完整性问题。就地修改的特性,且不采用回滚日志,最后也没做出来--幸好没做出来。时至今日,ADP、Protein在读取到坏文件时,几乎100%会发生故障,安全性问题更是没有任何评估。然而就这样的东西,现在居然有人将之部署到网站,用来处理用户提交的数据文件!这是打算引诱人犯罪吗?</p> <p>当被自己预言过的问题坑了的时候,我不会觉得自己英明,而是为自己的无能为力感到羞耻乃至愤怒--愤怒不过是无能的另一种表现形式罢了。乃至于到我快离开ADP时,还给了我一个几乎让我暴走的任务:为了避免阻塞在费时的ADP保存数据操作上,要让写出过程是异步的,用多线程解决。要知道,这是ADP的运行时太慢导致的,即使我异步处理,仍然要读ADP运行时数据,而且为了保持数据一致性,就必须要锁住ADP系统--ADP可没有快照机制--直到保存数据操作完毕,那么要异步何用?这个任务我拖了好几个星期,并且被告了一状。最后说我临时先写一个,将来会用一个多线程IO的东西将之替换掉。没办法,我只好去做那个有害而无益的任务。在后来的Protein中,我不知道有多少次遇到多线程IO上出现的bug,而且,每次定位和修复bug都要花费好几天的时间。唯一值得庆幸的是,我写的那个临时方案确实很快被替换掉了,我不必为此背负骂名。</p> <p>Protein团队的组织有点松散。我到了Protein长期处于无事可干的状态。在完成了Property和PropertyTable的实现之后,发了两次邮件请示工作无果后,我也就懒得关心,看看代码就是了。Protein由多个模块组成,我们的主要工作是其中一个dll,即提供对材质库的访问和管理,以及为材质对象建模。Protein API的风格应该是受Java或者.Net的极大影响,全部设计为接口,整个库导出一个唯一的全局函数(GetIAssetLibraryManager())作为入口点。因为没有导出任何实例类,所以一切的工作都要从这个library manager对象出发,逐步展开。对于这样风格的API,如果设计的好,未尝不可。但不管怎么说,这样的系统问题也是很明显的:</p> <pre><code>1.难以获得语言的支持和配合。C++鼓励扁平化的类设计,为什么?说白了就是对非扁平化的设计支持不好。 2.从依赖关系上来说,library manager依赖整个系统,任何使用了library manager的地点,都隐含地依赖整个系统,这回导致系统僵化,难以重构。 3.无法直接创建任何一个接口的实例类,必须从library manager出发去创建。而library manager又不是factory,因此,任何一个组件都无法从系统中单独拿出来考察。这意味着组件隔离做不到,按需创建测试目标代价高昂,单元测试将非常困难和低效。 </code></pre> <p>除了两个给用户的扩展外,Protein的每一个接口,实际上只有一个实现类--就像Pimpl的接口和实现那样。然而,和Pimpl不同的是,Protein的接口和实现类的功能不是一一对应的,接口功能只是实现类的一个子集。这就导致一个很大的问题,在Protein的实现代码中,拿到一个接口指针,第一件事就是Downcast成其对应的实现类对象。这是典型的抽象不足的表现。然而,我们是缺乏API重构的权限的。事实上,大多数项目都缺乏此权限。我当然理解API变化会影响客户代码,但是我不明白,为什么在项目早期也不鼓励改进API呢?我坚信,没有人可以在不写实现代码的情况下就设计出足够好的API来。</p> <p>另外,因为完全依赖C++的接口,在C++中就不可避免地要返回接口指针,如何维护这些返回对象的生命周期就是要慎重对待的。当然,如果有统一的约定,情况还好些,可惜Protein并不统一,所幸问题到也不很严重。Protein的实现完全是构建在FBX之上的,因此了解FBX也是必要的。FBX当中有一些质量不错的东西,但是也有大量不可理喻的东西。我不了解FBX的历史,倘若FBX最初写于1995年前,我觉得这就是一个不错的东西,如果写于2000年,那就平庸得很了。这让我想起AutoCAD的2D引擎Heidi。很多人对Heidi表示不屑,但是如果考虑到那是在80年代一个人搞出来的东西,就不能不表示尊敬,Heidi达到了它那个时代业界的最高质量标准。即使放到今天来看,Heidi所表现出的简洁性、一致性、严密性仍然是值得称道的--尽管已不符合当今一些人的口味。</p> <p>除了接口,Protein的实现质量在我看来也是很糟糕的。各种special case--我就不知道有啥不包含特例的--对我这样的人来说,记忆这种逻辑不明的东西就是个噩梦。大量的代码逻辑复制粘帖自FBX,以至于读代码是完全不知道在干什么,性能也很糟糕,各模块的交叉依赖简直是做到了极致。另外,Protein还附带了一些渲染处理,界面工具,而非仅仅是一个数据模型和包管理。这种混乱的层次关系也让我对Protein很是不喜欢。Protein唯一值得表扬的就是,它确实做了一件用户需要的事情,用户也确实在用。因此,我认为无论如何,Protein比ADP强。特别地,当有人拿ADP和Protein做比较,说Protein质量差时,我都忍不住要反驳:Protein很烂,但是有人离不开它,他有用;ADP不那么烂,但是所有人都不想要,恨不得扔掉它。至少从我的角度来说,我希望ADP彻底消失。一来可以使用了它的那些软件免于腐蚀,二来可以掩饰我的失败。</p> <p>Protein所谓的材质(Material),实际上是一个属性集合对象(差不多是这样,Protein管它叫Instance)不同属性可能有不同的数据类型和值,某些属性还引用了另一个Instance。Instance中的属性值有些可以改变,有些不能改变。Instance不能添加或删除任何属性,因此,每个Instance都从一个模板--叫Definition--clone出来的。可诡异的是Definition没提供接口去访问其中的属性,要想查询,先clone一个Instance吧!想修改,没门。到了2011年,我们还搞了个叫Schema的东西,专门定义Defintition/Instance有哪些属性,属性的数据类型,默认值等等。这种三层的结构实在是让人头疼到爆。然而,我们无权更改API。</p> <p>我不想再去吐槽那些让人抓狂的错误设计,毫无营养可言,回忆它们也不能从中汲取任何有益的教训。我每次在代码上受到挫折时,就到那个ADPLite上写一点代码,舒缓一下心情。然而写不了几天,我就又要开小差了--我都不在ADP干了,写那玩意儿干啥--去尝试和验证各种新鲜的想法,甚至就是学习。然而,在这断断续续大半年的时间里,我发现也积累了一点代码。我厌烦了牵挂这么个东西,想做个了断。</p> </div> 梦断代码 --一个程序员的自白(六) http://icerote.net/blog/post/69 http://icerote.net/blog/post/69 <div style="direction:ltr"> <p>我对公司有一个很不满意的地方,就是缺乏像样的职业培训。我来到这里之后一直是传授东西给别人--当然我对此并不抗拒,还很乐意--自身的水平却没什么提高。于是我打算还是学点东西。我把业余的时间都拿来玩Linux和开发板。我工作中接触的第一个操作系统就是Unix,Sco OpenServer,所以自然地对Linux有着好感,但是一直没有系统地学习过Linux。但是2001年初次接触RedHat时,确实给我留下点心里阴影,后来就一直没怎么碰Linux,但是RedHat后来也确实带给我的都是挫折和痛苦。有一次,我被要求装一个RedHat 5 Enterprise用于Build AIRMax的各个项目,软件是各种缺,Yum还要收费。后来买了RH5的服务,Yum还是不行,要么没有,要么版本太老,各种受不了。最后,我还是到做Build的Team那里,用tar把整个系统打回来,再重新配置一下才搞定。一周以后,美国一个做Build的同事也要干同样的事。但是他比较有毅力,一个个RPM下载,安装,干了一天加一晚上,然后郁闷地告诉我,还有100多个包没下载。这事儿让我很是欢乐了一阵,这哥们儿太实诚了:D。最后中美人民达成共识:“I hate it”。</p> <p>既然是学习,就要做点功课。这次还是认真地比较了一下几个发行版,决定从最符合开源精神的Debian Squeeze开始,先熟悉Linux的使用。还好,Debian的资料很丰富,而我这次首先就是熟悉包管理系统,也算是对路。不过,有几样东西一直还是挺折腾的。最烦的就是X,变化还快。当初配一个XFree86还是XOrg,还是有点烦的,所幸,都很容易解决。早期的GDM也不够方便。在大致熟悉了Debian之后,我开始按照LFS的教程,尝试自己Build完整的Linux发行版。我并没有完全按照教程建议的那样选择软件版本和编译器,而是直接用当时最新的编译器和绝大多数最新的软件源代码。我小心地把每一步都用shell脚本记录下来,并且编上号。我忽然觉得我比那个下了100多RPM的美国哥们儿还有耐心。其中大量的时间花在看Linux的config选项上,可惜当时有人已经给那些config项写了个中文的解释,要不然我也可以考虑写一个。前后花了大概1个多月的时间,终于完工了。看着VMware中自己一步步做出来的LFS出现终端提示符的时候,还是很有满足感的。另一个感叹的是,这些开源的软件编译起来都太方便了,会了一个,其他的全都会了,这真正是一种愉悦的体验。哪像在公司build的项目啊,不折腾死你不算完。就像我早说过的,《程序员修炼之道》中强调的--其实更早就被强调过了--,必须一个按钮,或者一个命令就可以启动从编译,到测试,到打包,乃至到集成、部署的全部过程。就这事儿,还老是有人说:哎呀,全自动是好啦,可是很难做到啊,这个很难啦,那个很难啦,我以前的公司项目也是想做做不成啦。。。你全自动都没做成过,就敢说什么“是好啦?”这些事情,都是项目Setup的头一个月,甚至第一周内就应该全部到位的,自己做不来就赶紧求援,找人来做!我认为本年度我见到的最搞笑的事情就是,持续几年的项目马上/已经结束了,Continues Build终于上线了。早干嘛去了?</p> <p>搞LFS的过程比教程上的还要顺利,因为我使用最新的源代码,教程上需要打的patch大部分实际都已经fix了。因为我目的是为了搞ARM,所以接下来要要搞一个交叉编译的平台。我不想用开发板自带的交叉编译系统,太老,而且不能随时升级。另外,似乎怎么搞一个交叉编译系统的文章也不大靠谱,大多只是搞个别环节。这也可以理解,完整的工具链还是有点复杂的。所以CLFS就是必须学习和完成的。还好,CLFS对我来说反而更顺利和快捷,除了Perl遇到了一点问题。接下来,就可以用这个CLFS去build一个ARM的Linux系统了,这个步骤又和LFS几乎是差不多的,之前写的脚本就节约了大量的时间。所以后面的CLFS+ARM只用了1周多一点就完事儿了。这时Build出来的ARM linux还不包括内核,只是可以chroot切过去,然后可以用最新的工具链。这个过程的失误也不少。第一是我应该装一个QUMU虚拟机的,而不是直接上到ARM板子上,这后来浪费我许多时间。第二是我应该及时做笔记,把整个过程记录下来的。</p> <p>接下来,就是要给ARM板子移植一个Bootloader了。幸运的是,我买了mini 2440的板子,网上的文章铺天盖地。理所当然地,选择移植U-Boot。这一次我又犯了个愚蠢的错误,我居然没把U-Boot先放到版本控制中去。当时我已经开始学习DVCS,并且开始使用Mercure(Hg)了,建个库只是分分钟的事,也可以离线。但是我居然一直没建,不知道当时哪根神经搭错了。移植U-Boot对我来说是一个全新的体验,特别是ARM的汇编部分对我来说很是痛苦,只能用4个LED来调试,直到串口工作了,才算回到正常世界。所幸自举代码量很少,只是启动加载和少量硬件初始化。接下来的日子就是读Datasheet,算时钟频率,查寄存器手册,调驱动。这也是个没有干过的事情。一开始也是很慢,渐渐地才适应。当时一个是串口和串口工作前花的时间特别多,另一个是网卡,丢包的问题让我很是头疼。不过,U-Boot工作正常以后还是很开心的。而且因为网卡折磨我很久,网卡好了以后,我也就要折磨一下它。我把那个NetConsole模块弄工作,再用nc连上去,这就相当于可以通过网络进PC的BIOS了。所以,当时就希望能通过netconsole给ARM板子装操作系统,Debian,可惜我最终还是没能成功。</p> <p>Bootloader有了,Linux系统也有了,就差Linux Kernel了。其实U-boot的代码也是从Linux Kernel里剥出来的,我当时用的是1.6,但是kernel好像我已经用2.6.28还是32了,驱动部分的代码不太一样了。所以硬件部分的修改要再次找出来,改好。有了U-Boot的经验,这次倒也算轻车熟路,况且Linux对2440的支持还不错。当然现在的支持更好了,我有看到编译Linux kernel时可以直接指定mini 2440板子了--以前只能选S3c2440芯片组。</p> <p>我在做整个LFS,CLFS以及ARM的构建时,只要可能当然是优化全开的。不知道是不是得益于此,当我第一次完全地在ARM上启动到全部亲手构建的Linux后,其性能还是让我很振奋的,非常快。后来又裁剪系统,塞到NAND中(用的是Ext2),那种搭积木的感觉真是爽快啊。在玩ARM过程中犯的错误不谈,遭遇的挫折和困难,知识的缺乏,学习之枯燥,直至后来用JTAG调试系统,比ADP不知道恶劣多少,可是却一直能兴趣不减,结果也很不错,收获颇多。可是为什么ADP就那么令人绝望呢?</p> <p>有了一个就手的ARM,干点什么呢?当然是当服务器用。第一件事,当然是装MLDonkey啊!汗!接下来就是折腾Hg。今天看来,当时没有折腾Git是个错误啊。又开始申请域名(当然不会是在国内申请),因为这些都需要web前端,又开始折腾Nginx,https,根证书--做CA有种很牛X的感觉,很奇妙。这一段时间我也没干什么正事儿,就是做文档仓库和代码仓库,当然还练习了几天网页和fcgi。不过得夸奖一下,MLDonkey确实很给力,就连好莱坞大片我都看不完。当时还想要的一个东西是blog系统,选择了据说是比较快的Wordpress,我用了sqlite做数据库。结果性能惨不忍睹,根本就不能用,我实在是很讶异这也能叫性能比较快?连带我做优化的兴趣都没了。另一个后果是,我长时间不再写Blog。结果,后继的一些打算也都没有做:笔记,邮件服务器等等。另一件让我印象深刻的事情是,我在arm上编译--不是交叉编译--boost release build时,第一次体验了内存颠簸的感觉。但是奇怪的是,系统并没有死的很彻底,这和pc上的反应不一样--PC在颠簸结束前完全是假死状态。一共build 过两次boost release,每次耗时在10个小时以上--内存不足,非战之罪。哦对了,搜狗和百度的爬虫太粗鲁了!我那可怜的ADSL就512K的上传,受不了天天有爬虫啊!Google显然礼貌的多,知道先拿robots,而且很久才爬一次!当时就一个很好玩的想法是:布置个应用,然后给爬虫喂误导性的数据,甚至有目的地诱导爬虫,会怎么样呢?</p> <p>虽然发现自己有太多硬件和系统内核方面的东西要学,可是又觉得要学就要有个结果,而那显然不是一个短期计划可以见效的,也就没有继续。LFS也有自己的问题,就是什么软件都要自己build,这作为一个固定功能的平台是够了。可是对于一个抱着折腾目的的人来说,这是不够的,能装上Debian才行。可惜我一直都没能把Debian移植上去--当然是从kernel开始的那种移植,让U-boot支持远程安装Debian自然是更加没影的事儿了。现在想来,我有点一根筋了,chroot一个Debian也足够用了啊,干嘛一定要和kernel较劲呢?如果把那时间用来研究UML(User Mode Linux)和LXC(Linux Containers)多好啊!这样我就可以放心地布置几个蜜罐,研究一下了。可惜梦想很丰满,现实很骨感。</p> <p>既然hg的代码仓库建起来了,免不了要开几个项目,写点代码练习一下。写点什么呢?想到那个让我不爽的ADP,我在ARM上建了一个叫ADPLite的项目,试试看,能不能一个人把ADP干翻。</p> </div> 梦断代码--一个程序员的自白(五) http://icerote.net/blog/post/68 http://icerote.net/blog/post/68 <div style="direction:ltr"> <p>O同事给我的回复中,除了强调控制内存使用外,原来还指望通过扩展string的实现来支持数组。他真是太高估我的结果了。但是同时,他又提出了一些让我难以理解和接受的内容:不喜欢暴露新的接口,数据必须保持为char<em>形态,模板不能跨边界分配和销毁。站在他预设的,string必须是char</em>的立场上,对string提出了许多质疑,在我看来是颇为可笑的质疑:</p> <ol> <li>是否意味着在加载数据时,如果有那么多string的话,是否会创建大批string对象?</li> <li>如果有1000个只含义个字符的字符串,是否要创建1000个string?言下之意,string太浪费了。</li> <li>我为什么将string的内容部分分离出来,而不是和string对象本身在同一个内存块上?</li> <li>任何程序都将会有自己的string类型,并且会只想访问ADP的char* buffer。</li> <li>坚持用户使用他们自己的string,这样string将会在我们的DLL外面。用户修改了就应该存储回ADP的runtime。</li> </ol> <p>质疑1毫无意义。质疑2不符合实际情况,况且我们有许多优化的空间。质疑3属于毫无头绪,透过合适的allocator当然能做到3,但是这么做意义何在?好处是什么呢?质疑4则是妄作假设。质疑5则只能说是基本功不足了。说什么string在dll外面的这种话,我很怀疑他明白自己在说什么吗?</p> <p>对于对象跨DLL边界,后来我发现不是O一个人的问题,许多人的理解都有问题。他们其实不明白为什么有时候一个对象不能跨边界,那些有问题的做法又是怎么导致问题的。甚至在许久以后,好像是在实现StringPointer的时候,M还指导过一位中国的W同事这个问题,然后我回信说他弄错了。他还和我争论了,并写个代码片段证明他是对的。其实跨边界行不行很容易判断,只要看边界两侧的代码对同一个对象的内存布局的假设(或者说约定,但是实际上没有约过)是否一致。如果是一致的,就能够跨,不一致,就跨不了。而对象方法的代码是否一致根本不重要,一个debug build的DLL有时候可以链接另一个release build的DLL就是例子。曾经容易出问题的一个地方是堆内存,原因就是边界两边使用的就不是同一个堆管理对象啊(其实只是些数据结构),但是现在堆在许多情况下已经不是问题了,比如VC就运行库。事实上,C++标准库早就跨边界传递对象--不是对象指针--好多年了。</p> <p>O重做了string的支持。决定采用定长的buffer来存放string!32,64,...512等几种。我被彻底打败了。更烦人的是,O总是把他的runtime的设计和XML存储下来的效果放在一起讨论。你一个runtime的结构和最终存下来是什么样子有一毛钱关系吗?把runtime是什么样子的描述清楚就够了,我们看100遍存出来的XML也还是不能理解你的runtime啊。当时被折腾的最惨的是TD,要通过读示例XML,来给runtime写测试用例。TD读不懂来问我,我也读不懂啊!</p> <p>多年以后,我也渐渐有点明白O的思路,到底错在了哪里。O当时一心想弄一个非常“Low level”的runtime,然后再加一层好用的wrapper,给用户用,底层负责解决高性能问题,wrapper负责解决易用性。貌似我刚开始写程序的时候也有过类似的想法。软件要分层是没错,但不是这么个分法。如果分层是个简单的活,那软件也太好做了。而且,这种分层法注定要很难逃过抽象惩罚的,因为不同层不但概念分层了,运行时也分层了。层次越多惩罚越重。写到这里,我忽然想,是不是很多的抽象惩罚其实都是这种无意义的自虐呢?不过我没有兴趣再去看这样的代码了。</p> <p>但是老实说,O的邮件中的态度在某种意义上激怒了我。我们可以加班讨论他那些晦涩难明的设计,但是对于我们发出的设计、解释,O实际上并没有认真理解。仅仅因为我通过placement new去构造对象,他就能以一句“the code logic is not clear to me”否决我们几天的工作 --第一次知道,placement new的逻辑原来是好混乱的啊。</p> <p>那个时候,ADP的其他同事也没闲着,我印象深刻的事情是研究Zip格式,要支持两件事,Zip64和inplace editing。这两件事其实是个信号,Zip64意味着ADP想处理大型数据,Inplace Editing则意味着ADP想提供程序运行时的高性能支持。</p> <p>从ADP启动开始,过了只是半年,就败象初成了。我认为ADP的两大主要目标全告失败了。目标之一是可交换的文件格式,之二是公司范围的统一对象模型。对于目标一我写过一个长篇邮件,指出要做哪些事情,同时也是对O同事用memcpy来保存数据的否定,可惜美国那边根本不当一回事 -- 即使我当时的经理重发了一遍我的邮件以期望那边能重视也没用。按照我的快速失败的观点来看,ADP此时就可以停止并且反省了。要么改目标,要么纠正方向。而次要目标,运行时的高性能支持,已经注定是不可能做到的了。</p> <p>虽然当时我已经对ADP失望,但并未绝望,认为有的是时间纠正错误。我因为之前引入了DBC,在一连串误会中,又扯出了错误报告机制,实际上就是个Log。我个人是不大喜欢Log的,也不认同很多人对Log的使用方式。我认为Log是用来记录工作流的检查点,而不是用来核对程序正确性的。程序正确性需要靠UT来保证。Log虽然也能起到错误诊断的作用,但是那只是副产品,就好比某人爱拍视频,但显然不是为了作为破案的证据的,虽然它确实有那作用。我写了个Log的设计给O,那个设计中提供了三个接口,Logger,Formatter和Device。分别用于过滤Log等级,格式化,指定输出设备。设计成接口的目的当然是为了可以定制和替换。另外,免不了的,还会有个总成的地方。并且,我也强调了,初始化这个日志系统的决定权应该交给最终用户。这次还不错,O觉得很好。然而正是这个东西,最后让我下决心远离ADP的主要工作,以免自己的声誉受损。</p> <p>那时候G同事忽然插进来,说生产者/消费者可能更好,然后logger的实现就可以非常简单,只要把数据打包然后发到一个队列里去就行了。这样,生产者都不需要考虑同步,消费者处理负责同步就行了。他所谓的生产者是用户代码,消费者是将数据打包发送到一个内部队列,这个队列被一个工作线程维护,然后那个工作线程负责格式化并把数据写出到设备。好处是Log调用处不会被阻塞。这样,我们只需要一个logger的实现就够了。</p> <p>当时正是多核狂热的时候,多线程和并行计算也被热切地讨论。G有这个想法很自然。而且,总的想法也不坏,可惜太不完善。首先让人难受的是术语。在讨论多线程的时候,我没见过把不需要同步的东西叫生产者/消费者的,只能理解为之前几天他被多线程的话题轰炸太多了。其次,说一个logger实现就够了,只是自大的妄想。今天回头再看当时的邮件,可明显地看出我和G在设计软件上的差异:我考虑的是Logger要做什么,应该让用户怎么用,那些地方用户会需要扩展,怎么扩展;G则是着眼Logger应该怎么实现,怎么可以一下全部完工,怎么用上先进的技术。我认为我们两者的一个根本差异在于,我相信程序库和产品是一个共存的关系,程序库要遵循Open-close原则,而G眼中的组件都是封闭的。</p> <p>其实,G所谓的那些更好的选择,其实在我的设计里面已经都解决了。我的那个Device接口只有一个write函数,非常容易实现,也就意味着非常容易扩展,这是故意的。要同时输出到多个设备,只要实现一个伪Device类,转发给多个其他Device对象就完了,根本不是个事。至于输出阻塞不阻塞,是不是放到队列里去,是不是起一个线程,有必要在Logger设计中考虑吗?留给扩展就行了。其次,Logger还负有过滤日志等级的职责,第一时间过滤当然是代价最小的。因为Log一般还要采集当时的一些执行数据,这就意味着要格式化以后才能打包进队列,这是开销很大的事,如果日志很多的话,这么做性能会很成问题。即使将格式化后移到工作线程中去做,前面打包数据进队列也是开销很大的,这还限制了所能采集的数据类型,数据得能打包才行啊。所以显然的,日志等级过滤必须第一时间做,甚至还要优化--理论上可以优化到只有一次标志检查的开销。</p> <p>本来这个Log是实现在一个DLL中的,G曾经要求将它放到叫core的静态库中去。O没有理会G的意见重新设计,但还是把代码提交到了Core中。结果,没多久就遇到了一个Bug,有两个DLL都link了Core,出现了两份Log管理器的实例。实际上我的原始设计中已经提供了解决方案,只要显式地初始化Log系统,就可以使两个Log系统使用相同的Logger,Formatter和Device对象。G对此很不满,认为我们如果按照他的意见实现就不会有这个问题。其实按他的要求实现只会问题更大,怎么启动的那个worker线程就会惹上大堆的麻烦,事实上也确实如此。不管怎么说,G后来还是给出了自己的设计,然后让另一个同事实现了。从此这个东西就开始一直折磨我们。一会儿性能出问题了,一会儿加载DLL锁死了,一会儿又程序退出锁死了。有产品要求我们不能默认启动,有产品又要求启动。还有产品一会儿要求自动启动,然后又要求不自动启动。程序崩溃时锁死,不能退出这个特性为我们招揽来了许多的崩溃报告。当然,修这样的Bug,还是可以彰显ADP的存在感的,多酷啊。从那件事以后,我就再也没有动力去思考ADP的方向了,我只冷眼旁观,只是多少有些不甘心。</p> <p>接下来的日子里,我发现我就没做成过什么事,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锁。真是恣意奔放,激情燃烧啊。只可惜烈火中没有重生的凤凰,只有灰烬。</p> <p>我那时其实已经有数年的多线程项目经验,并行计算,Lock-Free什么的也是接触了好几年,至少也是从正儿八经的教材和文章开始学起的。CSP虽然到今天我也没啃下来,但好歹是知道点方向的。可是有什么用呢?G同学拿着Lock-Free的大锤满世界砸的时候,我只能重复Andrei Alexandrescu的话,告诫说:“General Programming很难,异常安全代码也很难,可是和多线程比起来,那俩不过是小娃吃奶”。我想,那时候我没告诉他们有种东西叫MPI,大概是我在ADP项目中作出的唯一正确的选择。渐渐地,当有别的team人问我ADP的问题的时候,我开始不愿意去解释那些垃圾的设计。因为当别人一脸谦逊地问我为什么要这么做,有什么好处时,我觉得无地自容。我实在是没法解释,只能为自己开脱,说:这代码不是我写的,那个不是我设计的。这是怎样的一种悲凉啊。</p> <p>ADP还以一种病态的方式追求性能。比如坚持使用memcpy,坚持不用传统的Mutex,用TBB替换掉Boost.Thread等等,却在真正需要性能的地方挥霍。为了从ADP的runtime复制一个整数,首先要通过字符串名字查map来确定偏移量,然后再new两个串接起来的对象以shared_ptr返回。仅仅为了读一个整数,整个过程要做一个LgN的查询和3次内存分配!Runtime相关的测试中,内存分配释放一度耗费70%执行时间。就这样的结果还好意思提什么高效内存管理?G同学也不甘落后,实现了一个可局部排他锁定的图的实现,本意是想多个线程访问同一个图时,如果操作的部分不重叠,就可以避免锁等待。我自始至终就没看懂那个图的实现,但是我会测试啊。结果测试数据拟合下来的复杂度是O(N<sup>4</sup>)!就这样的东西,我真无法想象是如何还有勇气推销给别的团队的。明明就是个本地变量,栈分配就够了啊,非要new出来,觉得指针就是高效的?莫名其妙的做法不胜枚举,可以写本C++傻事儿大全了。</p> <p>渐渐地,我发现自己离开了开发的核心位置。我开始做各种杂乱的事务,比如在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代码的美国同事无人发表任何意见。</p> <p>然而也有高兴的事,08年春节前,我的宝宝出生了。那一年的上海下了很大的雪。然而,项目却越来越糟糕了。我不确定是否还有人有和我一样的感觉,或许,有些人感觉到了也不会承认吧。整个08年,我做的工作很少。我该是赶紧推ADP一把,好让它早点毁灭,还是无论如何,都要让它多撑些时日?和我的悲观相反,ADP此刻仍然卷入大量的资源。我的情绪却越来越坏,以至于我想把ADP干掉。我又想起我曾经做过的那个prototype,是不是重写一个ADP呢?即使我只有一个人?然而工作之余的时间几乎没有了,有时候写了几行,又找不出我这么做有什么意义,就又放弃了。</p> <p>到了08年年底,有三位ADP的同事转去做Protein了,我还继续在ADP打杂。Protein主要部分也是一个程序库,用来一个处理3D材质包的。到了09年年初,ADP开始要集成到Protein中去了。这时,公司开始裁员。我等着被裁,拿一笔钱走人了,不想再在ADP无谓地耗下去。然而很奇怪,没有裁到我头上。难道是因为我还算物美价廉吗?不管怎么说,既然没有被裁,工作还要继续。</p> <p>ps:关于术语、内容晦涩难懂的问题,我是知道有这个问题的。我想从技术层面去反省过去的种种失败,就不免会多谈些我认为的技术方面的失误,而不仅仅是像怨妇般地诉苦,那绝非我所愿。然而,正如我在前面说的,和技术人员沟通尚且困难,何况是外人?不是我不想写得更明白,而实在是我缺乏这个能力。如果指出具体的问题,我会适当修改,泛泛而论我就无所适从了。能看懂,固然欣喜,看不懂,也只好随他去吧。</p> </div> 梦断代码--一个程序员的自白 (四) http://icerote.net/blog/post/67 http://icerote.net/blog/post/67 <div style="direction:ltr"> <p>一个周末过去,对于我所回复的,如何在运行时存储字符串的问题的解决方案,O也有了回应。O对我的方案非常不满,认为我的方案是他之前就考虑过的,我完全没有解决他所担心的问题。应该说,这次合作不愉快,我也是有很大责任的,主要是在沟通方面。在之前的一周时间内,我除了问他问题,他回答,就没有真正双向的交流。他所担心的问题之一是内存碎片化,而我对此则根本没放在心上。他认为,对一个数据对象,存放在连续内存中是必须的,而在我看来完全不必要。而我则低估了他对这一点的看重。在我看来,因为这一层是紧靠IO层之上的。相对于IO的时间来说,内存操作的开销就不算什么了。另外,内存排布问题基本上可以通过一个定制的内存管理器,比如allocator之类的东西去控制。也就是说程序不直接关心内存从哪里来,反而给予allocator以机会去充分利用操作系统的各种机制去优化。从上层来说,使用方便也比一点点性能损失来的重要。何况运行时对象管理实现得越简单,从长远来看,反而有利于性能。悲剧的是,我错误地理解了对方心中主要问题。</p> <p>我当时主要的关注点是三个方面的问题。第一,这个runtime不能依赖于一个物理的IO接口,必须将IO操作集合先抽象出来。我们既不能假设IO输出到文件,也不能假设其输出成XML。一个条件反射式的方案就是adaptor模式。更进一步,在adaptor中支持compose模式。那么IO的骨干就完工了。我们很容易扩展adaptor,为输入/输出到本地文件系统,zip文件,http,ftp写专门的adaptor,这是一个刚毕业的程序员也能够胜任的工作。运用compose模式,我们就很容易实现加密、签名,支持文本、XML、二进制格式数据。更重要的是,这样做的好处是,工作被充分分解了,每一项工作对人员的水平要求都降低了,还可以多人并行工作。容易测试,也容易保证质量。这么做还有一个理由就是ADP只是个程序库,最终用户(产品)有各种理由要求调整和扩展。因此,ADP根本就不能和具体的IO adaptor绑死,而是必须提供多个小的组件供用户选择并装配后使用。当然,ADP可以提供一些预置的装配,方便使用,但最终 决定权必须交给产品。也就是说,可扩展性,可配置性(可装配性),简单性三者必须都要照顾到。后来的事实也证明,这种预见是必须的。否则,就是噩梦般的折磨。</p> <p>当然,这么做IO adaptor的设计风险也增加了。但是这些风险实际都是微不足道的。对于C++来说,可能的抽象惩罚也是几乎不存在--事实上,只要在设计和实现中避免冗余的数据复制就差不多了。毕竟,我们只是在和一个低速的IO系统打交道。当然,如果要较真,adaptor模式显然不适合大规模、重度的IO场景,比如,同时处理成千上万的文件、网络连接。如果是那样,就需要慎重对待了。显然ADP不是这样的项目。</p> <p>我关心的第二个方面是用户代码如何去访问这样一个内存中的数据?O给出的接口是非常原始和不安全的。例如,为了读一个int数据,用户需要准备一个int变量,然后调用接口,把数据复制回用户的int变量中。这种内存复制的操作是非常低效和不安全的,因为用户还要告诉接口int变量的字节数。接口实际只相当于memcpy,只是知道从哪里开始复制给用户而已。所以,我认为我们需要给用户一个高效、安全的数据访问接口。这不难做到。如果要访问一个int,那API就返回那个数据的int引用给用户即可。至于string,也是如此。当然,这就要求ADP的对象管理内部真的放了一个string对象才可以这么做。这也就是我为什么的解决方案中用了个string类型来放字符串的原因。</p> <p>第三个问题是真正的核心问题。至少我是这样认为的。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上。</p> <p>这样,ADP的一个主要目标,统一公司的数据模型对象的企图就必然要破产了。然而,也并不是说ADP支持了自定义类型和类型组合就能成功。这个机制的效率,空间效率和时间效率都必须非常高,要和C++相当,至少差别不大。此外,还必须有非常健壮、易用的API,才有可能让产品接受。要知道产品都是给公司挣钱的,都非常强势。幸运的是,这几个要求都是能够做到的。不幸的是,ADP一条也没做到。</p> <p>关于这个类型的扩展、组合机制,我后来又在另外两个程序库项目中见到,都是采取蛮干的办法,例如通过硬编码支持int,double,float,和string的数组。既不能允许用户自己扩展,也不允许组合新类型。唯一的办法就是修改库代码来支持新类型。因为修改程序库不方便,所以,产品么,只好将就吧。</p> <p>对于这三个问题的认识并非事后诸葛亮,而且,我也估计O同时并没有想那么多。所以,虽然O对我的方案很有意见,但是我没特别放在心上,而是认为事实会让他清醒的。后来的事实表明,我再次幼稚了。</p> <p>因为之前在DWF项目上时就profile过其中string类型的效率,指出了其中一些实现上的严重问题,也包括一些接口设计的问题。结果关于性能的结果得到了重视,而接口上的毛病则毫无音信,这颇让我怀疑美国同事在这方面的水准和品位--Linus 所谓的那种品位。因为DWF string的性能有问题,所以,我们应该避免这些问题,然后实作一个更高质量的string了吧?我还写了封信给G,说我打算实现一个更适合ADP的string,因为那也算是补足O的方案中未完成的部分。G的答复让我先是极为诧异,然后就凌乱了。G同事决定,我们不能使用string数据类型,因为string类型性能低下,所以,我们使用char<em>,哦,因为要支持本地化,所以,我们要统一使用wchar_t</em>!我理解不了这理由和决定之间的逻辑是什么。WTF,这是一种什么样的精神?至于我原来打算怎么设计那个string,G自然也是毫无兴趣过问了。</p> <p>这件事让我对自己也起了很大的怀疑,因为这样的事情不是第一次了。为什么我屡次想做得更好,却总是得到了一个更糟糕的结果?我究竟错在哪里?怎么做才是对的?是我根本就不适合在一个从属地位上做团队合作吗?短时间内一系列出人意料的结果,从UT,DBC,到这个string,第一次让我怀疑起自己和别人的沟通能力。我已经把对自己的要求降低到不指望和外行人交流了,可是现实却告诉我,我和内行人交流一样失败。我不知道为什么会这样,我开始对自己的能力感到强烈的不自信和怀疑。</p> <p>这个string的故事还没完。wchar_t*显然是很难使用的,可ADP还是就这样坚持一两年。最后实在受不了性能问题--实际上是必然的一个后果,过度扫描和复制字符串--要用引用计数。好像是美国的M同事,写了个叫StringPointer的东西。看名字就能知道,根本就不会有什么像样的API,事实上也确实如此。StringPointer一直用到ADP快结束时,为了将ADP集成到另一个叫Protein的项目中去,又弄出一大堆string类型来。这一次出来的东西更加让人受不了了,然而寿命却注定了出奇的长。留待后继再吐槽吧。</p> <p>继续第三个问题。类型复合和自定义用户类型是可能的,因为我们项目所使用的语言,C++就是一个现成的例子,JSON则是另一个例子。C++中可以定义任意类型的数组,这一点在std::vector表现的更清楚,通过变更模板参数就可以做到,当然如何在运行时做到还需要一点小技巧。而C的struct就是自定义数据类型的典范。如果ADP能够做成这两件事,那么就能够具有和C一样的用类型建模的能力--当然,离抽象数据类型(ADT)还差点儿。</p> <p>在那两三个月之后吧,我花了一个周末的时间做了个原型,尝试了一下想法是否可行。对于基础数据类型,我们可以知道,也只需关心其对象大小,对齐属性。我把要自定义的复合数据类型用一个表格来描述,就像struct所作的那样。这样,我就可以算出每个成员的大小和在对象中的偏移量。最后,得到的复合数据类型也具有一定的大小和对齐要求。这就和基础属性所需关心的特性一样了,换句话说,可以和基础类型一样被处理了。复杂的基础类型,如string,还需要关心构造和析构,这是为了和C++对象交互所必须的,其他某些语言则可以不考虑这个问题。而复合类型的构造和析构也很容易自动生成出来:按照恰当的顺序遍历成员进行析构和构造就行了。</p> <p>对于数组的支持稍稍复杂一点。我需要实现一个特别的vector类,元素类型不是模板参数,而是在构造时告诉vector的一个描述数据,即Type。vector内部就医根据Type提供的size,alignment等信息,计算出给定元素的偏移量,也可以用Type给出的构造析构方法初始化元素。</p> <p>这样定制出来的新数据类型可以和C++的struct描述的结果内存布局完全一致(当然你不能在C++中放任何virtual的东西),内存布局一致,就意味着用户可将之cast成他们喜欢的数据对象并直接操作。当然,直接cast是危险的,也是高效的。我们还可以提供一套带检查的cast机制,作为常规的使用方式。</p> <p>但是我当时在做这个的时候也还是犯了点小错误。出于简单起见,那个原型中作为输入的复合数据类型的描述表是一个数据结构,而不是文本,这样可以避免写一个解析器。但是,那个数据结构中的描述可能是有错误和矛盾的啊。检测错误是容易的,可是如何报告错误,并且尽可能精确定位错误呢?我不想,也不应该在将来解析一个有错误的文本时,扔出一个失败的结果,却不告诉用户错在哪儿了。用户不是小网站站长,我也不能做真理部不是?哦,sorry,Translation team,我真的不是在影射你们,你们不会告诉用户任何错误的,还会毁尸灭迹。</p> <p>我搞混了那个输入数据结构和文本的本质区别。以至于我后来为了实现一个即是异常安全的,又能精确报告所发生错误的解析器花了不少时间。要么难看之极,要么没有达成所有目标。最后才意识到自己理解上的错误,这大概就是一个人写代码所必须付出的代价吧,没人会指出你再明显不过的愚蠢之处。以后会有对这一蠢行更具体的记叙。当然,最后还是完满解决了。那是一个正面的教训:绝不轻言放弃,绝不轻易妥协。</p> </div> 梦断代码--一个程序员的自白 (三) http://icerote.net/blog/post/66 http://icerote.net/blog/post/66 <div style="direction:ltr"> <p>"程序员是工程师吗"?这是我在大约10年前初次接触敏捷方法时,让我有醍醐灌顶之感的一个问题。那个时候,称呼程序员为码农,IT民工,吃青春饭的,周围充斥着三十岁后能不能做技术的质疑。这个问题让我真正想明白自己从事的是怎样的一个职业。已经忘了最初在哪里看到这篇文章的,很幸运,我今天在Robert Martin的那本《敏捷软件开发》的附录D《源代码就是设计》中又找到了。它回答回答了两个问题,一,程序员是工程师,二,源代码就是设计。工程师的本质在于产生文档,代码就是程序员的文档。工程师只需要产生一次文档,然后其他人在此文档上反复工作,比如技术员、生产线上的工人。甚至,程序员的某些工作应该被看作是科研工作,因为“分类是科研的最初级形式”。我想,这一点未来在开源社区会变得更为明显的,一个优秀程序员做出来的突破性的工作,会被全社会共享。我曾经和一位朋友说,如果说工业革命依靠的是工人,那信息革命依靠的则是程序员。程序员不该看轻自己。</p> <p>我是以工程师的心态看待自己的工作的,也是以文档的心态看待自己的代码的。说到文档,我必须对那些不愿付出努力,就想理解系统的人表示鄙视。那种微软让扫地大妈来测试可用性的故事就是个骗局,太多程序员不还是用不好Word?那些认为自己可以让一个“untrained eye”也能轻易理解自己工作的人,不是狂人,妄人,就是所作的事情不值一提。和其他行业一样,程序员的文档只对专业人士负责。所谓简单设计,简洁代码,也只能是对合格程序员说的,而不是让一个未入门的菜鸟觉得简单。菜鸟的唯一出路就是变成专业人士。我对自己写的代码的态度也是如此,只有代码表达不出来的东西,比如系统的高层设计,如问题分析,动机,原理这些无法在代码中体现的东西才需要文档。最多再有个别技术难点需要点解释文档。此外的部分,所有文档都在代码内,用用doxygen这一类工具抽取出来方便阅读就够了。要求给vec.erase(unique(vec.begin(),vec.end()), vec.end())这样的C++语句写注释的人,一概鄙视之。</p> <p>当时的ADP也是很多琐碎的工作。QA的团队刚刚设立起来,如何做测试其实并没有什么经验。虽然我不认同测试、开发分离的组织模式,但这不是我能改变的。我对软件质量的态度也许偏执,但我坚持认为:逻辑,绝不会因为你写一个要求不高的程序就心慈手软,网开一面。在我看来,软件缺陷就是你的债务,而高质量、高可靠就是软件的不动产。当软件投向市场的那一刻起,你就必然,也必须不停地偿还债务和兑现资产。那种认为要求软件高质量就意味着的更多资源,更多投入的观点根本就是错误的。那种拿航空器、生命支持系统软件来说明高质量软件代价高昂的人,根本就是在说外行人的胡话。高质量软件开发过程非但不会增加投入,反而提高开发效率。只有高质量的代码,你才能,才敢于写完后忘记。只有忘记实现的细节,程序员才能将自己的智力解放出来,真正投入到分析问题,设计问题解决方案上去。那些总是纠缠与Debug,修Bug,各种Special Case的程序员,我无法想象怎么才能有精力去分析问题?怎么才能有勇气说,写下的所有不完善的代码,全都可以随时丢掉重新来过?在公司,我往往是作为一名C++专家,或者是能解决棘手问题的人被大家知道。其实这对我来说,不是什么褒奖而是诋毁。因为这无异于说我是“Language Lawyer”,而语言律师向来是我最为鄙视的。同样,对于Debug也是我深恶痛绝的,甚至我刻意地不去学习debug的技巧,事实上我也没有这样的技巧。我充分理解当年Linus为什么反对kgdb,而我也持有类似的观点。 。Debug必须被视为可耻的,是坠入地狱前最后的救赎,但却是不可靠的。所以,那些请我帮忙调试程序的同学们,每次你们都该请我吃饭以抚慰受伤的心 :P。</p> <p>言归正传。</p> <p>当时公司迅速扩张,我的另一件事情是面试,每周都有好几个。直到那一年年末,我大概面了近百人,虽然不是面试最多的人,但也名列前茅了。很多人不喜欢干这事,但是对我来说非但可以放松一下,还可以从来面试的人那里学习到许多有意思的东西。我是抱着找比自己优秀的人的心态去面试的,也是代表公司的,所以反复提醒自己,必须给予面试者足够的尊重。我后来听说有来面试的人因为对面试官很恼火,回去后在网上骂人的。然而,我面过的人当中居然有两三个发邮件来感谢我,有一个还是通过别的部门同事辗转联系到我的。这让我觉得非常意外,但是也很开心的事。我好像当时还做着给刚毕业的同事做些培训,可惜老板的要求太急功近利了,实际没什么效果。</p> <p>在我的极力主张下,把单元测试搞起来了,用的是boost.test作为测试框架。QA最初也使用boost.test。但是很不幸,首先是美国同事在UT中输出性能测试数据,后来又更过分地输出许多的log信息,这让我极为崩溃。即使多年以后,我仍不免要说:他妈的!中国的同事年轻,没有经验,算是情有可原的话,那么美国资深的,经验丰富的同事们呢?居然也连UT都不知道怎么做!难道还要我来给他们讲如何写UT?更何况这事儿是他们开的头!我给中国的同事解释为什么不能这么写UT,他们还是能虚心接受的,可是美国同事根本不理这茬。相反,后来代码出了Bug,或是Build break了,美国同事指责我们说为啥不写UT,或者是为啥不检查UT的结果。我勒个去!不客气地说,绝大多数开发和QA,对什么是软件测试,有那些类型,分别是干什么的和怎么干完全是一脑袋浆糊(包括但不限于ADP)。这是我第一次觉得自己给自己上了个套。除此外,还有另一个更让我抓狂和沮丧的事情就是引入DBC(design by contract)。</p> <p>在没有正确实践过DBC之前,我不确定程序员是否能理解DBC的美妙。当我在过去的项目中同时运用DBC和异常安全时,我得到了如同编程初学者所写的那种简单直白的代码。所不同的是,我不露痕迹地完成了错误处理。整个代码几乎完全由Happy Path构成,逻辑主干突出,分支语句比例大幅度下降(if语句比例可下降2/3。一个极端的例子是1400多行代码缩减到不足200行)。那些代码看上去简单质朴,但实际上都是深思熟虑的。反璞归真,我认为我的代码是有能力达到这个境界的。对许多程序员,甚至是资深程序员来说,如何设计软件是两眼一抹黑的。“设计之道,不在增无可增,而在减无可减”,不理解这话的人都还在纠结加什么功能特性上,要处理那些错误和特例呢。然而DBC虽好,还必须让运用的程序员能立刻从中收益,才能被愉快地接受。</p> <p>我为ADP写了一个contract的库,提供了PRE_CONDITION,POST_CONDITION,和INVARIANT三个宏,而且说明,只有一个PRE_CONDITION是必须的,提供三个只是为了概念的完整。可以等同于assert来使用,搞不懂的时候,就只用PRE_CONDITION,用错了也出不了大事儿。唯一要牢记的是PRE_CONDITION(expr)中的expr不能有副作用,这对于理解assert的人来说,应该完全没有记忆负担。本来,我还打算继续在此基础上,讲述如何设计API,如何划分API职责,要注意些什么。最好,能推行异常安全代码。我把Contract库,一些资料的链接,以及解释写了封长邮件给美国的架构师G同事。他先是回了封邮件说很好,说质量很重要。但是,过了个周末,他又回了一封信,这是让我彻底抓狂的信。</p> <p>在我看来,一个技术,如果带给程序员很大的心智负担,那也不能算好。按照Brooks,并且我也认同的观点,软件开发自身的困难未来都是可以解决的,而要解决的问题域所含的困难是本质性的,永恒的。一切软件技术的进步都要以解放程序员的智力、精力,使之可以更多、更好地投入到分析问题中去。这就是为什么我只推荐一个PRE_CONDITION的原因,也是我不喜欢繁琐而严格的命名规范的原因。G同事的回信中,他把contract库重写了,然后提供了大约几十个宏。这些宏和assert完全不同。有类似<em>ADP_CONTRACT_PRECONDITION_RETURN_VOID, ADP_CONTRACT_SIDE_EFFECT, </em>ADP_CONTRACT_POSTCONDITION_EX_MSG_ENFORCE这样的名字超长,不知所云的东西。特别是XXX_SIDE_EFFECT,XXX_RETURN这种的,摆明了是逻辑混乱的产物啊,怎么能这么干呢?SIDE_EFFECT,哼哼,成功了给个副作用吗,还是失败了给个副作用?然后信里面还给了两个DBC的链接,一个是Wiki的,还给我解释了一下什么才是DBC。我实在无语的很。那时,我已经接触和使用DBC好几年了,相关的文章也读了不少,他一个周末就可以在这方面指导我,然后弄出这么个奇葩的东西而不自知。这种自信是怎么来的呢?不管怎么说,G同事的回信就是决定。他写的那些宏,直到ADP结束我也没记住。我唯一死心的是,我不需要说怎么写异常安全代码了,甚至都不需要去提。事实上,ADP早期还用着异常规范呢,这可是2007年了。我后来还是忍不住,从《Exceptional C++》里面扣出几条,说明别用那悲催玩意儿了,这才把异常规范去掉。TMD,又做了一次语言律师!</p> <p>--回头看一下这篇文章,成了炫耀贴了。炫就炫吧,对上这些人,我还真有炫耀的资本呢。</p> </div> 梦断代码--一个程序员的自白 (二) http://icerote.net/blog/post/45 http://icerote.net/blog/post/45 <div style="direction:ltr"> <p>AIRMax是一个庞大的项目计划,需要3~5年的时间来完成。这个缩写是取公司的主打产品首字母和特征单词组成的,也意味着这个项目要影响到所有这些支柱产品。公司的这些产品都是不同领域的设计>软件。这个项目计划在三个方面做资源整合,即为所有产品提供统一的程序库或者框架:程序外观(GUI),渲染(主要是3D引擎),和文件格式(保存设计成果)。这些产品都是公司的现金牛,都很强势。这 就意味着AIRMax是一个极富挑战性的项目,而且需要高超的组织协调艺术才能成功。</p> <p>AIRData项目就是这三架马车之一,目标是打算统一文件格式和相关程序库。项目名称中的“Data”表明了项目的性质。后来又简称为ADP,是取Axxx Data Package的意思。但这个简称实际上要到一年后才被普遍使用。ADP项目美国那边有一位架构师总负责,和另外三位程序员。我们这边最初好像也是四个,有一个同事来了没几天就因为没兴趣而去了别的项目组。</p> <p> <a href="/blog/post/45">查看更多...</a> </p> </div> 梦断代码--一个程序员的自白 (一) http://icerote.net/blog/post/44 http://icerote.net/blog/post/44 <div style="direction:ltr"> <p><em><center>--当一个有价值的人或事物逝去,缅怀他的人便为之立碑,写下墓志铭。</center></em></p> <p><em><center>谨以此文献给Protein。</center></em></p> <p>看过一本同样叫做《梦断代码》的书,英文名叫《dream in code》 我总觉得翻译得不准确。 在那本书中,讲述了一个软件项目Chandler失败的前后经历,让人读来扼腕叹息。 然而今天,这四个字不断地在我的脑海里盘旋,仿佛是为我这六年来的经历下一结语。</p> <p>在AIRMax启动时,我来到公司尚不足一年。经理让我参加一个叫AirData(后来也叫ADP)的项目,我对此也是跃跃欲试。然而一开始什么都没有准备好,除了让我们看一些资料外,还让我们看了一个叫DWF的项目代码。没想到,这一看就让我工作在上面好几个月。</p> <p>因为当时已经知道ADP中将有一个很重要的部分就是管理property,因此,我也就重点看了DWF的property部分。当时,美国那边提出ADP的性能和内存占用是很重要的,还特别提到要考虑如何缓存处理property。从我知道的信息来看,已经知道ADP是读写一个本地的文件包,并管理一系列property map的对象。对于这样的系统,数据规模方面的信息也是知道的,我觉得性能低下是难以想象的问题,或者说是不理解。不仅是我不理解,我认为美国同事也没想明白问题出在那里。而对一个不理解的问题,设定了一个解决方案则是更不可思议的。在我看来,这根本就是软件设计中的大忌。可是就是这样的错误,在后来的日子里一犯再犯。</p> <p> <a href="/blog/post/44">查看更多...</a> </p> </div>