十年码工路——大学篇

看了@tinyfool同学的从业总结,想想自己也工作10年了,一时手痒,不顾东施效颦之忌,也准备写上几篇,以做纪念 ...


转眼间工作10年了 …

我是96年考入天津大学计算机系的,上大学之前仅在高一时短暂的接触过Basic,老师手里的5寸软盘只见过没摸过。高三的暑假,找了一本薄薄的C++的书硬啃,只记得了一点点点的皮毛,知道了有这么一门带class的编程语言。刚入学,老师给我们扫盲时,看到老师手里的3寸软盘,觉得好生羡慕,从贩假软盘的师兄手里买过一盒10张的软盘(当时不知是假,Maxwell,好牌子的)。和其他有一定基础的同学相比,简直是相差太远了;许多同学是用过C的,也有同学是参加过编程比赛的。仗着在小霸王学习机上突击学的指法,还不至于在TT(80后的同学们可能没机会用到了)练习上被甩下太远。

刚上大学时,觉得学习是次要的,锻炼自己的各方面能力更重要,凡是社团活动都积极参加。大学第一学期期中考试,险些挂科,终于警醒,开始上晚自习了。第一学期期末考试时,终于追赶上来,在班里排在4名女生的后面,加上其他乱七八糟的总评分,混了个班级第三。下半学年,和同寝的哥们儿凑分子,买了年级里的第一台电脑(二手的奔75,好像是6或8M内存),去取电脑时,南开的师兄给我们show了一把红警,把我们都给震住了。其他寝室的同学们也陆续买了机器,大家用同轴线缆连局域网打红警,晚上接走廊的灯泡偷电通宵打游戏。走廊里的电据说是半副的交流电,CRT屏幕接上电四角会有奇怪的彩虹纹,而且有源音箱一接就会烧。我们寝室,慢慢从最整洁寝室,变成了最脏乱差寝室。原先班里开班会,都是到我们寝室来的。后来,大家懒到晚上洗完脚都不倒洗脚水的地步了,地上堆满了报纸和烟头,呵呵 …

在校期间,正式学习的第一门编程语言是Pascal,第二门是汇编,数据结构也是基于Pascal的,甚至大二暑假实习的时候要用Pascal写一个Pascal的小型编译器(很遗憾我当时被借调到实验室去做项目了,没赶上)。大二时,为了进学校的IBM中心实验室,我自学了点C/C++和Java。学C++时,找了本《Visual C++从入门到精通》,照着书上的例子敲进去,呼呼呼,编译器报了好几百个错,仔细地一点一点查看,有的是自己的typo,也有是书籍的印刷错误。从此,我就特别痛恨书名是从入门到精通的书籍,绝对是伪劣书刊的代名词。在实验室给老师做项目,还是学了不少东西,我们一帮同学还给实验室写了个网站,参加全国高校IBM中心实验室的比赛,拿过个一等奖呢。当时去参赛时,老师是把网站的前端后台刻了张光盘带去参赛的,呵呵…

在实验室一开始,跟着两个高我一年的师兄搞Java+VRML,大三下半年的时候,还帮两个研究生做毕设来着,由那开始,我就特瞧不上研究生教育,决心坚决不上研究生。后来,大四上半年的时候,阴差阳错和同学参加了一个语音传输的小比赛,是一个什么日本公司赞助我们学校搞的。当时用了多线程、多播等,调用Windows ACM来压缩音频,还自己臆想了一个类似freelist的缓冲区管理。据说我们这一组总评排第三来着,后来为了鼓励本科生的参与精神,颁给我们一个一等奖。到毕业设计时,我和搭档开始弄视频传输,主要也就是用Windows VCM来压缩视频,据说后来评了个优秀毕业设计,还据说当年老师手下几个研究生在继续完善我们的系统,拿这个东西毕业呢。

我很痛恨学校里的教育方式,总体来说是,学不“知”用。还好二年级就学了基础物理,要不然我都不知道微积分有啥用。还有计算数学,虽然会用到微积分,但是还是不明白,计算数学是干啥用的。典型的还有,线性代数、概率论等等。现在回过头来看,这些数学多重要啊,真后悔当初没好好学。好不容易离散数学是真章吧,老师又忒离散,把我们这些学生搞得神经分裂,最后才考了60多分。如果当初能从一个比较前沿的应用领域入手,例如基于统计的自然语言处理,我们肯定会有很大的兴趣,把这些基础学科都学好。还有C语言,大三才开始教,我仗着自己C++/Java都很熟,满不在乎,结果考试时尽是考查错题和输出题,险些不及格。害得我找工作时,每每说自己擅长C/C++时,都被很严格的盘问…

在学校4年,最大的收获是,交了女朋友,也是我现在的老婆。我们是同班同学,从大二开始确定恋爱关系。在那时,我们是很认真的、本着将来结婚组建家庭来谈恋爱的。当时追她的时候,在食堂凑到她身边吃饭,因为我吃得快,她吃得慢,为了不至于我吃完了不好意思不闪人,我那个时期的饭量很大,吃7两米饭,然后还喝一大盆粥。大四时,她希望考研究生的,我就帮她复习,也去陪考了。最后她如愿考上了本校的研究生。我的总分太低,英语还不及格,以至于实验室的老师本来想特招我,都没办法。毕业后,我就到北京工作了,两地分开的两年半,我的电话费花了很多,呵呵…

现在想想,如果不是在校期间就情定终身,我们这种社交圈子小而又小的IT民工,还真很容易变成剩男剩女。现在,我们结婚已7年,小孩儿都快3岁了…

借助shared_ptr模拟RCU(read-copy-update)

RCU较RWL有更好的性能,其读操作几乎是free的;writer需要先对原始数据做一份拷贝,再进行修改(在同一时刻只能有一个writer),完成之后替换掉原来的指针(如果swap不是原子性操作,需要critical section的保护);然后,由reclaimer在适当的时机将原始数据占用的内存释放掉。这样,writer不像RWL会因为有reader而被阻塞。RCU和RWL一样,比较适用于读多写少的情景。

RCU多见于操作系统的内核中,也有user-space的实现可供参考(liburcu)。个人觉得,如果将reclaimer的回收工作分摊到某个reader或writer上,借用tr1::shared_ptr,应该可以很方便的实现。下面是实现的代码,我对并发编程经验尚浅,一直拿不太准,是否正确实现了,贴出来请大家多指正!

基本思路是这样的,reader可以通过get_reading_copy()获得当前数据的shared_ptr(记为Generation 1,G1);不过当m_is_swapping为1时,要阻塞等待。writer通过get_updating_copy()得到当前数据的一个副本(记为Generation 2,G2),由m_is_writing进行保护,只允许有一个写者进入;在更新完成之后,writer调用update()将这个副本的shared_ptr传回来,然后通过swap操作令m_data_ptr指向新的数据(G2),然后打开m_is_writing和m_is_swapping。writer持有的那个shared_ptr在调用update()之后指向了原始数据(G1),之前reader(s)持有的shared_ptr(s)也同样指向的是原始数据(G1),当这些shared_ptr(s)统统被析构时,会释放掉原始数据所占用的内存(可能发生在writer或reader上)。新的reader则会获得新数据的shared_ptr。如果有另外一个writer进入,得到的是新数据的副本(记为Generation 3,G3)。因此,如果指向G1的那些shared_ptr(s)还没有被完全析构时,有可能存在多个不同代(generations)的数据副本。

template <typename T>
class rcu_protected
{
public:
    typedef T                                   type;
    typedef const T                             const_type;
    typedef std::tr1::shared_ptr<type>          rcu_pointer;
    typedef std::tr1::shared_ptr<const_type>    rcu_const_pointer;
    rcu_protected() : m_data_ptr (new type()) {}
    rcu_protected(T* data) : m_data_ptr (data) {}
    rcu_const_pointer get_reading_copy ()
    {
        LockGuard< CRWLock,
                  &CRWLock::read_lock,
                  &CRWLock::read_unlock> l_guard (m_ptr_lock);
        return m_data_ptr;
    }
    rcu_pointer get_updating_copy ()
    {
        m_writer_lock.write_lock();
        LockGuard< CRWLock,
                  &CRWLock::read_lock,
                  &CRWLock::read_unlock> l_guard (m_ptr_lock);
        return rcu_pointer(new type(*m_data_ptr));
    }
    void update (rcu_pointer new_data_ptr)
    {
        LockGuard< CRWLock,
                  &CRWLock::write_lock,
                  &CRWLock::write_unlock> l_guard (m_ptr_lock);
        m_data_ptr.swap (new_data_ptr);
        m_writer_lock.write_unlock();
    }
private:
    CRWLock     m_ptr_lock;
    CRWLock     m_writer_lock;
    rcu_pointer m_data_ptr;
};

自注:

  1. 在VC2005之后(包括2005),编译器对volatile变量的访问会自动加fence,所以那两个barrier(s)可以省去。
  2. 其实在update()中,直接将new_data_ptr赋值给m_data_ptr应该也是可以的,但是可能导致在update()中去析构m_data_ptr指向的数据,而我们应该让update()能尽可能快的完成。
  3. 感谢Dmitry Vyukovreview,我遗漏了一种简单的情况,当reader等到m_is_swapping为0,准备拷贝指针时,突然又有writer线程进入要update指针,这就可能导致crash了… 应该用一个rw_lock将读和写保护起来 … 教训啊,任何关键数据的读写,都应该是全程保护的 …

更新SunPinyin-MacOS-2.0.2 beta 1 (10.5/10.6)

本此更新的主要内容包括:

  1. 为全拼切分器加入了模糊切分的功能,即根据上下文将fangan自动切分为fang'an或fan'gan。
  2. 为双拼加入了南方模糊音的功能。
  3. 将删除用户自造词的快捷键改为ctrl+command+num,以避免和Space的快捷键相冲突。

上述的一些功能,虽已大体稳定,但尚未经过严格的测试;另外还有其他一些bug fixes,也计划在2.0.2中加入。欢迎有兴趣尝鲜的朋友下载试用,SunPinyin-MacOS-2.0.2-beta1.zip。已安装2.0/2.0.1版本的朋友,无需删除已安装的版本,直接运行安装程序即可。

首次安装的朋友请注意,当安装程序进行到“下载数据文件”步骤时,请点击“开始…”按钮下载必要的数据文件(文件较大,可能比较耗时)。

Mastering Python in 2 hours

这是准备向同事介绍Python v2.x的Slides,很抱歉我成了标题党,“mastering”的说法实在是言过其实的。我只是把一些python常用的用法,以及例如decorator和metaclass等不是很常用但比较常见的用法,罗列在一起。总体还是比较粗糙,也不免有很多遗漏和误谬,请大家多指正了 ...

SunPinyin-2.0模糊音节切分的实现

模糊切分,即根据上下文,将有歧义的拼音字符串进行自动切分,例如,将fangan切分为fang'an或fan'gan。这是许多现代拼音输入法都具备的功能。SunPinyin的ime-core本身具有搜索多种切分组合的能力,只要在buildLattice时,保证传入的segments是按照起始位置(m_start)排好顺序的即可。

那么首先要解决的问题就是,根据得到的最佳句子,反查到对应的切分序列。例如,最佳句子是“我的方案获得通过”,可以推得“wo'de'fang'an'huo'de'tong'guo”。

我们为TLexiconState加入了m_seg_path的成员,用来记录这个LexiconState对应的切分路径,例如我们有一个lexiconState对应是上例中的fang'an,其切分路径是[4, 8, 10]。然后,为CLatticeState加入了m_pLexiconState成员,用来记录之前transfer时所引用的那个lexiconState。这样,在backTrace最佳句子的时候,就可以得到对应的音节了。由于易混淆音(即z<->zh)的存在,一个seg_path可能对应多个syllable_paths(m_syls);但是最后在存入用户词典时,必须要知道真实的syllables,所以没有采用TLexiconState结构包含一个seg_path、和多个syllable_paths的方案。

接下来,就是该如何产生模糊切分的了。我们为CPinyinData类加入了一些额外的table,这些table都是使用pinyin_data.py脚本生成的。包括fuzzy_finals_map,这是为处理型如xian->xian/xi'an的模糊切分的;以及fuzzy_pre_syllables和fuzzy_pro_syllables,分别代表可能产生切分歧义的前一个音节和后一个音节。经过pinyin_data.py的筛选,发现只有“r/g/n”三个声母,可能作为前一个音节的结尾,或后一个音节的声母。

接下来,我们为CQuanpinSegmentor加入了一个新的helper functor,CGetFuzzySegmentsOp。这个助手类的输入是主切分器(即基于double-array trie的、改良的、最大后向匹配算法)所得到的segments;输出是,其对应的模糊切分segments。这个寻找模糊切分的过程,和切分器的主过程是平行的。但是,我们并不是简单的每次输入都从头到尾扫描一遍。

首先,根据主切分序列的最后一个segment(记为seg),invalidate那些受影响的fuzzy segments。在我们目前的实现中,fuzzy_segs中的模糊切分都是成对出现的,我们从后向前一对一对的进行筛选,只有当某一对的右边界(r),小于或等于seg的起始位置,才能够保留下来。然后,我们仅需要对主切分的最后一个切分(如果是xian的情况),或最近两个切分(如果是fangan的情况),进行处理。然后小心地调整好updatedFrom的值,并返回给CQuanpinSegmentor::_push()。

因此,如果输入xian,会生成xian和xi'an的两种切分,如果继续输入一个a,则会得到xia'na和xian'a,而不会包括xi'an'a这个切分了。主切分器的最后一个segment(即na),会先将xi'an给invalidate掉;然后辅助切分器会将xian'a加入到fuzzy_segs中。从这一点来说,我们和google和sogou输入法的处理仍然有所不同,他们都会保留xi'an'a这个切分。

我个人的感觉,sogou和google输入法每次在追加或删除一个拼音字符时,都是会从头进行一遍扫描处理;其间处理了各种情况,包括易混淆音,自动就错,和模糊音节切分等。而SunPinyin的push/pop操作,尽可能少的对拼音字符串进行扫描和匹配,应该来说效率要高一些。而且,我感觉目前的这种实现方式,也基本满足大家的需要了。:)

返回到_push方法之后,如果设置了易混淆音,就对m_fuzzy_segs的最后两个segments,加入易混淆音。无论之前是否已经加入过易混淆音,CQuanpinSegmentor::_addFuzzySyllables都会先将seg.m_syllables resize为1,即清空了之前的易混淆音。

最后,在getSegments()时,将m_fuzzy_segs和m_segs合并到m_merged_segs中,并按照m_start排好顺序,返回给外层的调用者。我们今后可能会改进这部分,其实m_segs和m_fuzzy_segs都是有序的,只要让CIMIContext::buildLattice可以按照m_start的顺序,同时迭代这两个有序的vector就可以了。

还有其他的一些辅助的修改,例如,CIMIContext::getCandidates的循环退出条件不同了,导致我们现在迭代的次数会明显增多了,需要想一些更好的解决方法。

endianness for bit-fields

考虑下面的一个简单的C结构体:

struct {
    unsigned char a:4;
    unsigned char b:4;
}t1;

以前一直想当然的认为,编译器会把a安排在高4位,而b在低4位。不过在小端系统上,多数编译器是将b放在高4位的;而在大端系统上比较符合我的直觉,a在高4位。不过,这种安排并非是一标准,依赖于编译器的实现,不具备可移植性。因此,如果定义的数据结构是要跨大小端系统的,可以考虑通过位操作而非bit-fields来实现。且从汇编代码中亦可以看出,编译器其实也是通过位操作来支持bit-fields的。

那么下面这个结构呢?

struct {
    unsigned short a:4;
    unsigned short b:12;
}t2;

假设t2.a = 0xa; t2.b = 0xbcd,那么在小端和大端上的内存布局通常分别为:

小端:                    大端:
+--------+--------+    +--------+--------+
| d   a  |  b  c  |    | a   b  |  c  d  |
+--------+--------+    +--------+--------+

在小端上系统上,首先是b的低4位、加a的4位,然后是b的高8位。而在大端系统上,则首先是a的4位、加b的高4位,然后是b的低8位。可以看出,如果一个结构里有跨8bits的field,如果不进行字节序的转化,是不可能保存成平台无关的数据文件的。


导入fcitx用户词典

首先请下载这个导入工具,解压缩到某个目录中。注意:如果您使用的是实验版词表和语言模型,请下载open-gram项目的词表,解压并覆盖sunpinyin_importer目录下的dict.utf8文件,然后再执行下面的步骤。

如果在linux上,可以直接运行:

$ python import_fcitx_userdict.py

如果是要导入到mac平台上,请先使用mb2org(fcitx自带的工具),将用户词典导入到一个文本文件中:

$ /usr/bin/mb2org ~/.fcitx/pyusrphrase.mb > fcitx_userdict.txt,

然后将这个文件拷贝到mac上,再执行:

$ python import_fcitx_userdict.py fcitx_userdict.txt

导入QQ和紫光输入法的用户词典

大家可能已经注意到了,我们的用户词典导入工具,已加入了对QQ和紫光输入法用户词典的支持。

首先请下载这个导入工具,解压缩到某个目录中。注意:如果您使用的是实验版词表和语言模型,请下载open-gram项目的词表,解压并覆盖sunpinyin_importer目录下的dict.utf8文件,然后再执行下面的步骤。

导入QQ输入法的用户词典

在windows上激活QQ拼音输入法,然后打开“属性设置”对话框,在“词库管理”标签页下的“本地词库管理”中,点击“导出”按钮,将用户词典导出到一个文件中(例如,名为qq_userdict.dic),然后将这个文件拷贝到您的机器上(mac或linux),然后执行下面的操作,

$ python import_qq_userdict.py qq_userdict.dic

导入紫光华宇输入法的用户词典

在windows上激活紫光华宇拼音输入法,然后打开“设置”对话框,然后在“词库管理”标签页下,选中“用户词库”,点击“导出...”,将用户词典导出到一个文件中(例如,名为ziguang_userdict.txt),然后将这个文件拷贝到您的机器上(mac或linux),然后执行下面的操作:

$ python import_ziguang_userdict.py ziguang_userdict.dic

导入google和sogou输入法的用户词典

此次更新的导入工具,加入了对google和sogou输入法用户词典导入的支持。由于sunpinyin用户词典的一些限制,只能导入长度为2~6个字符的词条,并且最多可导入6万多个词条。我们后续会改进sunpinyin,以支持更大的词库和用户词典。该导入工具也可以在linux或solaris下运行,不过目前只支持ibus的前端。(因为我们不是很好判断,用户所使用的是xim还是ibus平台。)

注意:如果您使用的是实验版词表和语言模型,请下载open-gram项目的词表,解压并覆盖sunpinyin_importer目录下的dict.utf8文件,然后再执行下面的步骤。

首先请下载这个导入工具,解压缩到某个目录中。

导入google输入法用户词典

在windows上激活google拼音输入法,然后打开“属性设置”对话框,在“词典”标签页下,点击“导出”按钮,将用户词典导出到一个文件中(例如,名为google_userdict.dic),然后将这个文件拷贝到您的机器上(mac或linux),然后执行下面的操作,

$ python import_google_userdict.py google_userdict.dic

导入sogou输入法用户词典

在windows上激活sogou拼音输入法,然后打开“设置属性”对话框,然后在“词库”标签页下,在“词库操作选择”下拉框中选择“导出文本词库”,并点击“执行该操作”,将用户词典导出到一个文件中(例如,名为sogou_userdict.txt),然后将这个文件拷贝到您的机器上(mac或linux),然后执行下面的操作:

$ python import_sogou_userdict.py sogou_userdict.txt

基本上,只要输入法提供导出用户词典的功能、且导出格式为文本文件的话,为sunpinyin实现一个导入小工具是很简单的(可参考已有的importer)。大家可以自行编写一个,欢迎您为常用的输入法编写一个导入工具哦 :)

如果需要将fitx的用户词典导入到sunpinyin中,参见@Yunkwan同学编写的导入工具