四年半工作回顾 Part2
在工作半年左右,即参加工作的第一年底,我接到了第一个非本专业的任务,说老实话,在此之前,我可能都以为自己会做图像处理一直下去,或者至少做上那么几年,或者能让我做的很深入,至少我没有意识到改变会来的那么快。我本科的专业是信息与计算科学,简单地说,主要还是数学,后来我把主要理论方向定在了数字图像处理方面。
新任务是为Adobe Acrobat Reader编写特定的插件,这件事对我来说是一片空白,对公司来说也是一片空白,没有任何基础和积累,我唯一了解的一丁点情况,就是很多人试了都没有结果。我不知道为什么会让我去做这件事,后来的结论是,可能那时我相对比较闲,其实图像处理和Acrobat的插件都不是公司的主要业务,本来就是做不做成功都无所谓的东西,只不过我都做成功了罢了。
这件事让我深刻地了解了什么是“雇佣”关系,你没有任何选择的权利,你就是劳动力,你要做的就是做一名合格的劳动力,努力成为一名适合任何任务的熟练工人,其它的什么都不是。你不是专家,更不是救世主,多数情况下不需要你发表意见,如果领导向你征求意见,多数情况也是礼貌性的,如在大众会议上,你或许会认为自己的意见很有价值,但领导其实会直接忽略你的发言。所以,没有必要期望在语言上获得重视,总之,应该首先、主要、重点地做好自己的本分工作,即解决眼前工作的具体问题,或简单讲,就是编码。所以,在参加工作之初,应该以一种学徒工的心态去对待问题和人,应该牢记自己是一名被雇佣者,仅此而已。
最初进入我视线的就是长达两千多页的全英文接口说明书,当然,现在这也不算什么,因为我已经学会跳跃式阅读。在那时是着实让我郁闷了一把,那是个猜测-编程-验证-再猜测-再编程-再验证……的反复循环的过程,没有人可以问,没有资料可以查(网上搜不到),看见网上有个项目经理级的人说,这东西学会并能熟练掌握进行实际开发,大概需要半年。
Adobe Acrobat Plug-In,这是它的全名,我大概用2-3月的时间(具体记不清了)基本知道了它究竟是怎么回事,并实现了需要的功能,还连学代用三天用Java写了个东西。之前在图像处理上用C++为Java写过JNI方法,但那还是C++,这三天用Java感觉它真是啰嗦啊,尤其是当你习惯C语言那种强制转换的时候,Java里ByteArray, CharArray, String都是不同的对象,能把你郁闷死,我的程序有50%都是在做它们之间的内容转换,三天后我决定放弃Java。但Java的学习使用经历让我明白,对程序员来讲,语言的语法是最基本但也是最次要的,尤其是当你熟悉或精通一门语言的时候,学习另一门语言其实是很快的,如C、C++、C#、Java它们都属于C语系,当然想要深入地理解它还需要时间的琢磨,我可能对Java的理解都算不上入门,但我会用了,而且我自我感觉接受不了它,所以我到现在也没有去学C#。
Acrobat Plug-In是个有趣的东西,它的接口封装方式,它的开发调试模式,它的调用机制,如果你用它的SDK去做一些东西,并思考过这几个方面的问题,怎么说呢,你的编程境界和视野都会提高和开阔很多。我认为,它的SDK的接口封装,从各方面讲,达到了一种独到模式下的典型高度,可以算作经典;它的回调机制,即在程序中运行子程序,对于安全性的控制,对于各种功能的支持,也都可以算是经典;你会发现,优秀软件和一般软件的区别,从上到下都是天差地别,大多数人是这样写程序的,那是常理,而少部分人是那样写程序的,那时奇迹;你会发现,优秀软件并不是在一般软件的基础上做的十全十美,而是,它是那个样子的,而多数软件是这个样子的,而我们写不出那样优秀的软件,是因为我们总是在这个样子的圈圈里打转。
在04年春,我接受了一个算法任务,开始沾边我现在的领域(全文检索),是做一个中文语义自动分类的工具,供公司的一款网爬软件使用,要求对任意下载并经过提取的文本,根据其内容的语义进行分类,以便归入相应的数据库或置上分类标记。这也是一件有趣的事,你会发现软件是神奇的。给我的资料是公司曾经购买的论文和代码,由于作者的代码写的太好了,所以一直都没有忍心实际使用上。领导的本意是把它改改,变成个可供调用的东西,但最后我决定还是把它重写一遍。
我通读了论文,充分理解和掌握了算法的原理,这是一个指导性学习机的典型算法,即算法分为训练与执行两个部分,训练需要语料的积累,通过对语料的学习,获得学习的成果数据,执行是使用成果数据与目标数据(待分类的数据)匹配的过程,最后获得匹配程度,取较大或最近似者。我还发现论文中有一部分内容没有相应的代码,最后鉴定这部分内容是拿来主义或为了凑数,直接忽视之。然后,我又在原算法的基础上做了改进,按照我自己对问题的理解,主要改进了在分类深度和准确度上存在的理论缺陷,还为训练过程做了优化(但那仍然需要一定的时间)。最后实际程序的执行效果,堪称神奇,那个时候我意识到,好的程序员就是魔术师,要做优秀的程序员,去创造那些不可思议的东西。
至此,在参加工作恰满一年的时候,我的C和C++已经十分熟练。后来我也一直维护着这个算法,陆陆续续又做过一些改进,后来又重写过两次,一次是在某产品中将它作为一个独立功能模块,大改和重写,我总是倾向于重写;最后一次是为新华社做的自动分类软件。我还将算法简化为另一种新的分类算法,针对不需要训练或没有语料的情况。
后来还考虑过自动聚类的算法,不过由于一些原因中止了。
在闲暇时,我开始关注自己在技术方面的提高,主要是从谈论高级编程技巧与经验的书开始,有一段时间拼命在网上搜罗这些,也去书店。最先看见的是林锐博士的《高级C/C++编程技术》,我用一天时间将它全部看完,也是因为它页数比较少,但都很精华,没有废话。其实它主要谈的就是编程风格,好的风格导致好的习惯,好的习惯导致避免致命的问题,相比之下,我的代码风格可以说是惨不忍睹。我意识到不改不行,于是就强制自己的工作中使用良好的风格,大约有一个月的时间,编码速度明显下降,一个月后又恢复到原先速度,基本风格也形成了,可以说是完成了一次蜕变,前后的代码就像是两个人写的。在那个时候我形成了现在代码风格的雏形,之后又根据自己的经验逐渐地做了调整,但代码风格是那个时候树立起来的。我建议所有的程序员,包括非C/C++程序员,都去看看这篇文章,它的精华并不在语言本身。老实说,我觉得自己看见这篇文章的时间有点晚了,改变风格的确是一件非常痛苦的事,应该在学习编程之初就对程序风格的问题有所认识,像我,就需要花费一定的时间有意识地去做这件事,如果是现在,那几乎是不可能再改变了。
还有一本是《C专家编程》,我在书店买的,用两个晚上看完,它的作者是SUN的一位资深工程师,它列举了C语言编程中有可能导致的种种问题,有些问题是我从来都没有想到的,里面还有一些有趣的故事,它让我真正地接受了一次来自大师的洗礼。在看完此书之后,我才算是真正地走向高手,高手和工人的编程区别仅在于思想,即先有高手的思维,后有高手的代码。
这两本书对我的影响最大,后来的一些书都属于同类的书,基本大部分内容是重复,或我已经知道的事,仅有极小部分对我有帮助。如侯捷翻译的《STL源码剖析》中对某版本Allocator的分析,让我充分理解了什么是内存管理,还有模板的萃取技术等等;梁肇新先生的《编程高手箴言》中讲到的VC6中的“真实”的调式方法,和自己如何实现虚函数的方法;包括《编程之道》,这些都引发了我的很多思考,它们让你更深入地去理解,什么是语言,什么是程序,什么是软件,什么是程序员,什么是程序员的思想,对这些问题的思考,会令你完成一次又一次的蜕变。
图像处理,Acrobat插件,自动分类,在漂亮地完成这些任务之后,我终于接手公司的核心产品,全文数据库的检索部分的维护,需要在全文检索中提供相关度值的计算。在我现在看来,之前的事情都只是公司边边角角的东西,甚至领导本来就做好了你失败的准备,只不过我都成功了,它们都有共同的特点:不重要,有难度,你一个人;所以,我把它们理解为考验。工作和学习一样,你只有小学成绩好,才能去好的中学,只有中学成绩好,才能考上好的大学,大学成绩好,才能继续读研读博,乃至一辈子留在学校里当教师、教授、校长、教育部长,这是一条线,只要你有一环做的不好,那么就会降级到另一条线,人的第一个阶段就是这样被区分开来的;工作也是这样,它是一个新的起点,但同样不能失败,你可能几年都遇不到什么事,也可能一个月就遇到好几件事,只要有一件做的不好,就会对你的前途产生分支。想想原始人是怎么记录历史的,在绳子上打结,你的事业就是这条绳子,你在工作中遇到的问题就是绳子上的结,如果发生了大事怎么做?换一条绳子继续打结,那就要看你换的是更长的绳子还是更短的。
工作就是在积累自己的信用,每做成一件事,信用就加,连续做成,信用就乘,只要有一件事做的不好,信用就除,甚至立即清零。所以,最初的时候,必须连续地做成,因为那个时候你的积分低,只要有一件事做不好,那基本就是清零了;相反,当你积累许多积分的时候,别人也就允许你偶尔失败几次,你也可以有很多转还的余地。所以,起步很重要。
这些都是机会,有的时候,你没有意识到这是个机会,但事后你会发现所有的事积累起来就是下一件事的机会,或者说,历史的总和等于未来的动因,你过去做的所有事的结果加上你现在正在做的事的结果,就是你明天的机会,当然,这个明天可能会相距比较远,但机会只在今天和过去。
当然,机会也需要一定的偶然性,所以我说这个明天可能来得晚些,也可能会来得很早。当时公司比我牛的C/C++程序员只有两个人,其中一个人一直负责网爬软件,是个体力活,现在还和我在一起;另一个人后来离开了,所以我就成了当时最恰当的选择。应该说,我真正理解C++,是靠他们教的,他们当然不会像老师那样陪着你,但我会问,不会问问题的程序员绝对不是个好程序员,你会发现提问是一种非常快速、直接、有效的学习方法。我向他们问了许多“弱智”的问题,加上我自己的工作积累,这些帮助我逐渐地充分地理解了C++是个什么东西,在此之前,可以说我对C++的理解和使用仅仅是语法层面的。
我看见代码的时候,最后的注释修改时间是1995年,近十年都鲜有改动的代码。我大概用了一个月的时间基本掌握了全文检索的算法,并为全文检索增加了相关度排序的功能。从此我就开始了对这份代码的陆陆续续的改进工作,最后直到它面目全非,我也充分地掌握了这个算法的奥秘。可以说,这份代码是与我之后直到今天的工作绑定在一起的,它集中了我在此时此刻之前所做过的最难、最巧妙、最神奇的所有算法,我从中所总结出的一些经验与技巧,绝对是独到的,甚至是常人所无法理解的;如果我告诉你,你可能会认为是错误或者荒谬的,但那是我从实际中体会到的,并信奉为真理。
比如,不追求错误的原因,先追求解决的办法,这样的情况很多,把错误更正了,只知道应该这样改,但不知道导致错误的原因,也不追究原因到底是什么,你绝对会认为这是一种错误的思潮,甚至是一种不负责任的行为;但是,试想想给你一个别人写的足够多足够复杂的代码,而那个人又不知去向,你要用一个月的时间去寻找错误的原因,未必能找到,找到了可能还要修改大段大段的逻辑,因为这里还掺杂着你的代码;而我只用三天时间在我的逻辑范畴内规避或修改这个错误,让它不至于影响到更高的层面去,或者兼容它,道义上你或许是唯一正确的做法,但正确的做法未必能赚到钱,而项目与企业利益会站在我这一边,或者说是我在这个问题上,率先站在了它们一边。
我遇到了太多此类的实际问题,比如内存分配和释放不配对,导致泄漏,我才不会在那样复杂的逻辑中用肉眼和单步跟踪去找宝,多数情况它甚至根本就不出现,它总是在你无法调试的时候才出现。知道我是怎么做的么?其实很简单,记录所有分配的地址,在检索结束时把残余的释放,就那么简单。我还在外围写了在空闲时让主进程重启一下的策略,这样长远看来内存就不会涨了;还有在内存达到一定量时让检索暂停服务的策略,等等。但我始终没有根本地去解决这个错误,因为那不值得。你应该以代码无错为目标,但不要寄望程序会不错。