页面

分类

Posts in category ‘其他’.

How to Read an Academic Article

2014年12月21日星期日, by wingfire ; 分类: 其他; 0 comments

Author: | Peter Klein |

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.

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?

How to Read an Academic Article
  1. Caveat: no single style works for everyone!

  2. Klein’s basic steps for skimming, scanning, processing…

    1. Read the abstract (if provided)
    2. Read the introduction.
    3. Read the conclusion.
    4. Skim the middle, looking at section titles, tables, figures, etc.—try to get a feel for the style and flow of the article.

      1. Is it methodological, conceptual, theoretical (verbal or mathematical), empirical, or something else?
      2. Is it primarily a survey, a novel theoretical contribution, an empirical application of an existing theory or technique, a critique, or something else?
    5. Go back and read the whole thing quickly, skipping equations, most figures and tables.

    6. Go back and read the whole thing carefully, focusing on the sections or areas that seem most important.

  3. Once you’ve grasped the basic argument the author is trying to make, critique it!

    1. Ask if the argument makes sense. Is it internally consistent? Well supported by argument or evidence? (This skill takes some experience to develop!)
    2. 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?
    3. 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.
    4. 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.

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

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

Protein的性能瓶颈主要是在runtime,而runtime是基于FBX来写的。FBX已经停止开发,而且改动代价太高,因此,美国那边打算把把FBX从runtime中去掉。这是一个大工程。当时Protein的新经理M也刚来不久,我们都有点担心资源不够。这时,美国的架构师也忙于新的项目,也没时间给出Protein的新设计,中国这边就只好先做起来。在同事W的一再劝说和鼓动下,我决定努力一下,用息壤来替换FBX。下决心的根本原因是当时就觉得息壤说不定有机会替换ADP,那才一直是我最想做的事情。

从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还是立项了。这是我们第一次争取到一个本来很可能取消的项目,也是第一次,在我们这个部门内,由中国这边全权来做全部设计的项目。这对我们当然是个巨大的鼓舞。现在想来,之所以能拿到主动权,是因为我们做的早,美国那边还忙的不可开交的时候,我们已经为第二年的事情开好了头。如果没有息壤,也很难在这么短的时间里做出可以提供这么多性能测试的结果。

接下来的就是写文件包管理的部分,到春节前就基本上完工了。考虑到我们并非整个团队全部投入到4.0上,新的4.0能工作,并全部跑通,只花了整个团队1个半月的工作时间。这是我在公司所有项目中从未有过的速度。高峰时期,我们平均每个人一天可以提交四五次代码。虽然我们仍然会骂Protein API的设计很烂,会批评以前的实现代码为什么那么糟糕,但是我们还是尽力在允许的范围内做大最好。接下来的任务是通过回归测试,这花费了我们相当多的时间和资源。这是以前的设计中各种特例的恶果,还有就是针对实现和现状测试,而不是针对预期和设计来测试带来大量的虚假错误。为了通过那几百个回归测试,我们还是付出了相当多的努力的。到回归测试差不多都通过时,我发现因为前期为了能够表现给美国那边看,和M一味地赶进度,代码的质量很让人不满意。在兼容以前各种奇怪的逻辑和行为的时候,我们已经不得不引入了许多混乱和复杂性,如果我们不能把这些混乱和复杂性控制住的话,就会伤害到软件的质量和阻碍我们进一步的改进。代码改进越困难,我们就越容易丧失对代码质量的警惕性。我批评的第一个目标是单元测试。

在xirang中,我写了比较完整的UT,我想这足以作为如何写UT的范本了。但是,同事们完全没有正确地去写UT,即对每一个API,都要根据给定的输入,检测期望的输出。仍然按照以前的路子,以写功能测试和集成测试的方法写UT,既不在意测试用例的独立性和隔离性,也不在乎是否做了IO和执行时间。还是使用了我一再强调并反对的一杆子到底的测试方法,那会遇到执行路径的组合爆炸问题。测试数据也不是那种精心制作的,而是随便抓来的掺杂大量噪声的数据。当时的UT时间已经100多秒,而且还有快速上升的势头。我当时和M提出来,质量有问题,必须停一下,补上质量的欠债,否则后面的工作可能会遇到很多麻烦。但是M强烈反对,他打算投入更多资源去完善回归测试。这在我看来是及其低效和不可行的。当时还有一个开发X,也支持M,这让我很意外,也很难理解--我们过去的质量声誉并非很好,也为此饱受折磨。我们在之前的Protein开发中,单元测试最快的也要40分钟。我不能理解,为什么程序员能忍受?为什么不把这个看作是一种致命的错误?我是每次必抱怨的,从不掩饰。

4.0的UT执行时间必须被降下来,而且要降到1秒之内,这是可以做到的,也是我们必将做到的。我提出停止其他任务的要求其实是在漫天要价,也得到其余同事的全力支持,但我想得到的不过是制定质量改进的计划和任务,并且,至关重要的一点是,制定任务完成的验收标准。最后,妥协的结果是削减了一点其他任务,增加了质量改进的任务。但是关键的,为质量改进任务设定验收标准,M死活不肯。结果是一些人确实改UT了,但是换汤不换药。我又不得不自在完成自己的任务之余,再去改进那些有问题的部分。这个过程中因为触及了很多X写的部分代码,他可能因此对我很有意见。但是,我从把xirang带进Protein的时候开始,就多次说过,我们是集体代码所有权,xirang不再是我个人的代码,是所有人的,任何人都可以改动代码,无需先告知我。在改进过程中,我重写了X实现的AdpFs,和相关的archive。X的实现还是用ADP的api来操作的,不但慢,还有其他可靠性的问题,我直接用xirang里已经有的ZipFs来实现了。最后,我们终于把UT的执行时间降到了100多毫秒。

除了AdpFs,我还花了两周,把ADP中的DataStore类重新实现了。这时,除了残存在接口上的一些微不足道的类型(主要是String)外,ADP已经被彻底地从Protein中驱逐出去了,连带的,一个叫AdpPlugin的project也被我们彻底删除掉了。ADP花了三年时间,20多万行代码(不含TD的代码),数十个人年;xirang 2万3千行代码,数个人月,交付的功能比ADP还多。

UT的改进结果其实我是很不满意的,以改进UT为契机,反向推动4.0的设计和实现改进的目的也落空了。因为受不了恼人的复杂性和频繁的错误,以至于我们在后一个月又做了两个大范围的代码改进。效果不错,但是离我的期望还差距很远。其实我改进质量的终极目标是为Protein的下一版腾出空间。Protein的局限和危机是客观存在的,如果我们只是完成了预期的目标,那么来年被抛弃的命运是可以预知的。但是,我们又受到了很大的限制,不能放手施为。因此,在我的计划里,就是我们通过远远超前的进度,留下足够的时间,然后在今年多做一个版本。我设想的新版本是不改变文件结构,但是提供全新的模型和API。按照往年发布周期的惯例,我们有充足的时间来完成新版本。当今年正式集成时,我们将发布包含两套独立的API的Protein。我们不用刻意去宣传新版API,只需引导用户去选择即可,让用户的选择来为我们投票。一旦新API发布,我们就可以很容易做一个特别适合云存储的版本来。这样,就算抱上了公司云计算的大腿了。我们就有极大的机会和资本将xirang的技术推介出去。

然而经理M并没有这样的野心。虽然他也预感到Protein将来的地位岌岌可危,却缺乏为长远谋划的行动,而是安排了大量的资源和任务去做产品的预集成。这让我和一些同事颇感失望。ADP被彻底删除只是我个人的胜利,不是Protein的。Protein必须在API和存储格式两个方面取得根本性的变革,并拥抱Cloud才能生存。可是,因为我们的弱势地位,导致我们没法先先向公司抛出设想,然后争取立项和资源,再进行开发。我们成功的唯一途径是压榨自己的产能,先期做出成果,展示给美国那边看,然后才能获得完善的机会。一个可能的机会,正是我们前期高速运转的动力所在。本来,到6月份,我们就完成了所有的工作,只需一两个人处理修Bug,合并和集成的事情就足够。可惜,各种在我看来没什么价值的任务夺去了我们所有的时间。我认为M应该为此负责,并且他也为自己挖了一个坑。

接下来的时间,我们去做各种组件和产品的预集成--除非产品发布,否则这是个无止境的工作。给外界的映像我不得而知,但是我猜多半是认为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团队至此彻底烟消云散。这让我们觉得,过去大半年的努力不过是一群幼稚的程序员闹出的一出笑话。

尽管Protein已经结束,但是回顾过去一年的时间,我觉得需要特别感谢三个人。第一个是Jessica Cao,没有她的努力,我们中国这边可能不会有机会尝试做新版的Protein。其次是Scott Morrison,美国那边的架构师,他有出色的能力理解我们的设计和意图,也充分地信任和放权给我们。最后是Olivia Wei,没有她的一再鼓舞和劝说,xirang只会是我个人的业余作品。没有她做的大量文档化和解释工作,xirang也不会那么快被接受。Protein团队的其他人对我的支持和肯定,我也会永远铭记在心。无论如何,就我个人而言,做Protein 4.0的过程,是我在这家公司最难忘的一段经历,虽然中间并不轻松,结果也是个悲剧。

R.I.P, Protein!

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

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

因为最初我只是想证明我的那个可扩展的类型系统,因此我给ADPLite改了个名字,叫“息壤”,喻意类型系统会像息壤一样,能够生长。到11年夏天的时候,息壤已经有了一些东西:

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,一容器的方式提供迭代器对,避免不必要的容器复制。
......

虽然这些东西只是业余写写的,且断断续续,但还是严格地遵守了这些原则:正交分解,接口最小化,依赖倒置,区分方法和查询,Liskov替换,Open-close,异常安全等等。就这么丢了有点可惜,我何不花点时间把它写完呢?第一步,先清理这些代码。我删掉了一些练习和不成功的尝试,如一个STL风格的tree,可是最后却发现并不好用,关键是不能定制children的管理方式,这变得很鸡肋。一个奇怪的,编译期解析的配置文件读取类等等。一个完善现有STL迭代器分类的尝试。也保留了一些不完善,但是有用的东西,如range,tuple。写tuple完全是因为不想在接口中暴露boost,当然最后还是放弃tuple了,这其实是个很不情愿的决定。

在写息壤的过程中,有些决定很明智,有些则显得愚蠢。但无论如何,没有放松对质量和性能的要求。无论对错,我希望把这些内容记录下来,供借鉴或是批评。

  • DBC库的三个宏,最初的实现是支持搜集任意数量的错误点上下文的,但是因为对文本转换的支持不够理想而去掉了(在异常支持组件仍然保留了这一设计,但是实现手法并不相同)。用户可以通过一个额外的宏来控制是否生效而不仅仅是依赖Debug/Release编译宏。

  • Iterator本来有两个目标,一是擦除迭代器类型,二是实现新的迭代器分类,把遍历和读写访问分解。曾经有文章批评STL迭代器把遍历方式和访问性这两个正交的概念混在一起,导致一些棘手的问题,理想的做法是将之分离。因此读、写构成可访问性概念,遍历概念包括:可递增的,单遍,前向,双向,随即。我写出了将任意一个STL迭代器映射到新概念的实现,但是无论如何,都显得非常累赘和低效。要做的更好必须重度使用TMP(template meta programming),实现代码不但晦涩而且工作量也不小。这也和我TMP不够纯熟和缺乏支持库有关--boost不能用在这里,C++0x当时我还没打算用。最致命的是,如果我用这样的一个新概念STL,我就在STL和boost中步履维艰,许多必要的算法都不能用了。所以我最后放弃了,仍旧回到STL的iterator,但是优化了类型擦除时的性能。只要迭代器不是太大,就可以避免内存分配操作--这可以单独写一篇技术文章。

  • STL一大让人诟病的地方是,只有迭代器对,而没有提供封装(最多用pair,但没有与之关联的算法支持),要知道C++可没法返回一对迭代器的。所以,我用range包裹一对迭代器,这在返回一个范围时很有用。但是现在想起来,我对range的设计仍然不够好,这在配合内存读写时特别明显。我的range就是定位成一个半开半闭的迭代区间,这个有时过于抽象。当我看到GoLang的slice是,我才知道我错在哪里,并且应该如何弥补。go的slice还有个额外的属性capacity,这在复制数据到一个buffer时非常有用,slice的capacity可以允许安全地越尾写出数据。我应该写一个slice,用于内存操作相关接口的,而不是直接用range。

  • 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算法都可照常使用,和第三方库的交互性也更好了。

  • Archive是用来做io的。延续iterator的经验和正交分解原则,我把读写访问和seek分开了。数据访问有reader、writer。seek的接口有sequence(single pass),forward,和random。sequence实际意味着不能seek到一个新位置,比如标准输出。forward可以用于管道或socket,random则可用于文件。一方面,我让seek相关三个接口支持必要的查询,如当前offset,size信息(可能返回表示未知的值)。另一方面,尽可能简化,比如seek方法的定义:

      virtual long_size_t seek(long_size_t offset) = 0
    

在我看来,传统的fseek提供起点位置完全是罗嗦的。

接口的正交分解也不是没有代价的。C++的多重继承固然很容易组合多个接口,但是缺乏从多个接口中选择子集的能力。例如,我可能有一个实现了reader,writer,和random的archive类,还有一个函数接受一个指针,指针指向一个具有reader和forward接口的对象。C++中没有一种简易的机制让我描述该指针的类型(archive<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++鼓励的设计方式。)来做接口查询。这个设计的好处是简单,可行。坏处是缺乏弹性,僵化,丑陋。现在看来,我当时的决策太保守,应该更勇敢一些。这种设计也带来一些恶果。

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方法。

struct AIO_INTERFACE reader
{
     typedef buffer<byte>::iterator iterator;
     virtual iterator read(const range<iterator>& buf) = 0;
     virtual bool readable() const = 0;
     virtual const_view view_rd(ext_heap::handle h) const = 0;
protected:
     virtual ~reader() = 0;
};

Archive的实现类包括:基于文件,基于一个可交换内存到磁盘的heap,基于一个给定的buffer对象,共三组。还有一些adaptor。虽然数量众多,但是因为接口简单,每个实现也都很简单,总共也就800行代码。为了避免跨平台的麻烦,我是用boost.interprocess.file_map来实现文件archive的,这个file_map对windows平台的路径支持有问题,不支持wchar_t,只有mbcs。我不得不给它打了个patch,我看了最近的boost,好像到1.51也还有这个问题。还好,这个patch很容易打。

  • 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中出现的此类混乱和罗嗦的代码。

  • 对于采用utf8和单一路径格式这样的决定,并非都是模仿其他的范本作出的决定。软件设计通常应该要尽量推迟决策,这样会比较有灵活性。但是Ken Thompson说过:你总是要在某个地方hardcode。这在golang中很容易感受到这种设计哲学。一味地推迟决策也容易带来额外的复杂性。当然,后来在一些优秀的系统中看到相同的设计决策,就会额外地得到鼓舞。

  • 类型系统基本上是模仿C++的struct。因为要考虑到效率,空间布局和时间性能都是必须要非常紧凑。类型系统有基础类型和一个组合扩展机制构成。对基础类型定义了size,alignment,还有一个pod属性用来配合做对象初始化和销毁。对所有的类型,都定义了公共的方法,包括构造,析构,赋值,序列化,和一个typeinfo的handle,用来辅助检查绑定到C++对象时的类型是否正确。有了size和alignment,组合机制就可以像C++的struct一样,计算出每个数据成员的偏移量,也就可以直接访问了。组合机制还支持了多继承,这个纯粹是不想引起C++程序员的惊讶,所以没有坚定去掉的决心,后来实际上根本也没有用到过。从内存布局来说,继承和成员变量的方式并无区别。

类型系统还支持参数化类型机制,有点像运行时的C++template。这样,对于数组,就可以定义为一个带参数的基础类型,用到的时候只要和具体类型绑定就可以了。如果需要一个新的复杂数据结构,完全可以用C++写一个,然后作为基础类型加到类型系统中。这是很有用的,因为类型系统并不是ADT,不能随意增加方法,C++则很方便加方法。如果不需要增加复杂的算法,也可以直接在类型系统中进行扩展。两种方式在息壤都有使用。

类型系统处理的不好的一个地方是,其上有哪些方法是预定义好的,很难扩展。除了构造,赋值这种基本操作外,也可能需要比较,hash这样的方法。Java中的做法是在Object中一股脑加进去,这是我不喜欢的,因为不是所有的数据类型都一定可比,即使可比语义也未必也一样。我希望的是有一种自由的方式给某个类型附加任意多的方法。方法表似乎是出路,但是hash表的代价太高,我认为不可接受,数组的排序问题怎么解决以及查询机制又是个问题。怎么做最好我到现在也没想到,只能留着慢慢来。但不管怎么说,即使扩展方法的能力欠缺,但是扩展数据类型的能力也是意义重大的。在可以预见的未来,类型扩展机制够用了。这种用基础数据类型装配出新的类型,新的类型有和基础类型一样成为装配更新的类型的材料,仿佛可以自我增殖一样。正是这种特性让我联想到息壤,传说中可以生长的泥土,我也因此用息壤来作为项目的名称。

考虑到类型定义可能是一段手写的文本描述,那么手写的简洁性是必须考虑的,另外,还要解决类型名称冲突。为此引入了别名机制(alias),和命名空间(namespace)。有了namespace,就可以用完全限定名称来唯一标识一个类型。这个类型系统在我看来还有一大缺陷,是参数化机制引起的。在C++中,Array这样一个类型的名字是用某种拼接机制产生的字符串,参数很多或这嵌套很深时会导致很长的类型名字,这是极其让人讨厌的。另外,给两次Array产生两个类型也很让人蛋疼。如果每个类型都有一个确定唯一的id那就好了,但如何产生这个唯一的id很是头疼。我想过对类型的成员、属性做sha1或md5来产生id,但是如何处理碰撞呢?虽然碰撞的概率接近于0,但毕竟不是0啊。这个问题也是到现在也没有去解决的。

还有另一个和实现有关的问题也很让我闹心。息壤有一个运行时系统管理所有的类型,类型之间显然会存在依赖关系。那么我需要在很大程度上保证用户不会建出一个错误的类型来,也不能让一个建到一半、未完工的类型保留在系统中。当时我还希望把类型的描述通过一个数据结构提供给息壤,然后息壤一次性处理这个数据结构,如果失败,就要指出数据结构中错在哪里,并且对系统状态不能产生影响。看上去这个设计非常简单,但是实现起来却变得困难重重。我反复实现了三次,每次都不能让我满意,而且使用的时候,设置那个数据结构也很罗嗦繁琐。没办法,我决定先放弃那个数据结构和错误报告机制,先以最简单的方式完成功能再说。但异常安全是不能放弃的,因此,我写了一个transaction类,负责完成所有的数据building的工作,并在失败时回滚撤销。这样一来,transaction类又变成了一个大杂烩式的肥大类,充满了各种操作。但是transaction也还是有价值的,它把复杂的构建充分分解成了一系列充分小的操作步骤,这样每个步骤的调用点就只会依赖类型描述的一个数据点--不管这个描述是数据结构还是一种语言文本。这种推卸是可行的。我后来在写第一个demo时,很容易就在解析器中做了错误报告,根本无需依赖息壤。

之后,transaction改进的方案似乎一下子就自动浮出水面了。第一个transaction依赖息壤运行时的问题。在实现transaction的时候发现,transaction其实只在最后提交的时候才需要和运行时打交道,也就是说,我很容易创建一个完整的,但是没有登记到运行系统的类型。这是测试需要的,意味着我可以把一个类型对象隔离到更小的范围了,而无需像最初那样,为了得到一个测试对象,满世界都要被惊醒。另一个是transaction的那些方法很容分组归类,自然地,应该被拆分成多个class。自然地,先用builder模式创建对象,再在事务中提交类型对象到运行时就水到渠成了。transaction被分解成了若干个互不相干的builder,彻底解决了长期让我难受的问题。

虽然只能在工作和陪孩子之余才能有时间写代码,也想尽快地把息壤弄完撒手,但我还是给代码定下了--或者说坚持--质量优先的原则。在设计方面,主要坚持如下原则:

1.Design By Contract
2.基于组件
3.方法/接口正交分解
4.接口最小化
5.区分“方法”和“查询”
6.“方法”的效果是可观察的(通过“查询”观察)
7.异常安全
8.支持设计演化

当然,还有一些广为人知的原则是必然坚持的:KISS,DIP(依赖倒置),DRP(不要重复自己,Raymond称为SPOT,真理的单点性原则),SRP(单一职责)。MVC也是也别注意的。

对于质量实际上体现在两方面。一是设计质量,二是实现质量,当然两者也是相互依存的。特别是设计质量,很大程度上依赖于那些设计原则坚持了多少,另外单元测试也非常重要。成熟的程序员应该主要把写单元测试看作改进设计的手段,检测实现那只是副产品。对于实现质量,我认为单元测试是最经济高效的办法--当然,设计必须支持测试。很多开发人员对测试没有清晰的认识,分不清各种测试的职责和价值,更不要提写容易测试的软件了。测什么?怎么测?以及什么样的系统才是容易测试的?这不仅是测试需要关心的,开发一样要关心。对于开发来说:1.测的是接口,接口除了函数原型外,就是行为。而行为可以通过输入、输出的契约来描述--虽然可能有些契约无法用编程语言描述,甚至很难形式化。把对行为的关注转移到对契约和参数的关注上来,就是从面向实现编程转移到面向接口上来。这种转变实际上关乎到测试的能行性。2.给定被测试对象一个输入数据,然后观测系统的状态,检查是否符合预期。如果遵循设计原则中的5和6两条,那么这种测试方法就是可行的。3.符合2、3、4设计原则的才是容易测的。测试的一个基本要求就是能将测试对象孤立甚至是隔绝出来。如果给测试一个对象一个输入,牵一发而动全身,整个系统,无数的状态改变,又如何用有限的资源完成测试呢?一个理想的系统,测试代码量应该和接口函数的数量之间保持线性关系。另外,创建或初始化测试对象,或者说测试的数据准备工作应该非常便捷且廉价。我对息壤中比较基础的,重用很多的部分测得比较完整,因此也几乎没出过什么问题。

另一个重要的问题是性能。我认为很多人没分清性能和优化。软件的整体性能是很难通过优化解决的(老实说,我从未见过成功案例),优化只能改善局部性能问题--通常以复杂性为代价。性能必须是从初始设计开始就要关注并且持之以恒地维持的。我认为通常有两类性能问题,一是追求单位时间的最大吞吐量,另一个是追求响应速度。两者都很常见,是截然不同的两类问题。大部分情况下,性能问题的解决方案能同时改善这两类问题,典型的例子是算法改进。但是有时也会冲突,例如,为了解决一个响应时间的性能问题,有时可以采用并行计算来加速,或者把计算的某些部分延迟到响应之后。这些措施都存在额外的开销,这将降低系统的最大吞吐量。

我非常厌恶ADP中那种错误的性能观点-比如喜欢用memcpy,原生数组等等-但仍然注重性能。首先是在接口设计上就要充分考虑,避免抽象惩罚,archive中view的设计就是一例。借助于C++的能力,让息壤的抽象惩罚主要表现在编译期,而非运行期。解决性能问题要靠设计,要落实到数据结构和算法上。Brooks曾经说过大意是这样的话:你藏起数据表,给我看流程图,我还是不知道你要说什么;给我看你的数据表,啊,我不再需要你的流程图了。另一个忘了出处的话是:算法是流动的数据结构,数据结构是凝固的算法。还是在ADP项目的时候--当时有人在鼓吹算法的力量,这当然也没错--我就曾经对两个同事说过:如果你不曾纠结于数据结构的设计和选择,那你还算不上是真正的程序员。我说这话是因为看到太多的人,太多的代码,肆无忌惮地往class里面塞成员变量,有时候,仅仅是用来代替传参数给某个成员函数,还有的时候则仅仅是当作局部变量用。另外,一个状态或者属性,反复地出现在多个class中,而对可能造成的不一致性视而不见。怪不得有人说学过数据库设计的人,做出来的OO要好的多。我开始赞成所有使用数据抽象技术的程序员都应该接受范式的思想。Protein在这方面很糟糕--虽然还算不上最糟糕的。

接口设计也会关乎性能问题。不止一个项目中看到链表类提供下标访问,全然不顾这是个O(N)算法,用户用这样的链表,很容易就会不经意间写出O(N2)的算法来。我亲眼目睹过许多这样O(N2)的代码。少就是多,多反而会坏事。任何时候,当不得不选一个降阶的算法时,都不能轻易屈服,你今天对数据规模所作的种种假设,明天就会过时。任何时候都不要以“不成熟的优化是万恶之源”来反驳算法复杂度阶的变化(常数项不予考虑)。我在写息壤的过程中持续关注性能的方式就是关注复杂度。我的实践也表明,关注复杂度可以解决绝大多数性能问题,项目规模越大,复杂度就越重要。我认为,任何时候,改进复杂度的阶都不能算是优化,而是修Bug--设计或实现的Bug。把具体的实现问题正确归类到已知的算法的能力也是重要的,而不是随手写。比如我见到很多合并两个已序的序列(例如合并两个std::map到一个数组中)实现为O(NLgN)而不是O(n)的,在已序序列上遍历而不是二分查找的,不一而足--这种做法真的该打屁股。不要说C++算法库已经提供了可以直接调用的库函数,就是没有现成的函数,也应该自己按照算法写一遍。也许我孤陋寡闻,网上很多人喜欢谈论和关注的算法、数据结构,大多是特定于某个领域的,在某个点用过了,也就用过了。正经是排序,查找,二分,归并这些基本的算法像吃饭喝水一样,每天都要用到。数据结构也是,数组,链表,二叉树(C++主要就是map,set),hash表这些才是每天要用的。就连数组这样基本的东西,有些程序员仍然对其性能,内存,错误处理等等不甚了了,真是让人大跌眼镜。

2011年,在Protein为了改进性能和支持新特性,我主要参与两个部分。一个是文件存储格式做了改变,虽然还是使用ADP保存到OPC格式,但是主要的数据不再保存为XML,而是二进制。另一个是引入了Schema的概念。这两部分都带来了大量的不稳定性。Protein的新格式保存出去的数据格式是按照Schema的描述来的,并没有自描述部分。Schema因为没有版本,因此遇到版本升级、修bug、同名合并等问题,其内容并不是稳定的。一旦Schema和二进制数据不精确匹配,就会导致在加载数据是崩溃。这个问题我们也是在一开始就提出来过,也是一样被告知不会发生,接下来的故事就是历史的重复,而且因为一旦出问题问题的后果特别严重,特别难定位问题原因。幸好,大部分时候不是我来修这样的bug。

到6月底,当年的主要开发工作结束,然后就是集成。7,8月份,在和一款产品集成时(不是全新集成,只是程度更深),出了严重的性能问题。产品那边给出的接受标准非常高,只能是两边共同做优化。有段时间每天就是profiling,找热点,调整,等产品的反馈,然后继续。我做了两个优化,一个是修改FBX内部实现,对性能改善效果非常显著,但是读代码和修改时相当痛苦。另一个是改进Protein的引用计数机制,读代码那就不是痛苦了,而是想死的心都有了。改完了引用计数,还要改许多hack了引用计数的地方。总体上,我认为产品的优化对最后性能的改善作用更大些,但Protein确实也对性能做了巨大改进。

到5,6月份,我把息壤也整理的差不多了,代码有2万多行,仅仅是ADP的十分之一。我没有精力也没有兴趣以息壤为基础重新实现一个Protein,但是又不甘心就此让息壤消失。于是写了一个demo,和一个很简陋的PPT,作为知识共享,介绍给了公司的同事,想以此作为一个了结。当时Olivia同学一再鼓动我,让我把息壤的文档弄好,推广出去,但我已经毫无动力了。

当时公司决定全面拥抱云计算,中国这边的研发当然也是想方设法做一些原型,来被美国那边看重。我当时被要求做一个材质共享的原型。所谓共享,就是在一个程序可以从服务器上在线获取材质的数据和图像,如果程序新建或修改材质,保存结果到服务器上,另一台机器上的程序就可以浏览到最新的数据,直接使用。这个过程要求不需要显式的下载过程,必须是和使用本地材质库差不多的方式。服务器则使用公司自己做的一个云存储的服务。最后给了我们大概两三个星期的时间。

显然Protein的数据包格式不能满足要求,而两三周内在原来的ADP上改几乎是个不可能的任务。因为是原型,我们只想尽快把东西做出来。如果不考虑ADP,Protein只需在IO部分,把存出去的数据重定向到网络服务器就可以了。息壤的VFS来做这个任务是非常简单的。于是,我决定用息壤来做。首先是花了一周把息壤从Linux移植到windows。另一个同时包装了一下云存储的几个功能给VFS用,然后我就给这个云服务实现了一个VFS,最后修改了一下界面。结果,那个云存储的速度慢到无法忍受。于是决定自己搭一个本地服务器,不用云存储。这时,息壤的表现非常棒,虽然加了几个小时班,还是在一天之内就弄完了。性能不是问题,用来演示是足够了。

我本以为息壤的命运也就到此为止了,但是没想到,事情又有了转机。

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

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

大约是各方面对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项目。合作者也换成了加拿大的同事。

在产品集成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会四舍五入,所以舍入后方向并不确定的缘故。这种问题对某些软件来说完全不重要,对另一些软件来说,则是至关重要,因为它影响到数据正确性。我们公司的软件正是后者,多是用来做各种设计--也就是创造数据的。

另一个我认为很重要的数据完整性(包括正确性)问题,则从未得到重视,或者说从未被理解过。在我看来一个很显然的事实是,对于ADP、Protein要处理的任何外部数据,都要持恶意假定,即可能是任何形式的坏数据。在此情形下,在处理到任何坏数据时,都不能出现崩溃或其他的致命错误(如内存耗尽,死循环等等)。即便是安全性错误也是要竭力避免的,如临时文件漏洞,缓冲区溢出等等。对于保存数据,ADP采用了创建--改名的机制,虽然很慢,很笨拙,却也有效地避免了数据完整性问题。就地修改的特性,且不采用回滚日志,最后也没做出来--幸好没做出来。时至今日,ADP、Protein在读取到坏文件时,几乎100%会发生故障,安全性问题更是没有任何评估。然而就这样的东西,现在居然有人将之部署到网站,用来处理用户提交的数据文件!这是打算引诱人犯罪吗?

当被自己预言过的问题坑了的时候,我不会觉得自己英明,而是为自己的无能为力感到羞耻乃至愤怒--愤怒不过是无能的另一种表现形式罢了。乃至于到我快离开ADP时,还给了我一个几乎让我暴走的任务:为了避免阻塞在费时的ADP保存数据操作上,要让写出过程是异步的,用多线程解决。要知道,这是ADP的运行时太慢导致的,即使我异步处理,仍然要读ADP运行时数据,而且为了保持数据一致性,就必须要锁住ADP系统--ADP可没有快照机制--直到保存数据操作完毕,那么要异步何用?这个任务我拖了好几个星期,并且被告了一状。最后说我临时先写一个,将来会用一个多线程IO的东西将之替换掉。没办法,我只好去做那个有害而无益的任务。在后来的Protein中,我不知道有多少次遇到多线程IO上出现的bug,而且,每次定位和修复bug都要花费好几天的时间。唯一值得庆幸的是,我写的那个临时方案确实很快被替换掉了,我不必为此背负骂名。

Protein团队的组织有点松散。我到了Protein长期处于无事可干的状态。在完成了Property和PropertyTable的实现之后,发了两次邮件请示工作无果后,我也就懒得关心,看看代码就是了。Protein由多个模块组成,我们的主要工作是其中一个dll,即提供对材质库的访问和管理,以及为材质对象建模。Protein API的风格应该是受Java或者.Net的极大影响,全部设计为接口,整个库导出一个唯一的全局函数(GetIAssetLibraryManager())作为入口点。因为没有导出任何实例类,所以一切的工作都要从这个library manager对象出发,逐步展开。对于这样风格的API,如果设计的好,未尝不可。但不管怎么说,这样的系统问题也是很明显的:

1.难以获得语言的支持和配合。C++鼓励扁平化的类设计,为什么?说白了就是对非扁平化的设计支持不好。
2.从依赖关系上来说,library manager依赖整个系统,任何使用了library manager的地点,都隐含地依赖整个系统,这回导致系统僵化,难以重构。
3.无法直接创建任何一个接口的实例类,必须从library manager出发去创建。而library manager又不是factory,因此,任何一个组件都无法从系统中单独拿出来考察。这意味着组件隔离做不到,按需创建测试目标代价高昂,单元测试将非常困难和低效。

除了两个给用户的扩展外,Protein的每一个接口,实际上只有一个实现类--就像Pimpl的接口和实现那样。然而,和Pimpl不同的是,Protein的接口和实现类的功能不是一一对应的,接口功能只是实现类的一个子集。这就导致一个很大的问题,在Protein的实现代码中,拿到一个接口指针,第一件事就是Downcast成其对应的实现类对象。这是典型的抽象不足的表现。然而,我们是缺乏API重构的权限的。事实上,大多数项目都缺乏此权限。我当然理解API变化会影响客户代码,但是我不明白,为什么在项目早期也不鼓励改进API呢?我坚信,没有人可以在不写实现代码的情况下就设计出足够好的API来。

另外,因为完全依赖C++的接口,在C++中就不可避免地要返回接口指针,如何维护这些返回对象的生命周期就是要慎重对待的。当然,如果有统一的约定,情况还好些,可惜Protein并不统一,所幸问题到也不很严重。Protein的实现完全是构建在FBX之上的,因此了解FBX也是必要的。FBX当中有一些质量不错的东西,但是也有大量不可理喻的东西。我不了解FBX的历史,倘若FBX最初写于1995年前,我觉得这就是一个不错的东西,如果写于2000年,那就平庸得很了。这让我想起AutoCAD的2D引擎Heidi。很多人对Heidi表示不屑,但是如果考虑到那是在80年代一个人搞出来的东西,就不能不表示尊敬,Heidi达到了它那个时代业界的最高质量标准。即使放到今天来看,Heidi所表现出的简洁性、一致性、严密性仍然是值得称道的--尽管已不符合当今一些人的口味。

除了接口,Protein的实现质量在我看来也是很糟糕的。各种special case--我就不知道有啥不包含特例的--对我这样的人来说,记忆这种逻辑不明的东西就是个噩梦。大量的代码逻辑复制粘帖自FBX,以至于读代码是完全不知道在干什么,性能也很糟糕,各模块的交叉依赖简直是做到了极致。另外,Protein还附带了一些渲染处理,界面工具,而非仅仅是一个数据模型和包管理。这种混乱的层次关系也让我对Protein很是不喜欢。Protein唯一值得表扬的就是,它确实做了一件用户需要的事情,用户也确实在用。因此,我认为无论如何,Protein比ADP强。特别地,当有人拿ADP和Protein做比较,说Protein质量差时,我都忍不住要反驳:Protein很烂,但是有人离不开它,他有用;ADP不那么烂,但是所有人都不想要,恨不得扔掉它。至少从我的角度来说,我希望ADP彻底消失。一来可以使用了它的那些软件免于腐蚀,二来可以掩饰我的失败。

Protein所谓的材质(Material),实际上是一个属性集合对象(差不多是这样,Protein管它叫Instance)不同属性可能有不同的数据类型和值,某些属性还引用了另一个Instance。Instance中的属性值有些可以改变,有些不能改变。Instance不能添加或删除任何属性,因此,每个Instance都从一个模板--叫Definition--clone出来的。可诡异的是Definition没提供接口去访问其中的属性,要想查询,先clone一个Instance吧!想修改,没门。到了2011年,我们还搞了个叫Schema的东西,专门定义Defintition/Instance有哪些属性,属性的数据类型,默认值等等。这种三层的结构实在是让人头疼到爆。然而,我们无权更改API。

我不想再去吐槽那些让人抓狂的错误设计,毫无营养可言,回忆它们也不能从中汲取任何有益的教训。我每次在代码上受到挫折时,就到那个ADPLite上写一点代码,舒缓一下心情。然而写不了几天,我就又要开小差了--我都不在ADP干了,写那玩意儿干啥--去尝试和验证各种新鲜的想法,甚至就是学习。然而,在这断断续续大半年的时间里,我发现也积累了一点代码。我厌烦了牵挂这么个东西,想做个了断。

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

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

我对公司有一个很不满意的地方,就是缺乏像样的职业培训。我来到这里之后一直是传授东西给别人--当然我对此并不抗拒,还很乐意--自身的水平却没什么提高。于是我打算还是学点东西。我把业余的时间都拿来玩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”。

既然是学习,就要做点功课。这次还是认真地比较了一下几个发行版,决定从最符合开源精神的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终于上线了。早干嘛去了?

搞LFS的过程比教程上的还要顺利,因为我使用最新的源代码,教程上需要打的patch大部分实际都已经fix了。因为我目的是为了搞ARM,所以接下来要要搞一个交叉编译的平台。我不想用开发板自带的交叉编译系统,太老,而且不能随时升级。另外,似乎怎么搞一个交叉编译系统的文章也不大靠谱,大多只是搞个别环节。这也可以理解,完整的工具链还是有点复杂的。所以CLFS就是必须学习和完成的。还好,CLFS对我来说反而更顺利和快捷,除了Perl遇到了一点问题。接下来,就可以用这个CLFS去build一个ARM的Linux系统了,这个步骤又和LFS几乎是差不多的,之前写的脚本就节约了大量的时间。所以后面的CLFS+ARM只用了1周多一点就完事儿了。这时Build出来的ARM linux还不包括内核,只是可以chroot切过去,然后可以用最新的工具链。这个过程的失误也不少。第一是我应该装一个QUMU虚拟机的,而不是直接上到ARM板子上,这后来浪费我许多时间。第二是我应该及时做笔记,把整个过程记录下来的。

接下来,就是要给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,可惜我最终还是没能成功。

Bootloader有了,Linux系统也有了,就差Linux Kernel了。其实U-boot的代码也是从Linux Kernel里剥出来的,我当时用的是1.6,但是kernel好像我已经用2.6.28还是32了,驱动部分的代码不太一样了。所以硬件部分的修改要再次找出来,改好。有了U-Boot的经验,这次倒也算轻车熟路,况且Linux对2440的支持还不错。当然现在的支持更好了,我有看到编译Linux kernel时可以直接指定mini 2440板子了--以前只能选S3c2440芯片组。

我在做整个LFS,CLFS以及ARM的构建时,只要可能当然是优化全开的。不知道是不是得益于此,当我第一次完全地在ARM上启动到全部亲手构建的Linux后,其性能还是让我很振奋的,非常快。后来又裁剪系统,塞到NAND中(用的是Ext2),那种搭积木的感觉真是爽快啊。在玩ARM过程中犯的错误不谈,遭遇的挫折和困难,知识的缺乏,学习之枯燥,直至后来用JTAG调试系统,比ADP不知道恶劣多少,可是却一直能兴趣不减,结果也很不错,收获颇多。可是为什么ADP就那么令人绝望呢?

有了一个就手的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,而且很久才爬一次!当时就一个很好玩的想法是:布置个应用,然后给爬虫喂误导性的数据,甚至有目的地诱导爬虫,会怎么样呢?

虽然发现自己有太多硬件和系统内核方面的东西要学,可是又觉得要学就要有个结果,而那显然不是一个短期计划可以见效的,也就没有继续。LFS也有自己的问题,就是什么软件都要自己build,这作为一个固定功能的平台是够了。可是对于一个抱着折腾目的的人来说,这是不够的,能装上Debian才行。可惜我一直都没能把Debian移植上去--当然是从kernel开始的那种移植,让U-boot支持远程安装Debian自然是更加没影的事儿了。现在想来,我有点一根筋了,chroot一个Debian也足够用了啊,干嘛一定要和kernel较劲呢?如果把那时间用来研究UML(User Mode Linux)和LXC(Linux Containers)多好啊!这样我就可以放心地布置几个蜜罐,研究一下了。可惜梦想很丰满,现实很骨感。

既然hg的代码仓库建起来了,免不了要开几个项目,写点代码练习一下。写点什么呢?想到那个让我不爽的ADP,我在ARM上建了一个叫ADPLite的项目,试试看,能不能一个人把ADP干翻。

下一页