Shell's profileShell's RoomPhotosBlogLists Tools Help

Blog


    1/20/2009

    24点计算原理和程序

        最近开心上狂算24点,于是贝壳搞了一个24点计算程序,并且说明原理。
        我们将24点问题一般化,变成一个搜索问题。假定从一个初始表开始,里面有一些原子。我们定义一个操作,结合。每次操作任意从中选择出两个(或者以上)原子,使用算符连接,成为一个新的原子。那么,一般来说,24点就是计算所有可能的路径,从初始表开始,持续进行结合,直到只剩下一个原子,并且对这个原子求值得24。
        有人可能在算符优先级上想不开,其实不用考虑这个问题,每次求值的时候,按照求值顺序优先就可以。你想到的另外一种优先级可能,会在穷举的时候被列举出来算掉,不用担心遗漏。
        同时,算子必须是两目以上算子,因为单目算子可以持续作用于同一个对象,因此原子表中的原子个数并不严格单调减少,造成无法肯定路径收敛于有限步骤上。并且,如果允许单目算子,那么我只需要求导和阶乘就可以对任何数字求24点。
        ((a')!+(b')!+(c')!+(d')!)!=24
        因此,单目算符是没有意义的。
        另外,注意算符分可交换和非可交换的。例如:a+b=b+a,但是a-b!=b-a。如果不注意这点,倒是不会漏算,但是会造成搜索空间增大,并且有重复结果。
        以下是24点计算程序,python版本的。有兴趣的朋友可以用scheme重写,相信会更简洁有效。回头会用django封装一下,做成网页给大家玩玩。

    #!/usr/bin/python
    import sys

    symbol_list = [
        ("%s+%s", True), ("%s-%s", False),
        ("%s*%s", True), ("%s/%s", False), ("%s**%s", False),
    #    ("min (%s,%s)", True), ("max (%s,%s)", True),
    ];

    def diff_seq (length):
        for i in range (0, length):
            for j in range (i + 1, length):
                yield (i, j);

    def get_less_state (input_state):
        for i, j in diff_seq (len (input_state)):
            temp = input_state[:];
            del temp[j];
            del temp[i];
            for s in symbol_list:
                rslt = s[0] % (input_state[i], input_state[j]);
                rslt = "(%s)" % rslt;
                temp.append (rslt);
                yield temp;
                temp.remove (rslt);
                if s[1]:
                    continue;
                rslt = s[0] % (input_state[j], input_state[i]);
                rslt = "(%s)" % rslt;
                temp.append (rslt);
                yield temp;
                temp.remove (rslt);

    def do_node (input_state, output):
        for i in get_less_state (input_state):
            if len (i) > 1:
                do_node (i, output);
                continue;
            try:
                rslt = eval (i[0]);
            except:
                continue;
            if rslt == 24.0:
                output.add (i[0].replace (".0", ""));

    if __name__ == "__main__":
        rslt = [];
        for i in sys.argv[1:]:
            rslt.append (float (i));
        output = set ([]);
        do_node (rslt, output);
        for i in list (output):
            print "%s=24" % i;

     

    1/12/2009

    论同时的双系统--准虚拟对双系统的进一步扩充

        熟悉贝壳的人都知道,贝壳是个linux爱用者,不过因为工作关系,经常要使用windows。贝壳在自己的笔记本上使用了linux/windows混合双系统,并通过共用磁盘的方式共享数据,解决这个问题。但是长期的使用表明,这种解决方案存在几个巨大的瑕疵。首先是系统切换时间常,因此长期在一个系统中工作,而很少触及另外一个系统。其次是稳定性差,windows下一旦崩溃,进入linux后就需要检测数据盘,80G的数据慢慢扫描,感觉晕到死。那么是否有一种方案,能够同时使用两个工作级系统(注意,不是实验级,贝壳成功的在windows下的vmware里跑了一个oracle,这个可以说是实验级的典范。然而工作级系统的要求和实验级完全不同)。
        从系统发展史的角度来说,我们可以预见,将来的系统将是脱离硬件的。首要的原因就是和硬件不相匹配的各个层级的计算能力需求。现在系统发展有两个极端,一个是虚拟机,试图将一个硬件整体分离,运行多个系统。另一个是高性能集群,试图将多个硬件合并,运行一个系统。从根本上说,这是因为高性价比的硬件集中在了一个性能区间,而实际的性能需求却是完全分离的,因此我们才会出现如此两类完全背离的需求。而现在有大量宝贵的人力浪费在了系统和硬件结合,系统稳定性问题上,这无疑是对将来发展的一个巨大瓶颈。虽然无法预知将来的技术发展会以何种方式解决这个问题,然而可以预见的是,解决硬件和性能的背离将是人类计算机发展史上一个重要的里程碑,解决这个问题的人必定会在计算机历史上留下重重的一笔。
        同时,更进一步,贝壳揣测,将来的解决方案将是系统硬件调度/驱动和系统软件管理分离。一个软系统拥有一个用户表和一个硬件表,硬件表上写他可能有10个键盘,两个显示器,或者一堆其他设备。系统借助某个可信方案,管理了一系列虚拟抽象设备和真实设备形成的映射。作为系统层以上的软件,我们只要关心如何操作这个虚拟设备即可。而实际上,我们可以通过管理参数和对应关系实现各种需要。例如我们可以将多个机器的硬件管理核心加入一个系统,形成集群。或者我们可以在一个机器的硬件管理核心上加入多个系统,形成虚拟机。这个基本是分布系统的观点。如此一来,系统层软件就无法得知也无需得知自己是在到底运行在什么环境下。只是这个系统设计方案对高性能要求的子系统(主要是显卡)相当不利。
        从揣测回到现实,为了实现一个工作级系统(幸好,还不是工业级),我们需要为系统制定一些评判标准,以判别各个方案的优劣。我们首先能想到的评判标准就是速度,一个慢吞吞的系统解决方案是没有任何实用价值的。当然,这个速度是有差异的,可能是linux快一些,windows慢一些,或者相反。我们假定实际的需要是windows快一些,因为linux可以通过定制进行加速。
        我们的第二个评判标准就是稳定性,经常会崩溃的系统不比慢吞吞的系统好到哪里去,甚至会更加让人讨厌。虽然工作级系统并没有工业级那样高的要求,然而高负荷稳定,宕机平均频率低于3天/次还是要保证的。而后我们还希望两个系统可以做到数据互通,即两个系统间的数据尽可能的共享,至少要做到文件和邮件的共享。最后,我们希望解决方案简单易用,便于实施和维护。
        而后,我们列出了一个原始方案,和以下几个改进解决方案,并给出优劣评价,谨供大家参考借鉴。同时我们在其中还补充了一些无法实际解决问题的虚拟化解决方案,并且说明无法使用的原因,供适合的人自行选用。
        原始方案,windows+linux+数据分区。此种方案是最中规中矩的,性能最高的方案。具有对硬件最好的支持,最容易的维护。如果需要运行游戏(尤其是魔兽,WOW),这也是唯一可行的工作级方案。稳定性评价属于尚可,主要由于ntfs在linux的稳定性并不好,ext3在windows需要使用非官方驱动,和某些(就是avast)驱动不兼容。数据互通比较方便,通过数据分区可以轻松的共享文件和邮件。
        windows虚拟方案,vmware+虚拟分区。这种方案是改进方案中唯一可以跑游戏的,因为虚拟机随时可以关上。性能上满足windows快linux慢的要求,虚拟系统显示性能良好,也可以通过文件共享部分的解决数据共享问题(文件共享方便,邮件共享困难)。稳定性很好,基本没有什么不稳定的问题出现,操作和维护都不困难。然而之所以一开始这种方案就被排除在外,主要是因为这种方案无法让linux驱动实体硬件,无法通过机器启动。这样也许对一些跑起来玩玩的人或者是内核工程师/测试员比较有用,然而如果要在linux里面进行大量工作,编译程序,运行服务,这种方案就力有未逮。因此这个方案可以说是一个实验级方案,而非工作级。
        windows虚拟方案,vmware+实体硬盘。速度一般,windows快linux慢,基本和上面一个方案一样,唯一的区别就是linux也可以被实际驱动。然而这也成了整个方案的最大败笔,因为linux的驱动灵活性不如windows,因此无法经受这种系统切换的动作。举例来说,真实的机器上,硬盘是sata的,作为sda识别和使用。而虚拟机上则是IDE的,被识别成了hda。于是启动环境一变,就需要修改大量配置来调和这个问题。又例如,在真实机器上,X使用fglrx驱动,而虚拟机下面要用mesa。如果我在/etc/xorg.conf中不指定驱动,那么真实机器的驱动也会变成mesa,导致性能下降。如果指定驱动,又会导致虚拟机内X无法运行。诸如此类的问题林林总总,需要大量细节修正,因此维护复杂,稳定性差,不建议正式使用。在贝壳机器上更严重的,出现了虚拟机内和虚拟机外争抢数据分区的状况,这种情况下数据分区实质是被当做盘阵用了。使用非专用的磁盘作为底层共享存储,并在上面运行ext3系统,这是及其危险和愚蠢的。
        linux虚拟方案,xen。速度超快,但是上来就在贝壳的机器上暴出几个问题,因而没有继续测试。首先是安装xen后x无法启动,出现fglrx驱动无法加载的状况。其次是xen要求使用虚拟盘启动,可贝壳经常需要跑到windows下面打游戏。因此在简单测试后被剔除出局。感觉这种方案的最大问题在于配置管理太过复杂,debian下面已经很轻松了,只需要安装对应内核,使用工具建立虚拟机,但是依旧感觉麻烦到一塌糊涂。相信这种方案在专业级服务器领域应当有不俗表现。
        linux虚拟方案,openvz。这种方案压根就不适合贝壳的状况,因为这个虚拟方案要求宿主和客户必须是同一CPU同一系统(不要求同一linux发行)。主要用于希望将一个主机切分成多个独立的同构主机,以达到分离管理的目的(例如业务服务器和数据库服务器分离)。需要做大型网络管理/虚拟主机业务的人可能会对这个虚拟方案感兴趣。
        linux虚拟方案,vmware。速度一般,linux快windows慢,视频效果不错。vmware毕竟是商业公司,视频驱动挺齐全的。但是内核驱动的编译麻烦到死,首先是要求编译器版本和主内核编译器版本一致,于是贝壳去搞了个gcc-4.1,然后连接了上去。下面又是内核头定义出现版本差异,搞到现在还没有搞定。谁能搞的定的给个参考,最好是debian上的解决方案。
        linux虚拟方案,kvm。这个是贝壳目前使用的方案,基本比较理想。速度很快,和xen基本差不多,显示速度不如vmware(理论上说装好显卡驱动应该会好点,不过贝壳找不到CLDC5446的XP驱动,那是Win32和Win95时代的显卡)。linux快windows慢,但是还在可忍受范围内。稳定性很好,只要测试通过,运行中到目前为止没有死机(当然很多参数是加了之后开机即死机)。数据可以通过samba互通,邮件也同样可以互通。然而使用samba无疑复杂很多,而且性能并不太好。只是从稳定性上说,让linux自己去驱动ext3总比半吊子的windows驱动更好,同时也不会出现争抢的问题。易用性上还算可以,无论是内核编译还是系统使用都不太难,最大的麻烦就是网络配置。根据贝壳的测试,在真实机器上superpi运行100W位需要45秒,虚拟机内需要54-60秒,尤其在换用kvm-72.2后反而更慢了(54下降到60,折合真实机器83.3%下降到75%)。
        总体来说,贝壳更倾向于使用全开源的准-全虚拟解决方案kvm,主要因为他简便易行,对系统影响小,不改变现有系统。同时性能高,稳定性好。主要需要解决显卡效率问题。如果以上问题无法彻底解决,贝壳打算换用linux下的vmware,想办法搞定他的内核模块。
    12/17/2008

    emacs简介

        emacs一定是我梦想中的编辑器,设计者一定是个超级的混蛋。
        emacs是一种多平台的编辑器,具有非常古老的历史,和vi一起并称为黑客常用两大编辑器(在贝壳学习linux后,就“顺带”学习了vi——因为根本 没有其他选择)。作为编辑器来说,他只接受文本编辑,但是却具有收发邮件,编译软件,调试程序,甚至煮咖啡等各种诡异的非正经功能。很多人疯狂的热爱他, 认为他是人工智能的结晶,化妆成编辑器的操作系统。但也有人疯狂的咒骂他,认为这东西纯粹就是折磨人用的。贝壳这次的blog,就是侃侃emacs到底是 什么。
        说emacs是人工智能平台,其实这个说法并没有错。emacs的运作机理和我们通常的编辑器都有不同。通常来说,一个编辑器可能有多个windows 啦,frame啦什么的,但是最终作用于一个文本。你可以添加,删除,修改,或者标记一段文本,进行剪切,复制,粘贴等动作。设计比较完善的还有回退和重 做。让我们专注于其中一个任务,例如,删除,看看我们的编辑器是如何工作的。
        以notepad++为例吧,在选中一段文字删除的时候,你可以用<del>键,同样,如果没有选中文字,这个键作用于当前光标后面的一个文 字。(在此我们不讨论<backspace>和前后删除的关系)这很简单,一个内容,按删除,就没了。但是你是否注意了这个过程本身,为什么 按下<del>是删除,为什么不是按下<s>键或者<a>键?
        按照程序员的思维来说,编辑器的基础是一个文本内容和一个光标(或者说一个position),我们通过修改数据结构变更这两者以达到某种目的,这个过程 被称为操作。例如,作为删除操作,我们移除(remove)了文本内容的position处的字符。而后,我们将这个操作(也可以称为函数)绑定 到<del>键上。于是,我们在按下<del>键的时候,触发了删除函数,导致内容被删除。这个是<del>能够删 除文本的核心过程。从正常人思维的角度来说,一般我们都会将这个功能绑定到<del>上面,而不会是<power>或 者<reset>,或者其他更疯狂的键。同样的机理,我们按下<Ctrl+s>,文件存盘,是因为存盘的功能被绑定到 了<Ctrl+s>这个键上。好的编辑器一般带有键绑定修改功能,notepad++就可以自行修改热键。而emacs则更进一步。
        emacs允许你自行编写扩充函数,并且将这些新的函数绑定到键上,这样就赋予了编辑器无限的可能性。例如,你可以写一个过程,每次触发就在当前光标处插 入当前时间对应的纽约时间。或者你可以写一个过程,自动根据一个预定义的数据表格补充你输入人名的头衔。可以想象,当你需要重复单一工作时,这种扩充能力 是非常重要的。你可以免除记忆一堆领导的详细头衔,免除重复输入,免除繁重的劳动,只要你编写一次扩展程序,并且绑定到某个键上。然而现在编辑器的趋势 是,我们使用编辑器从事各种不同的工作。有的时候,我们用编辑器记事,有的时候用来写程序,有的时候用来看源码。因此我们对编辑器的个性化能力和强大都没 有要求,反之对编辑器的标准化要求很高。说的更通俗点,我们不需要自行扩充一个插件来省事,但是我们一定要用<Ctrl+c>来复制, 用<Ctrl+v>来粘帖。因为我们(指普通用户,而非以电脑为生的专业用户或者是变态的geek)不会程序,或者不喜欢为了某个目地花费时 间来编写程序,毕竟现在不是70年代,当时接触电脑的都是智力最高的一帮变态。现在接触电脑的都只是普通用户而已,我们不需要强大的扩充,但是我希望我用 一个编辑器的时候,这些基本功能在另外一个编辑器上不会产生区别。从这点来说,emacs差劲透了。
        emacs的键绑定是根据上世纪70年代unix下(那时候linus都没出生,何况linux)的键盘来的,因此emacs假定你有一个Meta键。这 个键在今天的电脑上已经找不到了,我们用Atl来替代。但是同志们,Atl是系统键,这么替代是有副作用的。例如自动补齐的函数热键是M-Tab,但是请 试试在windows下按Atl+Tab。亲爱的,你会跳到另外一个程序上。那是windows切换程序的热键。还有我们经常 用<Ctrl+c>来复制,<Ctrl+z>来退回。可是在当时的linux下,<Ctrl+c>代表终止程序运 行,<Ctrl+z>代表挂起程序到后台。emacs当然要避免这两个热键,于是——你自己试试在emacs下按这两个热键的结果吧。
        从热键约定的角度说,emacs是当之无愧的最差编辑器。不过这个很难怪罪emacs,毕竟他出生的年代不用说windows,连dos都没有出 生,cp/m还只是个样品。要怪只能怪windows的设计人员在考虑热键的时候根本没有考虑emacs的现有标准,自己瞎设计一通(尤其是最常用 的<Ctrl+c>,虽然从人机工程角度这个是最合适的键)。
        emacs更强大的特性是,可以根据当前文件的特征鉴定文件类型,并且采用正确的模式。例如,我可以在python模式下编写python程序,在C++ 模式下编写C++程序。对于用户来说,这两种模式看不出区别,然而他们本身有着非常多的细节不同。例如,在C++模式下,/**/是注释,而python 下,#才是注释。灵活的模式允许你使用同样的方法操作不同类型的文件,并且还具有各种扩充。对于C++,他可以编译,对于python,他可以校验。并且 有一些比较常用的超级扩充,例如etags。这个程序可以用来生成一些文件,帮助你找到一个符号的位置。利用这个扩充,你可以快速的寻找符号位置,自动完 成,等等。这些近几年才在VS IDE和eclipse里面出现的特性,早在数十年前就出现在了emacs里面。
        如果你有程序基础,并且长期从事相似的工作,例如写程序,写文档,并且有很多重复的工作,希望解放自己的劳动力,那么推荐你使用emacs。如果你是个找 酷的新新人类,希望找一个很少人用的编辑器,具有真正酷的特性,被很多人称赞,那么建议你用emacs。除此外的人,请珍爱生命,远离emacs——这东 西太容易上瘾了。
    11/10/2008

    SCIP,lambda,Church

        贝壳最近在看SCIP,感觉受益匪浅。其中有一个2.6,使用函数表达数字,很难理解。贝壳查了查资料,这篇(http://blogs.sun.com /yongsun/entry/lambda%E6%BC%94%E7%AE%97%E4%B8%8Echurch%E8%AE%A1%E6%95 %B0)写的很好,贝壳就不多说了。贝壳把自己写的内容贴上来,作为一个借鉴。
    (define zero (lambda (f) (lambda (x) x)))
    (define one (lambda (f) (lambda (x) (f x))))
    (define two (lambda (f) (lambda (x) (f (f x)))))
    (define three (lambda (f) (lambda (x) (f (f (f x))))))
    (define (add-1 n)
      (lambda (f) (lambda (x) (f ((n f) x)))))
    (define (add m n)
      (lambda (f)
        (lambda (x) ((m f) ((n f) x)))))
    (define (mult m n)
      (lambda (f) (m (n f))))
    (define (show-func-number n)
      (define (inc x)
        (+ x 1)
        )
      ((n inc) 0)
      )
    (show-func-number zero)
    (show-func-number one)
    (show-func-number (add-1 one))
    (show-func-number (add one two))
    (show-func-number (mult two three))
        结果:
    0
    1
    2
    3
    6
        show-func-number这个函数是将高阶抽象函数序列映射到一个具体的数上的。工作方法是,建立一个函数x=x+1,然后使用给定的高阶函数来 映射这个函数。n次高阶函数会映射这个函数n次,于是结果函数就是x=x+n。然后将这个函数作用于0,不难得到结果吧?
    11/3/2008

    一些关于盗版、黑屏、开源的事情

        大家都知道,微软搞黑屏了。贝壳暂时就这个事情不发表评论,而是先说一些其他的事情,然后大家再回过头来看这个事情怎么说。
        首先是软件的版权区别。开源软件,自由软件,免费软件,共享软件,收费软件,盗版软件,这些我们经常说的名词究竟有什么意义,有什么相同和区别?
        首先,大家要了解一个事情,上述对软件的不同称呼,其实是不可并列称呼的。免费收费,是指软件的付费方式,开源闭源,是指源码的公布方式,正版盗版,是指 是否侵犯版权。这些其实是不同的事情,只是很多事情有前后的因果关系,因此大家容易混为一谈。一般我们可以将软件分为是否收费,是否开源,什么版权三种分 类方式。分清其中的区别有益于阅读下面的内容。
        开源软件是指源代码开放的软件系统。多数情况下,开源意味着免费和自由,但是也存在收费的例子。例如许多大型系统(好像有些UNIX就是,但现在具体什么 情况,贝壳没有用过,也没有看过软件协议),其源码对使用者开放(注意,开源并不代表对所有人开放,只要使用者有权获得源码即可。当然,如果范围缩小到使 用者中的特定群体有权,则不算开源,例如微软的不可泄露协议),但是属于绝对的收费系统。大家很容易理解这里面的原因,既然源码已经开放,那么多数人都可 以轻易写出类似的系统,在这种情况下还要坚持收费就愚蠢了。除非源码庞大,需要相当的水准和时间来理解,这样才能保持收费。当然,更多的情况是开源免费, 收取专家服务费。
        这里中间还要插入一句法律问题(怎么感觉写成法律普及文了),目标软件的作用是给予使用,源码的作用是表达思想,这是公认一致的原则。换言之,如果你发 布的是病毒目标,则是违法。如果你发布了病毒源码(当然,要排除恶意发布),则是研究之用,不属于违法。当年DeCSS的审判之所以被判定无罪,即是基于 上述原则。
        免费软件是指授权方式是不要钱的。现在免费软件的很大一个来源是来自开源社区,然而并非只有开源了才免费,共享软件和试用软件就是其中的两个典型。共享软 件的作者允许你可以免费的使用它的软件,但是并不开源。试用软件的作者允许你在一定期限内免费使用软件或其中的一定功能(其实试用软件的完整授权也不一定 要用钱,写个邮件把作者夸一顿或者给他做些事情,例如翻译软件,一样可以获得授权)。这些软件虽然免费,但是往往会因为有其他的原因而选择闭源。例如微软 的Process Explorer,就是属于共享软件的典型。这个软件原属于sysinternels的作品,后被微软收购。如果是开源软件,搞不好要和微软打官司,也不 可能被收购。而Winrar则是试用软件的典型,大家都听说过Winrar推动检查中国大型公司内使用非授权产品的例子吧。这个例子难就难在取证这个软件 产品超过了使用期限,因为大多数人可以通过重装来避免提示。
        自由软件是一个非常复杂的概念,要理解需要了解一些西方法律精神。自由软件现在在中国基本被视同为开源软件,其实两者是完全不一样的两个东西。自由指的是 你拥有软件的选择权,包括是否使用,是否修改,是否散发,是否改善,具体可以参考这个文档(http://www.gnu.org/philosophy /free-sw.zh-cn.html)。为了保证以上权力,开源是必须的,然而开源并不代表你拥有以上权力。我们在上文提到过,是否开源和什么版权是 两个事情。开源软件可以选择收费版权,也可以选择非收费版权,但是禁止你修改,再散发软件。这些都不属于自由软件的范畴。
        自由软件的起因来自于上世纪70年代出现在美国的自由潮。受到自由潮的影响,当时很多软件大牛都是黑客精神(不是现在这堆脚本小子讲的黑客)的拥护者。他 们认为人类学习和使用软件的自由不言自明,他们拒绝为他们的帐户加上密钥,并且以破解软件系统为乐。他们所写的程序也是免费分发。很难想象,在上世纪70 年代的时候,很多现在具备极大影响力的项目在当时只是几个人看不爽而随手做的一些小程序。很多自由项目直到现在还无人可以超越,发挥着重要作用。
        自由软件运动是天赋人权观念在知识领域的延伸,目的是推动知识的扩散。因为知识产品都有一个学习的概念,新手需要不断的观摩和学习成熟的系统才能成长。 然而如果允许其他人无限制的学习,那么新知识的发明就无法给创造者带来利益,从而导致没有人愿意发明创新。因此专利法规定专利的存在,给予了发明人一定时 期的权限,使其可以从中获利。而同时规定了专利期限,使得新手可以学习。(贝壳注:现在的很多专利期限动辄50年70年,实在是太长了一点,10年到20 年的期限应当是合适的)而自由软件在创造伊始就放弃了自身的专利权,给予了其他人学习和改进的权利,因此被认为是软件业的第一推动力。尤其是近些年,在 GNU的推动下,出现很多很优秀的软件产品。当然,其中大部分是和普通人无缘的。例如flex分析器,emacs编辑器。
        盗版软件这个词很不好界定,因为有两种界定线。一种是收费软件不付费使用,一种是违反软件使用授权。从范围上说,后者比前者更广泛,因为付费主要是取得软 件使用授权,不付费一定违反了授权原则。而违反授权则不一定是不付费,也可能是试用软件超期(违反试用授权中期限限定),未授权可以修改而进行修改(这个 尤其多出现在使用源码库的时候),违反最终用户协定(在共享软件中常见)。一般我们说的时候都指前者,但实质上,后者也属于软件权违法的例子。我们不妨用 违法软件来称呼后者,而用盗版软件来称呼前者。
        盗版软件是否是自由软件思想影响下的产物?绝对不是。我们上文说了,自由软件运动的主要目的是普及软件知识,那么破解软件成果如何普及软件知识呢?无法自 圆其说。也有人说这个是打击收费软件,以扩大开源软件的影响力。这就要讲到西方的毒树毒果理论,这个理论认为,非法手段(毒树),无论为了什么目地,其产 生的结果一定是恶意的(毒果)。开源软件有着自己的适用范围,不需要也不可以通过这种方式强行介入收费领域。再者说,如果没有收费软件来为大型项目提供资 金,没有大型公司来消化软件人才,那么程序员的将来也就无法保证,更谈不上进一步普及和推进计算机研究发展了。
        盗版软件只是一些不喜欢付费或者根本不拿版权当回事情的人,为了自己的利益编造出来的一堆谎言。例如微软的这次黑屏,很多人都在抵制,都在骂微软。我们可 以想象一下,如果微软的产品出来的时候就带着黑屏措施呢?他们照用不误,最多就是搞一下破解。Winrar也带了保护措施,用的人照样一堆堆,破解照样满 天飞。微软只和合法购买者订立了合同,保证不会侵犯他们的权益。非法使用者从根本上就没有依据来保障,你的系统即使上了Windows就当场机器爆炸,也 无法控告人家。
        其实本质上说,贝壳也是违法软件使用者。在这个社会里面,看清每个软件的版权,然后一点不差的照做是完全不可能的,可能的只有知道行为违法后想法弥补。使 用盗版windows则是因为贝壳根本是linux用户,但是同事全是清一色的windows,沟通不方便而被迫使用。既然我不是主动高兴买的,就上个盗 版得了,被发现最多回到linux下结束(中国的法律对个人侵权行为只纠正行为)。使用盗版windows,我们人人知道违法,但中国的法律基于告诉乃 论,就是所谓的民不告,官不纠。自己知道怎么回事,回去闷声发大财就算了,明明是违法者,还跳出来义正词严的指责受害者,做人不能太CNN。
        就如同我在MSN名字中写的那样。我虽然不赞成你黑屏,但是我捍卫你黑屏的权力。
    10/21/2008

    程序生产流程管理的一些想法

        程序的生产管理本质上说应当可以纳入生产管理中,然而程序毕竟是一种特殊的产品,因此程序的生产管理也有其特殊性。下面贝壳从小到大阐述一下个人对生产管理的一些想法。当然,30人以上的团队规模贝壳根本没有碰到过,因此就不予置评。
        首先我们从软件的生产流程开始论述,当然你可以没有流程,然而你不能没有过程。没有流程叫做不正规,低成本和高效率,没有过程……没有过程我也不知道你怎么做的,直觉?
        管理的头一步是计划,软件生产的头一步是调研和需求分析,然后是紧密关联的系统构架,包括构架选择和结构设计。调研的典型情况是回答以下问题,软件为了谁 而做(Who),设计周期和维护周期是多长(When),软件的适用范围(When),软件的意义和核心价值(Why),软件要达成什么目的 (What),如何设计达成这些目的(How)。这些问题中,要达成的目的和如何达成是核心。而后通过详细的讨论,得出到底要做什么东西,具备什么功能, 以及一些细节问题。这时期形成的是软件需求分析报告和软件需求说明书。需求说明书的尺度是一个很难把握的问题,一般来说,需要灵活对应市场反馈的软件,需 求说明书不用过早细化,反之则可以早细化一些。开发人员多,结构复杂的系统,设计说明书必须详尽,反之则可以简单一点。不过如果忽视甚至无视需求说明书, 或者将需求说明书仅仅作为一个官样手续的团队,必定会在后面吃大苦头。如果要规避需求说明书,也并非不可以。贝壳会在最后描述一个原型系统方法,来规避灵 活和不确定的需求对需求说明书的挑战。然而注意,这只是需求说明书的形成手段,本质上是以程序员和最终用户的互动来清晰需求,同时潜在的让程序员熟悉需 求。并非真的不用讨论和细化需求,更不是提倡做项目不出需求说明。
        项目的构架选择是在基本明确需求后做的事情,这个阶段主要明确以下问题。是单机软件还是群体软件,C/S还是B/S,.net还是java还是 C,Windows还是Linux。中间是否使用ORM,使用的话怎么设计细粒度表,不使用的话怎么使用冗余设计。结构设计和构架选择互相分离又紧密结 合,结构理论上是脱离构架的,然而某些结构就必须适用某些构架(例如如果用了事务明显就不能用mysql,性能会差死),某些构架则要求你特殊设计结构。 结构设计是系统一个非常广阔专业而复杂的问题,有兴趣的可以看系统结构和构架的一些书,还有设计模式和UML的,这里就不细说了。
        在系统完成结构设计后,就开始系统的编码过程了。而项目时间确定,到这个时候才有依据。粗说是编码过程,其实又细分为构架实现,技术研发,编码,自测试, 系统整合几个部分。结构设计完成后,抽象的结构必须经过严格定义才能进行使用,其中进行严格定义并且文档化的过程绝对不要轻率处理。如果是大型团队,应当 指定一个人负责结构定义的维护,并指定几个技术骨干来讨论定义,讨论修改。这个人需要处理每一个对结构修改的请求,分辨是否应当修改结构,并且交给骨干团 队讨论。讨论确定后,对定义进行修改,修改定义文档,并且通知所有部门进行修改。
        当结构定义后,系统的框架模型已经清晰可见了,剩下的就是编码了。可是否能进行顺利编码呢?未必可行。因为实际上会在过程中碰到很多技术问题。例如需要使 用以前没有接触过的框架和组件,需要对抽象数学模型提出可行算法等等。这些问题如果不先行解决,后面的编码无法顺利进行,技术骨干的最主要作用就在这里。 在他们解决技术问题后(或者,更经常的,在中间提出的问题被他们解决后),系统就进入了编码阶段。编码阶段的代码一般来说会有不同级别的自测试,从最简单 的写个小程序到最复杂的单元测试+冒烟测试,按照项目的级别具体分析。不过即使是再小的项目再小的模块,一般写一点代码就写个小片断测试下是否可行是最基 本的常识,除非你能保证所有程序不论大小一次写对。在完成自测试后,还需要整合入系统,并可能伴随冒烟测试。如果结构设计良好,这个阶段会非常顺利,甚至 不出现什么大的问题。反之,如果结构散乱,不用到客户那里,在这步就会碰到非常大的阻力。
        在完成主程序的编码和整合后,项目进入收尾阶段。一般是漫长的测试和后续工作,主要包括叠代测试,性能分析,编写使用手册和编码手册,编写项目的技术分 析,系统分析报告和各种材料。在这个阶段最主要是要通过测试,先于客户找出错误,并且逐步修改掉。良好的测试结果应当是逐步收敛的,当你看到一个逐步发散 或者不稳定,根本没有规律的测试修改结果时,你的麻烦就大了。这通常是因为没有构架,构架错误,中间有不适应构架的修改,构架变化,核心算法错误,需求浮 动太大等根本问题所导致的。当然,在测试的同时还要进行全面的文档化过程。
        在测试和交付后项目是否结束呢?恐怕还没有。下面是漫长的客户服务期,需要收集和分析客户反馈,进行持续改进。不过那就是后面的问题了。下面我们按照团队的大小来逐步讨论团队的分配和任务。
        首先是从一人团队开始,当然,如果也能叫团队的话。作为一人团队,也就没有什么分工问题。文档化要以轻量为主,方便自己日后理解,除非是客户特别需求。
        而后是典型的一个团队,一个PM带几个技术,可能还有外面的美工支援什么的。人数不超过五人,不分组。这里前期的需求/后期文档都要由PM完成,程序员主 要注重编码和测试(尤其是单元测试)。如果时间充裕,建议一些专门编码一些专门测试,这样可以有效保证代码质量。互相的沟通以开会为主,信息的沟通要诀是 让每个人都知道别人的事情,尽量多的向别人传递信息。
        再下面是一个典型的“大”团队,两个PM带四个程序员四个测试,其中有两个以上技术骨干,再加上一个专职美工和专职的UI Design(或者两个美工,基本差不多),8-16人的“大型”团队。说大型是因为这个团队开始内部就要分工协作了,加引号是因为……即使10个人,基 本也就刚够分工的底线而已。
        贝壳个人建议,除开美工等支持岗位,将这种团队分成三个部分。一个是PM组,负责和用户沟通协调,产生需求文档,盯项目进度,调度程序员,产生用户文档, 产生其他材料。这组PM不要求高技术,对于技术建议会用,但不用精通(最好也别精通,业务骨干做PM是非常浪费的)。但是对于沟通技巧,协调能力和领导能 力要求非常高,也要求有相当的文字功底。毕竟他们是要和客户沟通的人,要是鸡同鸭讲就麻烦了。和客户产生矛盾,文档写不好,更是麻烦中的大麻烦。由于协调 要求非常高,因此PM组强烈建议至少两人,手机24小时开机!建议设立正副职,采取正职负责,副职挂钩的方式。
        第二个团队是研发组(Dev),至少一个技术骨干带队。这组需要负责编码,自测试,系统整合,出开发文档,出技术文档。对于他们要求是沟通能力过关,程序 编码效率高。对于系统经验,思考方式的全面和独特没有特殊要求。一般经过培训的新程序员就可以在研发组中担任工作。他们年轻力壮精力旺盛,相对编码效率比 较高。不过如果有条件,还是用比较有经验的程序员比较好。在系统的整合测试,返工引发的效率低下控制方面会有相当的好处。
        第三个团队是测试组(Test),至少一个技术骨干带队。这组要求负责系统的叠代测试和性能测试,可能还要帮助编写用户文档,进行项目实施,培训和售后支 持。这组人的要求是工作勤奋(叠代测试的工作量是非常高的),技术过关(否则无法发现一些问题),系统经验丰富,思考问题全面而独特。因此强列建议由最强 的技术骨干和一帮能吃苦的年轻人组成。一般来说测试组和研发组的人员比例在1:2到1:1之间。如果小于1:2,那么会发生测试不充分的情况。如果大于 1:1,只要成本允许,到是强烈支持。
        最后一个团队(有人算了算……怎么还有),是系统的管理组。负责项目的构架选择,结构设计,时间节点认定,人事事务,开发成本控制,技术研发。这个团队有 非常高的技术能力,管理能力和执行权限,一般由主PM,研发组和测试组骨干,客户代表,公司代表(多数和主PM是一个人)组成。主要是要对项目过程的监 控,项目中人员权限的分配,核心技术的研究和管理进行处理。这个团队等若公司和客户的联合代表,对项目负全部责任。
        对于30人以下的团队,估计可以按照比例放大组规模来使用同样的组织结构。不过如果再大,同样结构就不合适了。这主要是因为同一个组中的信息是互相完全流 通的,超过10人的组会造成非常高的沟通成本。这时候一般是分解系统结构,分解研发组和测试组。将一个大型系统分解为两个或者多个独立的部分,让每个组分 别研发和测试。这样可以避免每组内的信息沟通成本过高,可对文档的严密性和规范性提出了更高要求。通常来说,拆分方法有按照功能和按照构架。即按照功能划 分出一个一个的业务模块,每个组开发一个完整的业务模块。和按照构架层次分为数据库组,业务逻辑层组和客户层组。通常来说,我支持以业务为主的拆分方法。 因为此时组已经够大,让一个组精通所有层次不难。但是让所有组全部完整了解需求可就很难了。
        对于这种成规模的开发,注重的主要是两点,文档和测试。最高的要求(也是我认为最好的褒奖)是及时的文档和全面的测试。此时的难点在于,研发过程中程序员 经常为了修补问题而修改代码,但是忘记修改程序文档。或者需求变更后PM改了需求说明,通知了Dev,但是忘记通知Test。又或者通知了Test,但是 忘记修改用户手册。因为诸多文档其实是对同一内容的不同描述,所以相互具有关联性。其中之一变化经常导致其他文档落后陈旧,而且版本不统一。
        测试应当贯穿正规研发过程。从程序员实现一个个功能起就应当开始叠代,直到项目完成后。并且bug的管理应当和需求管理合并,成为几个组沟通的核心。测试的时候一定要注意充分测试和叠代测试,不要象微软一样弄出新的补丁补出老Bug的状况。
        下面贝壳讲以下系统原型法,其实这个就是业界常说的敏捷开发。系统原型方法是指以构建非常简单的系统,实现非常核心功能的原型系统为基础,逐步推导出正规 系统的功能和需求的系统分析方法。主要适用于系统需求不清晰,分析困难,开发周期短,程序员数量适中的项目。如果上述条件不成立,那么建议不要使用原型方 法。
        原型法的头一步是分析需求,不用说不确定的,就说为了实现业务目的(至少这个应该知道吧?)需要哪些功能。然后实现一个可用的,不用很美观,不用性能优化 的系统。有了这个系统后,客户可以逐步分析使用这个系统哪里不方便,而后交给程序员改进。逐步反复,直到客户满意为止。使用这个方法,客户和程序员间,程 序员互相之间要保证充分沟通,文档可以容后再写(前期还不确定呢,怎么写?)。主要是注意逐个的需求管理和Bug管理,这正好和我上面说的合并管理对应。 使用这个方法的好处是 当不清楚需求的时候可以马上做,逐步清晰。过程比较直观,做出来东西比较实用,也节约时间。坏处就是会浪费一定的程序员人力,而且一个没控制好就一直改一 直改不知道哪里是头了……
        当前国内软件业企业的几个问题就是,不注重需求,不注重测试,不尊重专业,不尊重规范,不培养人才,不积累技术,不重视信誉,不打算做事。下面贝壳逐个细说。
        不尊重需求,一般来说,老板讲的时候都是需求为重的,可当客户需要变的时候,老板很容易同意需求变更(虽然我可以理解,做生意也不容易)。不尊重测试,做 程序的非常理解测试的重要性,然而老板却认为那个岗位可以随便找个人来做。实际上,测试是一个非常专业非常流程化非常严密的东西,测试的主管最好是公司里 面最有经验的人。同时,还有不尊重专业的问题。并不是说老板干预程序员的决策,而是很多时候老板根本不了解技术骨干和PM,测试的区别。让技术骨干来做策 划,或是负责、或者主导和客户沟通,这都是超级缺乏效率的做法。至于不尊重规范,事先划定的流程,在遇到重大问题的时候,往往就变成了废纸一张。到不是说 在重大问题前非要坚持僵硬的步伐,可一个项目一半时间都是重大问题,这就过分了把?先说了项目过程中要推进知识积累,推进技术交流,推进这个推进那个,等 项目一忙就全飞了。
        同时由于程序员的超高流动率,当前中国的程序界有一个非常不良好的风气,公司基本不培养自己的程序员。都不知道公司是否能开到明年呢,培养了做什么呢?这 点在大型公司就比较好,无论什么情况,基础的内部交流总是保证的。只要签长约,多数可以弄到一些培训。中小公司不培养人才一方面是没有必要,另外一方面就 是没有能力。于是程序员就被迫自我培训,自学或者脱产参加培训。付出了成本,自然要赶快跳到能实现价值(能把钱赚回来)的公司里面去。中小公司为人员流动 付出巨额成本,而且很多都根本无知觉。
        举贝壳公司的例子吧。因为发展需要,今年年初公司曾大型招人,C#程序员,结果可用者寥寥无几,很多都是浮夸碰运气的。以至于一天面试十多个人,竟然一个 备选都没有的情况经常发生。一个人过来投简历,硕士,要价10K多。不说公司能否负担,看了看做的题目,算法题还不错,C#技术,解决实际问题都一塌糊 涂。这种人招进来差不多就是研究算法写Paper的主,要做程序还得培训一下。还有一个人,我前周刚刚送走,转眼又回来,估计是批量投简历的时候忘记筛公 司了。还有一个项目经理真的是不错,讲起问题来很深入,经验丰富,可老板认为用不到,贝壳一点办法都没有。由于人员仓促到位,我们在开发后期付出惨烈代 价!有一个程序员从到岗到离开公司,最大的贡献就是拖了三个多月的进度,因为他连static函数干吗用的都不知道。还有一些人,很适应岗位,可做不到多 久就走了(当然,这是有各种原因的,试用和刚满期的人走是比较正常的事情)。问题是,其他人就要重新接手他的事情,等来了人再换手。这样的直接结果是什么 呢?如果说项目拖延有一半是因为我们需求没做到位,另外一半就是团队的人才损失。如果在普通项目上碰到类似问题,来的人不能做事情,人员替换率高。那么本 来能按时完成的任务就一定会延后,而且分析的时候很难直接表现出来,多数会被认为是工作效率不够高,工作态度不认真之类的(某种意义上也没错,毕竟不能做 事的人,不是因为效率不高就是因为做事不认真)。不能留住人才造成的后果,是通过项目拖延表现出来的,使得公司往往失去了隐性可能的良好口碑。这种软性杀 伤是非常致命的但是又是难以直接表现的。
        如果说不注重人才,还怎么能积累技术呢?人是技术最主要的载体。尽管我们可以通过互相培训,技术交流,技术文档化来积累技术。但是如果没有老员工的指点, 那么新员工是很难吸收企业的原有技术体系的。无法积累技术的直接表现有两个,一个是中国企业没有核心技术,另外一个就是掌握技术的人就卡了公司的脖子。可 能有人会举出中国有多少专利多少成果。贝壳告诉你,按照贝壳做项目的经验,那个多数都是项目做好了用来表功的牌坊。很多技术都是公司不敢给个人,个人不敢 给公司,因为浮动率太高。许多真正有价值的核心技术往往是因为缺乏大公司(或者缺乏人)作为后台,而无法正式的走向商业化运作,更无法走向系统化理论化。 因此中国不但缺乏真正的核心技术(我指能解决问题,有实现难度的技术),更缺乏(这点可以确认)系统化理论化的技术体系。而掌握公司核心技术的人往往就能 卡公司的脖子,尤其是技术都掌握在一个人手中的时候。并非说程序员都有坏心或者什么的,而是程序员有很多和老板不一样的想法(例如要重视测试,要重视专业 等等)。当程序员觉得他是对的时候,为了和老板争辩,往往会使出走人的杀手锏。固然,公司是对产品拥有产权的。可是掌握核心技术的人不在,没有人能继续改 进,研发新的产品系列,这不是要公司的命么?这时候老板就处于弱势的一方。从某个事情来说程序员往往是对的,可是从企业发展来说却绝非好事。
        最后两个问题则是中国软件业更深层次的问题,不重视信誉,不打算做事。整天就想着前辈一夜暴富的事情,或者谁谁风投成功吃喝不愁的事情。根本不打算花心思 将事业做好,而是设法请客招待人拉风投,找人做假买点击量买排名,花钱黑掉对手的网站,盘剥底层员工,炒作一些无聊的事情增加知名度。某种意义上说,这个 才是中国软件业最大的毒瘤。
    10/12/2008

    分词算法的具体实践

        说到分词算法,可能很多人都很陌生,然而说起百度,google,很多人却是耳熟能详。google,百度在搜索的时候,输入关键词后瞬间就可以得到结 果,如果用通用数据库是无法做到的。实行这个加速的关键就是分词算法。例如"项羽是萝莉控"这句句子,我们一般搜索都是搜索项羽,或者萝莉控,萝莉。你见 过有去搜"是萝"这个关键字的么?因此系统通过分词,将句子分解为"项羽/是/萝莉控",去处单字常见词"是"(如果要索引"是",可以想像有多少文章没 有"是"的),我们就得到了项羽和萝莉控两个词。再通过反向关联,建立项羽,萝莉控指向文章的连接,就可以完成瞬间的搜索了(具体原理不说了,只要有一定 数据库基础的人都应当能想明白原理)。并且通过关联性,某种程度上也可以提供"是萝"的搜索(带"是"的词,带"萝"的词,相关度最高)。
        那么,如何来计算分词呢?方法很多,大家可以在网络上搜索下,贝壳就不赘述了。贝壳现在要说的是这次贝壳主要试验的方向,基于词典的机械分词中的最大分词算法。
        机械分词算法是当今的主流,关键原因在于速度问题。虽然正确的分词很有价值,然而如果速度太慢,一样没有什么用处。机械分词一般可以保证 98%-99.5%以上的正确率,同时提供极高的分词速度。而机械分词一般来说,都是基于词典的。主要有正向分词算法,逆向分词算法,最大匹配分词算法。 其中最大匹配分词算法具备最高的灵活性,只要你能评价一个切分的优秀程度,算法能把所有可能算出来让你评价。然而大家可以想像,这个是非常耗费CPU的。 贝壳在这个基础上,做了一个具体的实现和细化加速。并且有准备做为一个开源项目来长期运作(只要有人有意向接手合作)。
        首先我们先说贝壳这个算法的评价原则。贝壳认为,评价原则应当有以下几点。同时也必须要说明,以下原则是无法正确评价所有情况的。不过以下原则在原则正确 的基础上比较便于优化。一、无法分析的词最少(这是全局最大匹配的理论核心)。二、匹配出的原子最少(这是保证分词优秀性的指标)。三、匹配出原子的出现 概率和最高(这是纯粹没有办法了从概率上提高匹配正确的可能)。
        当我们分析一句话的时候,我们可以想像,这句话应当是正常的,可被理解的。换句话说,句子中应当都是有意义的词。那么,在匹配后无法理解的词是什么呢?一 种是匹配错误,一种是新单词,一种是单字成词和无意义助词。单字成词的例子有上面的"是",我们可以通过一个比较小的词典去除。那么,假定词典够大的情况 下,无法理解和分析的词越少的组合越正确。而同样一句话,匹配出的原子越少,在搜索的时候效率越高。因此我们有规定了原子最少原则。至于最后一个,在无法 分析词一致,原子个数一致的情况下,我们只能通过出现概率来猜测可能性。
        然后,现在让我们分析一下分词的特点,并且做一定的优化。首先就从最著名的例子,"长春/市长/春节/致辞"开始。
    >>>长春市长春节致辞
        首先,匹配算法一定要先搜索到一个出现的词,有词才有匹配优化问题。没有词的话,你试试看分词"嗡嘛呢呗咪吽"。根本无法可分。因此首先我们要计算出一个出现的单词。贝壳是从正向开始计算的(主要是因为词典的加速方法是头索引的)。
    >>>*长春*{市长春节致辞}
    >>>*长春市*{长春节致辞}
        好的,我们匹配到了两个,不过这就是全部可能么?不是,否则就变成了正向最大搜索。你可以看看"有意见分歧"。如果从头一个匹配到开始计算,无论如何都 是"有意/见/分歧",而事实是"有/意见/分歧"。因此我们还有一种可能,在头一个匹配到的位置,其实并不匹配。不匹配多长呢?最大长度不会超过最短的 匹配词。为什么?我们来看下面一个例子。
    >>>*长春*{市长春节致辞}
    >>>*长/春/(这两个字不是词,而是两个无法理解的字){市长春节致辞}
        很明显,后一种分法违背了我们的第一原则,无法分析的词最少。无论后面怎么计算,其最优结果是相同的。在后续结果相同的情况下,头一次匹配到词后,所有可能的跳空(搜索可能的不匹配)最大长度严格小于最短匹配词的长度。
        那么是否所有跳空都要搜索呢?也不,我们可以继续剪枝。对于情况"有意见分歧"来说,这个路径是必须搜索的。但是对于我们的例子来说,是无需搜索的。为什么呢?我们看以下计算。
    >>>*长/{春市长春节致辞}(下一个匹配是什么?总不会是春市吧,所以应当是"市长")
    >>>*长/春/市长*{春节致辞}
    >>>*长春*{市长春节致辞}
        大家可以看到,其实这个路径是无需计算的。那什么情况下需要计算呢?
        一旦跳空,其跳空后寻找到的下个词的位置必须严格小于最短词的词尾位置。否则就没有搜索价值。具体可以看以下示例。
    XXXXXXXNNNNNNNNNNN(X是词,N是无关紧要的)
    SSSSSSSXXNNNNNNNNN(S是跳空或者跳空后形成的无法理解字,X是词,在这种情况下,无论后面怎么评价,都不影响该匹配被剔除)
        OK,我们回到例子,刚刚我们说了,有"长"的匹配。但是通过刚刚的剪枝,又被剪了出去。我们下面分别计算两个情况。
    >>>市长春节致辞
    >>>*市/{长春节致辞}
    >>>*市长*{春节致辞}
    >>>长春节致辞
        好,我们先不计算下去了。通过上面的计算,我们发现,在计算过程中经常需要计算同一内容的结果。我们可以想一下,同样的分词,同样的算法,出现的应当是同 样的结果。就是说,分词函数是状态无关的算法。通过分解一个单词,得到一个最优结果。那么,我们对于同样的数据,何必需要计算两次呢?贝壳上文中提到过记 忆函数,这次就用上了。根据贝壳的试验结果,如果记忆全部词的分解结果,会造成大量的记忆-释放,而内容基本没有用到,造成效率下降。如果只记忆长词的分 解结果,往往又会因为太长,大多数句子无法达到长度而根本没用。这中间有个平衡值,具体多少贝壳就不说了。我们可以按照上文的方法计算以下两个过程,得到 结果。大家可以自行验证。
    >>>春节致辞
    >>>*春节*致辞*
    >>>长春节致辞
    >>>*长/春节*致辞*
    >>>*长春*节/致辞*
        结合上面的过程,我们推算得到结果。
    >>>*长春*{市长春节致辞}
    >>>*长春*市长*春节*致辞*
    >>>*长春市*{长春节致辞}
    >>>*长春市*长/春节*致辞*
    >>>*长春市*长春*节/致辞*
        按照上面的评价原则,我们得到了正确的结果。
        大家可以看看其他例子,这里着重说一下"有意见分歧"。
    >>>有意见分歧
    >>>*有*意见*分歧*
    >>>*有意*见/分歧*
        注意,有是单字成词,见可不是。如果见单字成词,做看见讲,那这句话就彻底成歧义句了。可以理解为,有意的要看到(或者让其表现出)分歧。这一般是古文语 法。由此也可以看出上述原则在理解古文的时候往往会出现问题。同时还要指出的是,在匹配"长春市长春药店"的时候,会出现以下结果。
    >>>长春市长春药店
    >>>*长春*市长*春药店*
    >>>*长春市*长春*药店*
        两者的无法理解词都没有,切分数一致,最后硬是因为春药店出现概率低而被筛掉。可见系统有的时候是依赖概率和人品在工作的。
        经过上面的原则和算法,贝壳实现了一个python的分词程序,1000行代码,原型系统。90W条词情况下,在AMD MK36上(2G主频)分词效率66K/s上下,具体看分词的选项(例如顺序分词就比较节约资源,分词排除重复就比较慢,启用多线程后在单CPU 机器上更慢),内存使用114M。使用C++写的核心词典后,90W条词的情况下分词速度80K/s,比python的核心词典快了20%,内存70M, 节约内存40%。不过可惜,这个核心词典是公司产权,贝壳无权公布。并且贝壳做了一些工作,准备使用分词程序来生成分词词表。这个么贝壳就不准备讲了。前 面讲的内容贝壳准备放到试验型站点 http://shell909090.3322.org/split_word/split_show/上面去,08年内有效。有兴趣联系我的可以发 mail给 我,shell909090@gmail.com,欢迎大家试验并提出意见。
    9/10/2008

    语言的对比

        最近贝壳在学(或者准备学)三门语言,python,ruby,scheme。全部都是高级抽象语言,都是脚本语言。加上贝壳本身已经非常熟悉的几门语 言,C/C++,Java,Asm,bash,贝壳这次是正宗的要"精通"七门语言了。当然,如果加上勉强会用如vb这些,十多种也不成问题。做为一个学 了多种语言用了多种语言的程序员,我想写一下自己多这些语言的认识,作为后来者的参考。
        首先是C,当然,是不包含C++特性的C。这门语言可以说是贝壳所见的语言中,最强大最广泛最具备生命力的语言。其他任何一种语言说到混编接口,基本就 是C语言接口。C语言也可以模拟非常多的特性,COM,C++,都可以用C模拟。当然,模拟的复杂程度是另外回事情。并且可以编写其他语言的解析器/虚拟 机,而这点来说其他语言很难做到。其实本质上说,C语言就是跨机器跨平台的万能汇编语言,所以才具备这些强大特性。C的核心思想是指针,其整个语言构架都 是基于指针的。通过指针,我们直接操纵着内存地址的读写。当然有利必定有弊,C语言太难掌握,效率太低了。使用C写代码,就是和机器打交道。你得控制数据 的读出写入,控制设备的初始化,控制内核交互。根本上说,一个强大的C程序员,并不强大在算法和创意上,而是强大在对系统的方方面面的了解上,强大在对基 础原理的掌握上。
        其次是C++,说实话,这语言有点高不成低不就。如果要掌握系统的方方面面,他不比C。如果要抽象要构架要快速要敏捷,他比不上所有的抽象语言。但是 C++的长处在于在系统的层级上引入了算法抽象,增强了编码效率。如果你确定需要在系统层级上编程,又不高兴从C的角度去写。C++可以极大的增加你写代 码的速度。当然,他的弊端就是对底层的掌控力和抽象实现的复杂程度。C++(我指的当然是包括STL和Boost的)的长处在系统层级,所以你写代码的时 候必须了解抽象的实现方式。然而抽象实现的越强大(例如boost的share_ptr),其底层的机制就越复杂,你掌握的时间也越长。而如果不了解底 层,往往会发生很多很奇怪的问题。例如smart pointer的问题,在其他语言中,这是语言系统的bug。而在C++中,则是你自己的问题。因此,想要真正玩好C++的程序员,必定首先是一个C高 手,而不是拿着C++的OO特性把C++当普通OO语言的人。
        再次是Asm,这种语言可以说没有什么生命力,因为他变化的太快了。一旦硬件构架改革,汇编就要调整。而且抽象层级不够高。除非你正好做操作系统底层,编译器优化和系统破解,否则最好不要考虑这种语言。这种语言的核心构架是寄存器。
        然后是java,当然,还有很类似的C#。这类语言开始,语言的抽象层次提高了,因此可以提供反射(python中叫做自省)。反射是高级语言中必要的 特性,然而C/C++并没有提供,原因么则必须说一下编译型和解释型语言。一般语言分为编译型和解释型,编译型的代码成型后只需要相关的库支持(其主要目 的是代码复用减少复杂度和内存消耗),而解释型的语言需要解释器。如果仅仅从方便使用角度说,解释型语言远远不如编译型(因为要单独安装解释器,当然,像 bash这类怪胎就表说了),而且解释型的语言运行效率仅有编译型的1/10左右。然后当今语言界,编译型语言远远比不上解释型使用广泛。其根本原因有三 个,一个是编译过程,一个是反射,最后一个就是内存管理。
        如果读者有编译代码的例子,应当知道,除非在同等的条件下,否则编译C++代码是很麻烦的事情。而配置同等的编译条件则彻底失去了C++跨平台的意义。 因此C++在不同平台下反复编译的时候,需要考虑大量的问题来达到跨平台的目的。往往这种事情麻烦到需要作者亲自指导编译的地步。如果一个软件,发布的时 候声明可以跨平台,然而使用前需要作者指导用户做一堆繁复的操作。估计这个软件只会受到专业用户的欢迎吧。
        所谓的语言编译,其实需要经过两步,编译和链接。编译的主要目的是将每行代码翻译成对应的汇编语言,而链接则是将符号引用转换为地址引用。举例来说,我 使用了一个变量str来存放一个字符串,这个变量str就是一个符号。我需要在上文中声明(declear)这个变量,以便编译器在编译的时候代换这个符 号对应的内存地址,和理解如何使用这个符号(关于上文没有声明的情况下的错误,请看"向下引用"特性)。我使用str的第四个字符的时候,str[3]就 会被翻译到固定的内存地址或者基于基址的偏移,机器完全不用理会str是什么。而这点,则是反射实现的最大障碍。反射可以提供一个对象是什么,有什么的信 息,并且可以动态创建对象。有了反射以后,才可以实现序列化,分布式等等高级应用。而C类语言在编译后失去了符号是什么的信息,只剩下一个名字,链接后连 名字都没了。这种情况下,你怎么知道一个对象是什么呢?
        Java/C#可以提供反射,因此属于解释型语言。但是他们又不属于完全的解释型,而是解释型的一个特殊分支,中间代码。中间代码型的语言,需要编译, 执行的时候又需要解释器。看起来没什么好处,可是在支持反射的基础上,大概可以以C代码1/2的速度运行,比纯解释快多了。原因何在呢?我们可以看看 C++为什么不支持反射。反射是保存针对执行结构的数据并且提供交互,而C++则在编译时生成后丢弃了这些数据。因此,理论上说只要保存了这些数据就可以 实现C++的反射。这就是中间代码语言所做的事情。当然,考虑到跨平台特性,编译的结果并不是汇编代码,而是类似汇编的代码(Java叫P代码,C#叫 IL)。后JVM直接执行P代码,C#则通过引擎编译IL到本地代码。因此JVM执行的时候效率基本恒定,而C#初次执行速度慢,后来则是比C++慢不了 多少。
        最后就是Java/C#的最强特性,动态内存管理。使用这个特性,可以使得程序员彻底的从内存分配和管理的泥潭中脱身出来。白痴的程序员写的程序可用, 强大的程序员写程序的效率提高。可是成也萧何败也萧何,内存不到底不回收,又有额外的内存开销,结果导致系统的缓存命中率下降。我们平时觉得Java类语 言执行慢最大原因在这里,半解释才不是根本原因。
        因此这些语言的特点就是中间代码,其核心思想是对象。这类语言的最大特性就是抽象和构架,使用强大的设计模式,将大型问题拆分成多个小型问题解决。在解决问题的时候,其代码量并不比C++少多少。
        再然后是python和ruby,当然,某种程度上还有bash,只不过他弱了点。这类语言的核心思想是抽象数据,例如字典,字符串等。bash是围绕 着字符串处理设计的,python是围绕着集合设计的。这些语言解决问题的速度非常快,但是模块化特征和抽象特征相对弱。一般情况下,和C++相比,解决 问题的速度大概是1:5,代码量则是1:3。
    9/9/2008

    程序员的几个分类

        程序员有高下之分,可高下怎么分?到底什么是高程?程序员要完成哪些任务,怎么评价是否完成的很好?
        下面由贝壳同学来胡诌一下他的个人感想,以下的程序员都指商业程序员。当然,爱好者可以类推。
        首先我们先讨论一个风马牛不相及的问题,程序值钱么?废话,人家账单大门兄都已经是世界首富了。可贝壳认为,程序不值钱,算法也不值钱,如果说值钱的话, 就和软件光盘上面的光盘一样,是个成本性质的辛苦钱。软件里真正值钱的是软件的思想,我们大可以想象一下,写一个让人想穿脑子也想不出怎么用的程序——当 然,会写的程序员想穿脑子——然后想想值钱不值钱。大家就会了解到,其实程序员,编程本身,是和装配线上的装配工一样的低附加值行业。不同之处在于,不同 培训程度的人劳动生产率不同,而且生产率差异远远大于普通行业而已。好的Coding比差的生产率会高上数倍,而且很难多找几个差的来替换好的,水平不 足。但是,做Coding,无论做多快,其价值只会线性增长。
        那么为什么软件也被称为今年来发展最迅猛的产业呢?其实关键在于软件业让人可以实现以前无法实现的一些想法。例如,以前不会有人能做到让地球上所有的人 (好吧,是大多的人)坐下来一起讨论一个事情的方法。那么,今天我们的技术已经可以做到。有个人针对当前的技术,设计出一个很好的让所有人坐下来讨论事情 的系统。包括一个BBS,带自动翻译系统。在线视频会议中心,可以附加购买在线翻译。一个邮件列表,带存档功能。一个文档编辑和管理系统,带同步编辑,版 本管理和资料索引。当我说出上面这堆东西的时候,可能有的人已经晕了,当然,六牙四皂小姐估计已经不继续看了。不过做过一些时间和电脑有关商务的童鞋应当 都理解这些东西的意义,并且可以想象这些东西带来的便利。在线翻译的支持系统,邮件列表存档系统,同步编辑,版本管理,资料索引,这些都是技术。尤其是资 料索引和自动翻译,更是技术的巅峰之作。可是如果不是结合起来让客户用的舒服,这些东西有价值么?
        软件业的价值在于将技术转换为客户的满意,并且最终转换成客户的钞票。越成功的规划,越能满足更多的客户,并且让他们付更多的钱。从这个角度讲,不论软件 做的怎么样,微软的规划是全球一流的。同时,能促进这个过程的人,才是具有价值的。不过遗憾的说,到这步基本就不是程序员,而是CTO了——
        程序员的最高价值,在于根据技术和行业,判断应当发展什么技术,采用什么框架,从而低成本,高效的做出让客户满意度最高的系统——而且不是一个系统,而是 一堆。这有两个非常苛刻的要求同时存在,对于技术非常熟悉,视野开阔感觉敏锐,否则怎么去感觉技术的价值,判断应当研发的技术?就这点而言,许多程序员在 超过30岁后往往都可以在熟悉的领域内做到,有条件的话大概可以在多数领域做到。然而最麻烦的在于,做这个事情的时候,你必须熟悉客户,熟悉客户需要什 么。这点往往是不可能的!客户是不可理喻的!技术不是万能的!要知道,使用最好的技术,设计你最喜欢的系统,往往是客户最讨厌的事情。行业客户如此,通用 客户更如此。
        如果上点你做不到,自然有高水准(也许吧)的人来做,那么你可以做次之的工作。什么呢?执行他们制定的方向。上层的人会告诉你应当发展什么技术,采用什么 框架。现在要求你——不用会——能够整合实现这个目标。说明白点,你可以招,但是要求能留下人,低成本。你可以培训,但是要求能做事。你可以研发,但是要 求好用。你可以买,但是要求低成本。如果你能实现这些目标,那么同样,你也是有价值的。
        这个层次的程序员往往是Project Manager(当然,很多PM根本不是程序员)/Team Leader/Core Programmer。对于他们的要求往往是兼顾技术,行政和人事的。他们需要能够组织研发,积累技术,产生产品。很遗憾的,个人编程能力往往又是次之。 不过程序员是很艺术化和个性化的一群人,在这个位置上的人,如果没有相当的技术水准,很难镇住下面的人,用普通管理人员来管理程序员的结果往往是给程序员 联合起来耍。因此一般情况下也需要了解大致的程序,并且最好有一定技术水准。
        最下面一个层次的人,基本就是能够实现程序,会用上面决定的框架和技术,编码效率高,工资要求低——别的没了——
        当然,以上是从职业分工来讲一个职业程序员的价值的,你可能说上面没道理,贝壳乱讲。不过事实是,职业的情况下就是上面的状态。当然,从业余爱好者,技术研发者来说,程序员又有另外一种不同的分法。
        第一个层次,是刚刚学会技术。能够使用某种特定语言,按照一些例子编写一些程序。实话说,按照贝壳的程度,入手一般语言做到这点不超过7天。然而很多人会 徘徊在这个水准无法进步,原因在于——他们能写程序了,而且写了能用。从这个引申开来,顺便说一下,程序员的进步是个很吊诡的事情。一方面来说,要勤于钻 研技术才能进步。但另一方面来说,如果不够懒,是很难有足够动力学程序的。因此,好程序员都是勤于钻研的懒汉。
        第二个层次,学会了使用框架,并且能够设计一些中度复杂的系统,开始接触第二语言。和初级程序员不同的是,他们能实现一套完整的系统,而不是一个个零散的 功能了。这要求他们了解框架,什么时候触发什么函数,系统间怎么互相通讯。并且,有水准的还可以写一些小型的框架。
        再上一个层次,了解软件工程对软件的意义,能够跨多种语言编程,灵活使用设计模式,能够设计复杂框架,习惯文档化。和上面的区别看起来不大,不过是能多用 几种语言,朴素的设计被设计模式所规范,设计的框架复杂化,并且会写一堆无聊的文档。不过从这步开始,程序员开始了迈向大道的第一步,在这个层次以下的只 能算爱好者。无论是研究技术,研究数学理论,还是什么,规范化都是必须而且是非常重要的。我们很难想象一堆工程师,各画各的图纸,最后房子还建的多快好省 的。同样,作为高级程序员,头一步就是学会和别人合作。使用设计模式的规范进行设计,使用文档描述系统,可以跨越多种语言协作,了解多种语言思想,这是必 须的。
        再上一个层次,就已经不是程序员的境界了。作为程序员,上个水准已经到头了。更强的程序员意味更规范?效率更高?那是八级钳工!作为程序员,你可以不认识 英文,你可以大字不识一个,然而你必须是个数学高手(其实现在数学高手大字不识一个几乎不可能)。作为程序员的巅峰,你可以很轻易和他人协作,使用合适的 语言,然而无法规避的是对问题的抽象描述和求解。在贝壳作为程序员的这段时间里,无数次的碰到数学问题,有些往往是大学里面我们所不屑一顾的。例如蒙特卡 洛法,拉格朗日乘子算法,这些在程序里面都有很重要的应用。有的时候更要自行抽象数学模型,并且设计满足时间限制和空间限制的解法。能够抽象问题,解决问 题的,才是真正的技术系的高手。
        OK,上面,贝壳从两个方面(工程和技术)论述了程序员的高下之分,作为他胡诌的结果,他目前的水准大致是——不知道。并且很遗憾的告诉大家,目前贝壳能看到的就这么多,再上面是什么样子——要么等到了再告诉您?
    9/6/2008

    C++下的Variant

        所谓C++语言,是一种强类型语言。即是说,C++种的某个变量,在使用时类型是已经确定的。这个并不是设计者的喜好或者是偏心,而是C++中的变量都会 被翻译成准确的内存地址和大小,如果类型不确定是不可能处理的。但是在事实中,我们经常要处理一种"变类型"。例如,我们可能需要解析表达式,这个时候我 们可能用一个或者两个栈来解决这个问题。可栈里面塞的东西就精彩了,对象,函数,数据,都在里面。这时候,如果是python,我们可以直接用list, 他是弱类型的。但是C++怎么办?
        一般来说,我们会使用Variant类型来解决这个问题。这是C++面对对象机制和算子机制所派生出来的产物,能够让用户自行定义对象的行为。如果一个对 象,可以表现的像这个又像那个,那不就解决问题了?因此在COM中就有一个variant。不过贝壳看过机制,是一堆东西的集合,非常的不美丽。今天贝壳 又看到一个variant的实现,漂亮多了。
        废话少说,上代码。
    #include <string>
    using namespace std;
    #include <boost/any.hpp>
    using namespace boost;

    int _tmain(int argc, _TCHAR* argv[])
    {
        any a;
        a = 10;
        printf ("%s: %d\n", a.type ().name (), any_cast<int>(a));
        a = 10.5;
        printf ("%s: %f\n", a.type ().name (), any_cast<double>(a));
        a = string ("str");
        printf ("%s: %s\n", a.type ().name (), any_cast<string>(a).c_str ());
        return 0;
    }
        当类型错误时,出现bad_cast exception。
    8/27/2008

    python的性能问题

        贝壳最近在一个朋友的网站上看到了关于SICP零钱兑换问题的python求解,使用了记忆机制,然后他给出了代码。然而他的代码计时上有点小问题,也没 有用包装器(奇怪的是,有写),而且python的栈深度有限。因此贝壳做了几个修改的版本,需要测试下性能,下面就是关于性能的几个问题和过程。
        本文详细论述了python语言下和C++语言下使用各种方法测试代码性能的方法,以及粗略的关于两种语言不同算法性能对比。
    原始的python代码是这样的:
    def change_coins(money):
        first_denomination = {
            1:1,  2:5,
            3:10, 4:25,
            5:50,
        }
        def cc((amount, kinds_of_coins)):
            if amount == 0:
                return 1
            elif amount < 0 or kinds_of_coins == 0:
                return 0
            else:
                return     cc((amount, kinds_of_coins - 1)) \
                + cc((amount - first_denomination[kinds_of_coins], kinds_of_coins))
        print "change_coins return %s" % cc((money, 5));
        return ;
    利用记忆原理包装后是这样的:
    def memoiza(fun):
        cache = {}
        def proc ( *arg ):
            if cache.has_key(arg):
                return cache[arg]
            else:
                x = fun( *arg )
                cache[arg] = x
                return x
        return proc
           
    def decorator_change_coins(money):
        first_denomination = {
            1:1,  2:5,
            3:10, 4:25,
            5:50,
        }
        @memoiza
        def cc(amount, kinds_of_coins):
            if amount == 0:
                return 1
            elif amount < 0 or kinds_of_coins == 0:
                return 0
            else:
                return cc(amount, kinds_of_coins - 1) \
                + cc(amount - first_denomination[kinds_of_coins], kinds_of_coins)
        print "decorator_change_coins return %s" % cc(money, 5);
        return ;
    不记忆,利用栈模拟递归展开是这样的:
    def native_change_coins(money):
        first_denomination = {
            1:1,  2:5,
            3:10, 4:25,
            5:50,
        }
        stack = [(money, 5)];
        rslt = 0;
        while len (stack) > 0:
            param = stack.pop ();
            if param[0] == 0:
                rslt += 1;
                continue;
            elif param[0] < 0 or param[1] == 0:
                continue;
            else:
                stack.append ((param[0], param[1] - 1));
                stack.append ((param[0] - first_denomination[param[1]], param[1]));
                continue;
        print "native_change_coins return %s" % rslt;
        return ;

        贝壳主要需要测试上面三个代码的执行效率和瓶颈,所以贝壳用的主代码是这样的:
    import time
    import timeit
    import profile

    def test_func(f):
        f (300);

    if __name__ == "__main__":
        t = timeit.Timer("test_func (change_coins)", "from __main__ import *");
        print min(t.repeat (5, 1));
       
        t = timeit.Timer("test_func (decorator_change_coins)", "from __main__ import *");
        print min(t.repeat (5, 1));
       
        t = timeit.Timer("test_func (native_change_coins)", "from __main__ import *");
        print min(t.repeat (5, 1));

        profile.run("test_func (change_coins)");
        profile.run("test_func (decorator_change_coins)");
        profile.run("test_func (native_change_coins)");

        下面是部分结果:
    change_coins return 9590
    1.22809910198
    decorator_change_coins return 9590
    0.00217178440277
    native_change_coins return 9590
    2.69215193551

        以上是时间测试结果,使用timeit模块来测试运行时间,重复5次,取最小值。具体原理可以看dive into python,详细请看上面的代码。从结果中我们可以看到,使用记忆技术后,性能提升了500多倍,这是符合规律的。然而使用了集合模拟栈之后,性能大幅 下降。下面我们看看为什么。

    change_coins return 9590
             1292596 function calls (6 primitive calls) in 13.591 CPU seconds

       Ordered by: standard name

       ncalls  tottime  percall  cumtime  percall filename:lineno(function)
            1    0.059    0.059    0.059    0.059 :0(setprofile)
            1    0.000    0.000   13.533   13.533 <string>:1(<module>)
            1    0.000    0.000   13.533   13.533 amount.py:102(test_func)
    1292591/1   13.531    0.000   13.531   13.531 amount.py:11(cc)
            1    0.001    0.001   13.533   13.533 amount.py:5(change_coins)
            0    0.000             0.000          profile:0(profiler)
            1    0.000    0.000   13.591   13.591 profile:0(test_func (change_coins))

    decorator_change_coins return 9590
             2494 function calls (881 primitive calls) in 0.027 CPU seconds

       Ordered by: standard name

       ncalls  tottime  percall  cumtime  percall filename:lineno(function)
          873    0.004    0.000    0.004    0.000 :0(has_key)
            1    0.000    0.000    0.000    0.000 :0(setprofile)
            1    0.000    0.000    0.027    0.027 <string>:1(<module>)
            1    0.000    0.000    0.027    0.027 amount.py:102(test_func)
            1    0.000    0.000    0.000    0.000 amount.py:51(memoiza)
        873/1    0.013    0.000    0.026    0.026 amount.py:53(proc)
            1    0.001    0.001    0.027    0.027 amount.py:62(decorator_change_coins)
        742/1    0.009    0.000    0.026    0.026 amount.py:68(cc)
            0    0.000             0.000          profile:0(profiler)
            1    0.000    0.000    0.027    0.027 profile:0(test_func (decorator_change_coins))

    native_change_coins return 9590
             3877778 function calls in 38.798 CPU seconds

       Ordered by: standard name

       ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1292590    5.824    0.000    5.824    0.000 :0(append)
      1292592    5.960    0.000    5.960    0.000 :0(len)
      1292591    6.076    0.000    6.076    0.000 :0(pop)
            1    0.000    0.000    0.000    0.000 :0(setprofile)
            1    0.000    0.000   38.798   38.798 <string>:1(<module>)
            1    0.000    0.000   38.798   38.798 amount.py:102(test_func)
            1   20.938   20.938   38.798   38.798 amount.py:80(native_change_coins)
            0    0.000             0.000          profile:0(profiler)
            1    0.000    0.000   38.798   38.798 profile:0(test_func (native_change_coins))
        以上是白盒分析结果,使用profile测试,主要分析函数的调用花费。具体可以参考http://www.sqlite.com.cn /MySqlite/11/480.Html。从上面的报表中,我们可以看出,最初的函数执行时间全消耗在了cc上。而记忆后,则是proc和cc基本对 半,有的时候has_key测试也花点时间。这表示cc花费的时间大幅下降,记忆技术则花了比较多的时间。而模拟的呢?大部分时间都花在了 append,len,pop这三个函数上!这说明原始集合的效率严重制约了模拟效率。如果要提升性能的话,使用其他的集合吧。
        另外贝壳又用C++写了一个,如下:

    const int coin_map[] = {
        1, 5, 10, 25, 50
    };
    const int coin_count = 5;

    int cc (int amount, int kind_of_coins)
    {
        if (amount == 0)
            return 1;
        if (amount < 0 || kind_of_coins <= 0)
            return 0;
        return cc (amount, kind_of_coins - 1) + cc (amount - coin_map[kind_of_coins - 1], kind_of_coins);
    }

    int dd (int amount, int kind_of_coins)
    {
        if (amount == 0)
            return 1;
        if (amount < 0 || kind_of_coins <= 0)
            return 0;
        int rslt = 0;
        for (int i = 0; i <= amount / coin_map[kind_of_coins - 1]; ++i)
            rslt += dd (amount - i * coin_map[kind_of_coins - 1], kind_of_coins - 1);
        return rslt;
    }

    class keys{
    public:
        int amount;
        int kind_of_coins;
        keys (int amount_p, int kind_of_coins_p):
            amount(amount_p), kind_of_coins(kind_of_coins_p)
        {}
        bool operator == (const keys & k) const{
            return (amount == k.amount && kind_of_coins == k.kind_of_coins);
        }
        bool operator < (const keys & k) const{
            if (kind_of_coins == k.kind_of_coins)
                return amount < k.amount;
            return kind_of_coins < k.kind_of_coins;
        }
    };

    map<keys, int> mCache;

    int ee (int amount, int kind_of_coins)
    {
        if (amount == 0)
            return 1;
        if (amount < 0 || kind_of_coins <= 0)
            return 0;
        keys k (amount, kind_of_coins);
        map<keys, int>::iterator iter = mCache.find(k);
        if (iter != mCache.end ())
            return iter->second;
        int rslt = 0;
        for (int i = 0; i <= amount / coin_map[kind_of_coins - 1]; ++i)
            rslt += ee (amount - i * coin_map[kind_of_coins - 1], kind_of_coins - 1);
        mCache.insert(pair<keys, int>(k, rslt));
        return rslt;
    }

    int _tmain(int argc, _TCHAR* argv[])
    {
        const int loop_times = 300;
        clock_t s = clock();
        printf ("kind of coins: %d\n", cc (loop_times, coin_count));
        printf ("times:%d\n", clock () - s);

        s = clock();
        printf ("kind of coins: %d\n", dd (loop_times, coin_count));
        printf ("times:%d\n", clock () - s);

        s = clock();
        printf ("kind of coins: %d\n", ee (loop_times, coin_count));
        printf ("times:%d\n", clock () - s);
        return 0;
    }
        注意到主函数中,使用的是clock来计量时间。如果C++下要做白盒性能测试就比较麻烦,需要用精确计时函数和宏。需要的可以单独和我联系。下面是部分计算结果,cc的和ee的,没有dd的。
    300的计算结果
    kind of coins: 9590
    times:62
    kind of coins: 9590
    times:46
    1000的计算结果
    kind of coins: 801451
    times:15953
    kind of coins: 801451
    times:11000
        单位,ms。
        原生的效率差异是20倍,用了缓存后性能只有略略上升?!反而是python比较快?
        看来C++下的map效率也不高,要用hash_map才好。
        倒是栈长度好很多,贝壳估计是131072次调用,大约是16384分。

    纠错:贝壳在上文程序中65行的
    rslt += dd (amount - i * coin_map[kind_of_coins - 1], kind_of_coins - 1);
    应当为
    rslt += ee (amount - i * coin_map[kind_of_coins - 1], kind_of_coins - 1);
        由于代码错误,导致性能差异。另外,程序编译为Debug版,没有使用STLPort,导致性能下降。现在重新贴出结果。
    300的计算结果
    kind of coins: 9590
    times:62
    kind of coins: 9590
    times:0
    1000的计算结果
    kind of coins: 801451
    times:2812
    kind of coins: 801451
    times:15
    10000的计算结果(只有加速部分)
    kind of coins: -1795806091
    times:1921
        性能优势太明显了。
    8/3/2008

    程序员入门的12个问题

         以下题目是应一个朋友问而写的,适用于刚刚入门有志或者有需要做程序的朋友的题目。题目脱胎于日常编程中常见的一些问题,很多是贝壳实际碰到问题的变形。 题目不注重所用语言,每道题目可以用不同语言解决。有意思向计算机方向发展的可以试试用不同语言来解决,看看哪种语言最方便解决这种问题。如果打算增加难 度的话,请使用C++来做,并且尽量抽象复用。在这个过程中积累下来的可复用代码会对以后编程有很大帮助。

    1.读出文件中的以下格式内容,计算逆矩阵,并按照同样格式输出。
    1 2 4.5
    3 0 1
    9 5 2
    数字间以空格分割,行以回车分割。
    难点:
    输入和输出应当可以选择是键盘输入还是文件输入,输出到屏幕还是输出到文件。
    逆矩阵计算中有可能求不出,出现除零。设法避免直接的报错。
    评价:
    很中规中矩的一个问题,有点竞赛的味道。只要做过程序的人一般不会失手。

    2.某个XML,其中记录了一些信息。信息是按照时间-地点-人物的顺序记录的,例子如下:
    <happen time="20080101">
        <location place="bus">
            <who name="jack">
                ...
            </who>
        </location>
    </happen>
    现在需要你颠倒一下,变成这样的:
    <who name="jack">
        <location place="bus">
            <happen time="20080101">
                ...
            </happen>
        </location>
    </who>
    难点:
    看看能想出多少解决问题的方法。
    试试尽量减小内存消耗。
    评价:
    解决问题的方法很多,比较一下这些方法的优劣。
    有一年以上程序经验的就可以最终解决,但要解决的比较完善需要两到三年经验。

    3.下载google的首页,跟踪二级连接(二级连接,就是首页中连接指向的页面,上面连接指向的页面)。
    并计算其中所有页面,显示出的非空白字符的个数。(显示的文字中的非空白字符)
    难点:
    试试看跟踪js脚本链接。
    登陆后的google首页是不一样的,包括提示,语言类型,设法统计登陆后的首页。
    如果是多级呢?
    评价:
    宽度优先和深度优先算法的应用,对集合运算有一定要求。
    重点在于获取和处理html页面的方法。
    一年以上即可解决,完善程度和技术水平关系不大。

    4.运行两个程序,A和B,将A的输出输入到B中。
    难点:
    需要等待A的输出和B的输入,以及程序的终止条件。
    评价:
    需要对系统熟悉,知道管道和用法。知道进程间交互的API。
    需要研究过系统,程序水平没有要求。

    5.遍历某个目录,找出其中的特定图片文件。
    难点:
    怎么分析图片文件?文件名是比较粗略的方法,更好的是使用文件签名分析。
    下次遍历的时候速度怎么提高(假定文件不变化)。
    评价:
    还是深度和宽度搜索题目,分析文件是难点。
    扩展要求对于数据缓存有一定要求。
    一年以上即可解决,文件签名分析看个人水平。

    6.监视某个目录的变化,将新加入的mp3的相关信息(IDv3)邮件发给我。
    难点:
    怎么监视目录变化?
    怎么提取MP3的内容?
    怎么发邮件?
    怎么保证不漏内容。
    评价:
    要对系统熟悉,了解mp3格式或者能够自行寻找库扩展语言。
    了解邮件发送协议,或者能使用系统库发送邮件。
    两年以上可解决,完善需三年以上水准。

    7.写一个程序,可以计算加减乘除,支持括号。
    难点:
    让你的程序算算1+2*3,看看是多少。正确应当是7,设计不良是9。
    看看你的程序,2/6*3得多少,是不是1.0(最好是1)。
    让你的程序设法支持乘方和函数。
    评价:
    对数据结构和算法要求很高。
    一年以上可解决,要扩展支持算符和算法,需要三年水准。

    8.画一只乌龟,保存为图片。
    难点:
    让用户动手画?
    试试保存为各种格式的图片。
    评价:
    实用项目,按照书本教程最多12小时就可以掌握。
    然而需要自行解决并做好,至少一年以上。

    9.写一个小桌球程序,让一个白球设法打落一个黑球。
    难点:
    注意屏幕闪烁。
    写个9球游戏如何?
    评价:
    对物理知识有一定要求,对游戏常识(双缓冲)有一定了解。
    扩展要求了解桌球规则。
    两年以上水准。

    10.写个小的管帐系统,告诉我这个月你钱是怎么花出去的。
    难点:
    随时记账。
    评价:
    系统并不难做,然而考虑实用性后,很少有系统能得到好成绩。

    11.写一个小程序,向一个文件的第11字节写一个A,并将程序编译好后的大小控制在2K以内。(不得使用虚拟,脚本语言完成)
    难点:
    控制大小。
    评价:
    对可执行程序的结构和优化,系统库结构有相当了解,这是为C++语言设计的题目。
    三年以上水准。

    12.写个小阅读器。
    难点:
    能读HTML么?繁体呢?
    小心编码问题。
    ZIP包内的文件能读么?
    评价:
    最终的实用项目。
    一年就可以做,但是做好至少要三年以上水准。

    7/31/2008

    avast4 collide with ext2ifs

    Affected Product:
        Avast4 home edition
        ext2ifs 1.10c
        ext2ifs 1.11
    Description:
        avast4 home edition is a free anti-virus tools. In 2008-07-30 it update some files, include some file called 'aswSP.sys'. According infomation in autoruns, it's avast self protection module.
    [Here is info from autoruns.]
    aswSPavast! self protection module    ALWIL Software    c:\windows\system32\drivers\aswsp.sys
    [Here is info from update-log]
    2008-7-30 7:36:14    file        Direct move of file: C:\Program Files\Alwil Software\Avast4\Setup\INF\AMD64\aswSP.sys
    2008-7-30 7:36:14    file        Installed file:C:\Program Files\Alwil Software\Avast4\Setup\INF\AMD64\aswSP.sys
    2008-7-30 7:36:14    file        Direct move of file: C:\Program Files\Alwil Software\Avast4\Setup\INF\aswSP.sys
    2008-7-30 7:36:59    system        Reboot set by changed resident C:\WINDOWS\system32\drivers\aswSP.sys
    2008-7-30 7:36:59    system        Driver file copied: C:\WINDOWS\system32\drivers\aswSP.sys
        If u use ext2ifs in system for share date with linux, it'll cause system crash with code BAD_POOL_CALLER. There is not evidence show it has connections with ext2ifs, but the crash always happen when I try to access data in a disk use ext2ifs. When I copy data to ntfs disk, it'll be all right. Here is dump analyze.
    *******************************************************************************
    *                                                                             *
    *                        Bugcheck Analysis                                    *
    *                                                                             *
    *******************************************************************************

    BAD_POOL_CALLER (c2)
    The current thread is making a bad pool request.  Typically this is at a bad IRQL level or double freeing the same allocation, etc.
    Arguments:
    Arg1: 00000007, Attempt to free pool which was already freed
    Arg2: 00000cd4, (reserved)
    Arg3: 04030401, Memory contents of the pool block
    Arg4: e13a7258, Address of the block of pool being deallocated

    Debugging Details:
    ------------------


    POOL_ADDRESS:  e13a7258

    FREED_POOL_TAG:  pSsA

    BUGCHECK_STR:  0xc2_7_pSsA

    CUSTOMER_CRASH_COUNT:  1

    DEFAULT_BUCKET_ID:  DRIVER_FAULT

    PROCESS_NAME:  _uninst.exe

    LAST_CONTROL_TRANSFER:  from 80544e86 to 804f9aef

    STACK_TEXT: 
    eb364b68 80544e86 000000c2 00000007 00000cd4 nt!KeBugCheckEx+0x1b
    eb364bb8 ee072a0a e13a7258 00000000 8055a584 nt!ExFreePoolWithTag+0x2a0
    WARNING: Stack unwind information not available. Following frames may be wrong.
    eb364be4 805c5e1c 00000730 0000016c eb364cdc aswSP+0x5a0a
    eb364c04 80639346 e3986008 0000016c eb364cdc nt!PsCallImageNotifyRoutines+0x36
    eb364d08 805c5bcd 7c810665 00000000 00000000 nt!DbgkCreateThread+0xa2
    eb364d50 805421c2 00000000 7c810665 00000001 nt!PspUserThreadStartup+0x9d
    00000000 00000000 00000000 00000000 00000000 nt!KiThreadStartup+0x16


    STACK_COMMAND:  kb

    FOLLOWUP_IP:
    aswSP+5a0a
    ee072a0a ??              ???

    SYMBOL_STACK_INDEX:  2

    SYMBOL_NAME:  aswSP+5a0a

    FOLLOWUP_NAME:  MachineOwner

    MODULE_NAME: aswSP

    IMAGE_NAME:  aswSP.SYS

    DEBUG_FLR_IMAGE_TIMESTAMP:  4881fba3

    FAILURE_BUCKET_ID:  0xc2_7_pSsA_aswSP+5a0a

    BUCKET_ID:  0xc2_7_pSsA_aswSP+5a0a

    Followup: MachineOwner

        The crash happened in aswSP+5a0a.

    Resolve solution:
        There is not solution to resolve now. Uninstall avast, or uninstall ext2ifs.

    以上内容的中文注释:
        不要同时使用avast4和ext2ifs,尤其在今天的更新后。
        会使用ext2ifs的,上面的东西应该也看得懂了,其余不翻译。
    7/10/2008

    新种病毒出现

        有新种病毒出现,大家当心。
        病毒症状如下:
        有MSN好友给你传一个网址,如同http://[用户名].imagecroco.info/。(贝壳注:现在已经被Mozilla列为欺诈网址)当浏览后中毒,会继续给好友发送网址。发送网址时用户离线,发消息用户不回复。中毒用户提示已经在另外一个地址上登录。
        机理估计如下:
        当你访问网站时,会被要求输入用户名或密码。或者被挂上马,等登录时被套出用户名和密码。当你不使用时,服务器会自动使用你的用户名登录,给你的好友发送病毒。如果不修改密码,即使本机清理病毒或者设置名称提醒也未必有用。
    5/11/2008

    python的几个改进

        首先需要增加的就是kill掉线程的方法,目前我们统统是调用系统函数。有没有搞错阿,需要针对系统写代码不说,还不安全。在线程关闭的过程中没有辗转开解和安全捕获。从最安全的角度上说,要关闭线程最方便的就是给其他线程抛异常。python并非不可以给其他线程抛异常,可非常麻烦不说,具体执行的时候发现,其实根本不是抛异常,而是在执行过程中检查异常。这样当程序在调用外部代码的时候死循环,想kill线程的时候根本不可行。所以安全的关闭线程的异常和直接kill掉线程的方法都要有。
        其次,这东西没有什么可以快速辅助处理集合的工具类,例如STL中的set_union等等。虽说每个都不难,可是统一的实现和各自的实现毕竟是有差别的。很多时候,我们只需要抽象的计算两个集合,一个和一个的交集,就OK了。
    5/6/2008

    反射的几个类型

        所谓反射,其实就是在运行时可以获得代码数据的技术,算是面对对象编程语言的专利。从这个意义上说,反射可以分为三个类型。
        头一类是RTTI,其实这根本不算反射,本质上只能说多态。RTTI是一种鉴别某个对象是否为某个类的派生实例的技术,在C++中就有实现。简单的方法就是实现一个特定的虚函数,将当前对象所属的类虚函数表和所属父类的虚函数表一一返回。这样对比某个类的虚函数表,就可以知道是否为派生实例了。支持RTTI,程序才算真正支持了面对对象,而反射则是更高一层的技术。
        第二类就是在C#和Java中盛行的反射技术,这种技术的核心在于可以通过名称寻找到对象。例如,我们可以寻找到一个叫做abc的对象,枚举其中的成员和方法,并且执行调用,这才是反射最大的意义。当我们遇到不同的数据输入时,我们可以调用不同的方法来处理这个数据,并且这个过程是动态配置的。而在C++中,我们无法通过编译器支持这个能力,必须手工的建立一个名称和一个对象的关联关系表,在合适的时候通过这个表,获得某个名称的函数入口指针。其实C#和Java中实现的方法和VM息息相关,他们的代码在目标文件中还保持着命名空间-类-对象的结构,Java还进一步的保留了源码(只是被翻译为了更快的P代码),而C#只保留了IL代码。这样VM在执行的时候自然可以很轻松的找到对应的函数,并且获得函数签名。而C类语言的特征是汇编时代的"符号链接"方式,编译的时候保有符号,完成链接就没了。
        中间插一句,其实我们完全可以写一个只支持高阶语言的系统。这样的系统未必高效,可一定方便阿。
        最后一种则是python中的系统,当用户调用一个类中的函数的时候,使用一个专门的函数来决定调用哪个。因此当对付SOAP这种东西的时候,python可以直接上。而C#,Java,C++都要通过工具生成代理方法。再用代理方法去调用公共函数库,实现调用。因为python直接将调用定向到了一个统一的函数上,所以压根不需要这步。不过这步的代价是严重的性能问题,因为每次函数调用都要去检查调用目标。python是纯脚本语言,占了这点便宜,所以才能这么干。
    5/2/2008

    C++继承,虚,转换规则探究

        以下讨论的东西都是在VS2005下跑出来的,如果想知道别的编译器规则,请照跑一遍。以下是类定义,函数内容为打印出当前函数名称,所以就不再贴了。
    class Base
    {
    public:
        Base();
        Base(const Base & o);
        virtual ~Base();
        virtual Base & operator = (const Base & o);

        void function1();
        virtual void function2();
        void function3();
        virtual void function4();
        //virtual void function5();
        virtual void function6();
    };
    class Derive : public Base
    {
    public:
        Derive();
        Derive(const Derive & o);
        virtual ~Derive();
        virtual Derive & operator = (const Derive & o);

        void function1();
        virtual void function2();
        virtual void function3();
        void function4();
        //compiler error
        //int function5();
    protected:
        virtual void function6();
    public:
    };
        首先我们讨论继承下的构造/析构顺序。
    pa = dynamic_cast<Base*>(new Derive ());
    delete pa;
    Base::Base
    Derive::Derive
    Derive::~Derive
    Base::~Base
        关于这段代码多说两句,如果我们把class Derive : public Base中的public删除,就会出现C2243错误,看来默认是私有继承。
        先是基类构造,然后是继承类构造。先是继承类析够,然后是基类析够。然后我们将virtual ~Base();的virtual删除,结果就变成了。
    Base::Base
    Derive::Derive
    Base::~Base
        注意继承类的析构没了。所以如果你打算让人继承你的类,记得将类的析构改成virtual,否则他怎么写析构都不会被调用的。
        然后是虚函数继承。
    pa->function1 ();
    pa->function2 ();
    pa->function3 ();
    pa->function4 ();
        结果是这样。
    Base::function1
    Derive::function2
    Base::function3
    Derive::function4
    Derive::function6
        看来,虚特性出来不出来完全看基类。注意到上面的function5么?假设你继承了一个类,打算写一个函数,和基类里面的某个虚函数具有一样的名称和参数,但是返回不一样。嘟嘟~~抱歉,编译器错误。而且注意function6,即使在继承类中声明说这是保护函数,也可以通过公开的基类函数的虚特性进行调用。
        下面我们要说一下拷贝构造函数,这不可避免的要说到定义。
    Derive::Derive(const Derive & o)
    {
        printf ("Derive::Derive copy constructer\n");
    }
        猜猜这个会出什么结果?
    Base::Base
    Derive::Derive copy constructer
        要是经常看我blog的人就不会意外,继承类的拷贝构造函数调用的是基类的普通构造函数。如果你打算让基类也拷贝构造,那这么做。
    Derive::Derive(const Derive & o):Base (o)
    {
        printf ("Derive::Derive copy constructer\n");
    }
        然后是拷贝构造函数的使用时机。运行代码如下,我们逐步分析。
    Base ta = *pa;
    Base::Base copy constructer
    Base::~Base
        当对象声明时,如果加一个=,则以=后的对象来构造当前对象,这是拷贝构造的第一个用法。
    Derive tb = *static_cast<Derive*>(pa);
    Base::Base copy constructer
    Derive::Derive copy constructer
    Derive::~Derive
    Base::~Base
        当然,如果我们声明继承类的时候,一样拷贝构造。
    //compiler error
    //Derive tc = ta;
        当我们试图用基类构造继承类的时候,理所当然的,出错了。
    void test1 (Base &)
    {
        printf ("test1\n");
    }
    test1(*pa);
    输出:test1
        如果我们以一个对象调用的时候,如果是引用,当然是不拷贝的。
    void test2 (Base)
    {
        printf ("test2\n");
    }
    test2(*pa);
    Base::Base copy constructer
    test2
    Base::~Base
        如果是直接调用,首先是拷贝构造,然后调用,最后析构。
    Base& test3 ()
    {
        printf ("test3\n");
        return Base ();
    }
    pb = &test3();
    test3
    Base::Base
    Base::~Base
        当返回对象引用的时候,只有很正常的构造和析构。
    Base test4 ()
    {
        printf ("test4\n");
        return Base ();
    }
    pb = &test4();
    test4
    Base::Base
    Base::~Base
        返回对象本身的话,哎,怎么会这样?
        熟悉语言的应该看出来了,return Base ();的时候,先跑了一次构造,建立在栈里面,返回的时候要copy到堆中。拷贝构造呢?
        这就是传说中的返回构造优化拉,直接构造在堆上面,省掉一次copy,下面我们看看原始的状态。
    Base& test5 ()
    {
        Base b;
        printf ("test5\n");
        return b;
    }
    pb = &test5();
    Base test6 ()
    {
        Base b;
        printf ("test6\n");
        return b;
    }
    pb = &test6();
    Base::Base
    test5
    Base::~Base
    Base::Base
    test6
    Base::Base copy constructer
    Base::~Base
    Base::~Base
        大家看到了?5的时候先构造,再传回,和返回对象引用的时候行为一致。6的时候可没有返回构造优化,于是先构造,然后拷贝。删除的时候先删除原始对象,再删除拷贝对象,大家可以自行证实这点。
        我们再修改上面的调用为下面的。
    Base td = test5();
    Base::Base
    test5
    Base::~Base
    Base::Base copy constructer
    Base::~Base
        首先是5的构造,析构,然后才是td的拷贝构造,析构。这个顺序,熟悉语言的人应该感觉到奇怪了吧。按照推论,应当是先拷贝再析构的。如果你这么觉得,还是先看完下面的东西吧。
    Base te = test6();
    Base::Base
    test6
    Base::Base copy constructer
    Base::~Base
    Base::~Base
        这才是预计的顺序。注意,这里并没有调用两次拷贝构造。虽然贝壳并不了解机制,不过估计又是一种返回构造优化。
        5中例子觉得迷惑的人,不妨在拷贝构造里面打个断点,看看你copy的对象是什么,无效对象!!!!
        返回引用的情况下,一旦返回对象的生命周期结束了,返回的数据就无法保证有效。因此返回局部对象是非常危险的,唯一的里外就是3例子中在返回的时候构造一个新的对象而引发的返回构造优化。
        下面是拷贝构造和operator =的区别和调用时间。
    Base ya = *pa;
    Base yb;
    yb = *pa;
    Base::Base copy constructer
    Base::Base
    Base::operator =
    Base::~Base
    Base::~Base
        上面一个是拷贝构造,下面一个是普通构造加operator =。
        最后是全部的定义和源码,类的定义参考最上面的。
    void test1 (Base &)
    {
        printf ("test1\n");
    }
    void test2 (Base)
    {
        printf ("test2\n");
    }
    Base& test3 ()
    {
        printf ("test3\n");
        return Base ();
    }
    Base test4 ()
    {
        printf ("test4\n");
        return Base ();
    }
    Base& test5 ()
    {
        Base b;
        printf ("test5\n");
        return b;
    }
    Base test6 ()
    {
        Base b;
        printf ("test6\n");
        return b;
    }
    int _tmain(int argc, _TCHAR* argv[])
    {
        Base *pa, *pb;
       
        pa = dynamic_cast<Base*>(new Derive ());

        // test inherit function rule
        //pa->function1 ();
        //pa->function2 ();
        //pa->function3 ();
        //pa->function4 ();
        //pa->function6 ();

        //test copy constructer
        //pb = dynamic_cast<Base*>(new Derive (*static_cast<Derive*>(pa)));
        //delete pb;
        //Base ta = *pa;
        //Derive tb = *static_cast<Derive*>(pa);
        //compiler error
        //Derive tc = ta;
        //test1(*pa);
        //test2(*pa);
        //pb = &test3();
        //pb = &test4();
        //pb = &test5();
        //pb = &test6();
        //Base td = test5();
        //Base te = test6();

        //diffrence between copy cotr and operator =
        //Base ya = *pa;
        //Base yb;
        //yb = *pa;

        delete pa;
        return 0;
    }
    4/8/2008

    python的非经典错误

    def comp_tuple_file (tuple_file1, tuple_file2):
        for i in tuple_file1:
            if i in tuple_file2:
                tuple_file1.remove(i);
                tuple_file2.remove(i);
    if __name__=="__main__":
        t1=[(1,"1"),(2,"2"),(3,"3")];
        t2=[(1,"1"),(3,"3"),(2,"2"),(4,"2")];
        comp_tuple_file (t1, t2);
        print t1;
        print t2;
        错在哪里?
        头一次循环,i=(1,"1")被正确移除了。但是接下来,i=(3,"3")?
        这个叠代器的行为很有意思哦,貌似叠代器内存储的是集合的索引。
    def comp_tuple_file (tuple_file1, tuple_file2):
        collection=tuple_file1[:];
        for i in collection:
            if i in tuple_file2:
                tuple_file1.remove(i);
                tuple_file2.remove(i);
    if __name__=="__main__":
        t1=[(1,"1"),(2,"2"),(3,"3")];
        t2=[(1,"1"),(3,"3"),(2,"2"),(4,"2")];
        comp_tuple_file (t1, t2);
        print t1;
        print t2;
        这才是正确的代码。
    4/5/2008

    链接上的问题

        贝壳最近在用库上吃了不少苦头,先是crypto++5.52。编译后怎么也链接不上。后来发现需要用/MT参数编译为多线程。后来又在STLport上又吃一次苦头,可见VC2003的默认单线程模式确实不得人心。
        下面说一下STL的编译手记。下载STLport,解压。运行vcvars32.bat设置环境变量,去build/lib下面,运行configuare -c msvc71(如果你是2003,否则按configuare --help察看你的编译器类型)。然后运行nmake -f msvc.mak install。可以看到有两个目录被建立了,bin和lib。把bin的复制到windows/system32下面,把lib的复制到系统目录下面。安装就OK了。
        上述和boost都差不多,然而和boost不一样的是,编写程序的时候,需要手工指定stlport的头文件路径。boost的可以以<boost/xxx.h>来引入,因此boost的头可以复制到系统里面去。然而stlport的必须以手工方式指定,否则就要覆盖默认的stl了。
    4/1/2008

    显示自身的代码

        void main(){char* a="void main(){char* a=%c%s%c;printf (a,34,a,34);}";printf(a,34,a,34);}
        核心是使用printf(a,a)来代换显示,并且用34来规避\转换。当然,完整的要带include,稍微有点区别。