Total Pageviews

Monday, 4 January 2016

一位计算机专业的大学生毕业五年

毕业五年
按大学毕业计算,转眼间已经有五个年头。看着公司不断加入年轻的充满活力的新同事,感叹自己真的老了。突然想写点什么,总结自己的技术成长经历。
大学迷迷糊糊报了一个高分子材料的专业,感觉完全提不起兴趣。时间回退到2007年左右,那时我还跟计算机还完全沾不上边,但是理工类的专业都会开设一门高级语言程序设计的课程,也就是教一点点C++的东西。当时就觉得这门课程学的东西很有意思,记得这门课程我的成绩是班级第一名。大概也是这个时候我已经在看《Unix环境高级编程》了,其实当时对计算机知识是完全没有基础的,但这本书似乎真的写得深入浅出。当时应该看懂了一些东西,以至于让我感到至今都是受益匪浅。那时已经在使用linux系统了,我记得自己安装的第一个linux系统应该是Ubuntu7.04,刻了光盘还保留着,直到研究生毕业搬东西时才丢失了。
时间回到2009年,这是我毕业前一年。知道自己喜欢的是编程,应该跟着内心走。于是,先跨考计算机的研究生混得文凭!考研这一年是我进步速度最快的一年,可谓一日千里。前面用了半年的时间,我去旁听了计算机系考研的四门课程:数据结构/计算机组成原理/操作系统/计算机网络。真正觉得自己入门,是学完数据结构这门课程。因为那时我开始可以写一点点代码了!能写500行代码规模的东西,我觉得是入门第一道坎。把各种排序算法,图的深入优先广度优先遍历,最小生成树,拓扑排序,Dijkstra算法和Floyd算法,不看书能够写出来就过了这道坎。那个时候每学习一种数据结构,回家就会认认真真地把书上的代码自己实现一遍,绝不偷懒。一切都在潜移默化中,直到复试上机我才发现自己coding能力早已完暴当时绝大部分所谓科班的同学了。因为上机的优势,复试成绩也是拿到了实验室第一名。
同样是在这一年里,读了非常多的计算机专业方面的书籍。因为后半年里在图书馆自习,累了我就会走到书架边拿计算机专业相关的图书看,对我来说是作为一种休息。不知不觉,几乎当时所有的计算机的书都看过一些,只不过有的看得深一些,有的囫囵吞枣罢了。这段经历,确实为自己的知识面打下了非常重要的基础,而不仅仅再局限于考研要考的那少数课程。也不纯粹是技术方面,像人月神话,unix编程艺术之类的涉及软件开发和管理,黑客文化和哲学等。如果说学数据结构会课后练习写每一段代码,使我动手能力提升,那么广泛阅读的这段经历,正像把所有课后参考资料链接都读一遍那样让我视野开阔。学数据结构时,我会把指定教材之外的书也翻一翻,对比更好的实现,比如说李春葆版的那个树的非递归后序遍历就比严蔚敏版的精简。学操作系统时,虽然用的汤子瀛版的教材,但课堂之外,我会去读minux设计与实现那本书,会去读一点linux源代码。之后就再也没有这一年这样进步神速的时期了。反而觉得考完研时的自己应该是最颠峰的时期,如果当时是找工作做面试题,肯定是完暴自己研究生毕业时的水平。
计算机喜欢从零开始计数,考研应该算第零年。研一算毕业第一年,也是我正式的进入程序员行业的第一年。这时的科研任务不算重,还会去上研究生的一些课程。课程作业中所有以团队为单位的要求动手实现的,其实都是我一个人做了,其它组员打酱油就行。研究生阶段最喜欢的一门课是嵌入式系统设计,当时的课程设计写了一个简单的操作系统内核。基于ARM7硬件的,最终效果是做了两个用户级的进程,一个不停地输出A,另一个不停输出B,因为进程切换的缘故,最终会看到类似ABABBBAABAA这种不规则的交替输出。操作系统内核听起来很高端,其实没有想象中难。首先会上电后bootstrap,这时会设置好各个寄存器状态,去读手册,就知道该怎么弄了。文件系统我省略了,进程调度用的一个基于多级队列的时间片轮询算法。内存管理方面,因为ARM7其实没有MMU(内存管理单元,负责虚拟地址到物理地址的映射),所以相当于一直是x86的实地址模式。甚至都没有做分段,做一个简单的固定页大小的内存分配器就可以了,其实跟写个用户级的内存分配器差不多的。这段经历真的很不错,我更了解底层了。以前是没有汇编基础的,做这个东西确确实实地写了一些汇编,准确地说ARM7汇编跟X86汇编语法是有一些差别,但也大同小异。bootstrap以及中断处理相关的代码写得最痛苦,因为必须全部用汇编写才能操作那些寄存器!booterloader需要设置那些寄存器状态,中断现场的上下文保存也是,后面写其它部分就能用C写了。当时能够写C那种感觉,激动得只能够用这句话表达:极大的解放和提高了生产力!
研一时比较自由,有一段时间对lisp语言着迷了。编程语言方面,C和lisp是两种极端。一个是代表图灵,走的是冯诺依曼这套体系的路子;另一个是丘奇,走的是lambda演算。很多人都听过所谓的图灵等价,但其实没多少人真正明白图灵等价是什么意思。也没有明白当时他们到底在解决什么问题。好像很玄乎,简单地说,当时还是计算机这种东西还处于正在发明中的阶段。可计算理论研究的是,假设给你一台计算能力无限的机器,那么什么样的问题是可以通过这台机器求解的。这么一说又提到了人工智能,机器能够自我思考吗?到底什么样算智能?哦,扯远了。
继续说回lisp,准确地说我指的是scheme。把scheme的语言标准r5rs打印出来也就六七十页的样子,语言本身不复杂。慢慢的就研究到了编译器,因为scheme大多资料都是跟它的解释器编译器相关的,《计算机程序的解释与构造》就是其中之一。后面就自己写起编译器来,虽然没有正统地学过编译原理,但是这份经历弥补了一部分。如果说,编译器实现要经历词法分析,语法分析,得到AST(抽象语法树),然后中间代码生成和代码优化这些阶段,可能很多人都懂。但是具体说LL分析LR分析,上下文无关文法转左递归文法好多细节,没几个人说得上来。因为正统的编译器课程让很多人在AST之前就阵亡了,只有极少数一部分人能活下来,能理解精髓的更是凤毛鳞角。以至于说到图着色的寄存器分配算法,可能很多人都没听过了。于是在大多人觉得,编译器是一种很高深很神秘的古老黑魔法,有一种敬畏之心。lisp的语法的简单可以把研究精力从这些细节的地方解放出来,学习曲线平滑很多。其实写编译器是很好玩的经历,因为有这种经历之后,一切不再是黑魔法。写一个基本的demo其实不难,无非高层的分支,顺序,赋值,以及函数调用协议对应到底层操作。实现if begin set! lambda几个scheme的关键字就可以了。循环可以递归实现,不是必须的,做尾递归消除后,都一样了。难的地方是完全去符合r5rs的规范,就有很多细节需要处理了。卫生宏是很难的一块,实现和理解continuation也是很难的一块。另外,函数式语言,有闭包,CPS变换,Y combinator等等好多概念,确实门槛比较高。有次看到知乎上面提问:到底什么是函数式编程可以解决,而面向对象编程解决不了的。其中一个答案是:“所谓的面向对象其实是解决了聪明的码农不多这个问题...而这也是FP唯一无法解决的问题”。看完会心一笑。
垃圾回收也是在写编译器过程中知道的,基本的三种垃圾回收算法,标记清扫/标记紧缩/引用计数。自己实现过标记清扫的垃圾回收算法。因为实现编译器配套的运行时,需要实现这项特征。垃圾回收的停顿影响挺大,优化的算法会做分代,并行的等等,这一块也是水很深。
许多年之后,我会半开玩笑半认真地说,自己是越来越菜了。想当年,那可是在写内核,写编译器呀!再看看现在写的算啥?只会写hello world了。
大概研一研二之间吧。实验室项目进行到一个文本系统,其实说白了就是一个搜索引擎的雏形,有文档入库分析、分词,建立索引,查询反馈等等。没有爬虫部分,也没有PageRank那种权重排序,只是简单的TF/IDF权重。我负责了索引模块以及最后各模块的系统集成工作。建立倒排索引是搜索引擎中一个重要的部分,这个索引其实本质上就是一个kv存储。key是一个词,value是一条链表,记录这个词所在的各个文档以及对应的权重。再往细化涉及一些倒排索引本身相关的东西,比如对海量的索引数据,那么压缩存储就很重要,涉及到了数据压缩。建索引的算法也可以做增量的merge,就像归并排序一样。也是初生牛犊不怕虎,连LSM都没听过,卷起袖子就开干了。直到后来读big table之类的论文,看leveldb资料的时候,才反应过来当时的自己是多么幼稚。懂得多了,反而变得谦逊。
做索引时接触到了存储。为什么这么说呢?其实跟数据库的索引是一个概念。不管是SQL或者NoSQL,索引无非是在内存中维护一个数据结构,存储key到一个文件加偏移的信息。外存则是用文件加偏移这种信息来维护记录。至于使用的是B树还是跳表或者哈希来组织,都可以。每种结构都有各自的优缺点,早期数据库喜欢用B树,随着NoSQL的崛起,LSM成了主流。单机存储引擎是分布式存储的基础,另一块则是分布式架构,暂时不提这个话题了。研一结束,实际地做了一点项目的东西,慢慢地过渡到了第二个阶段,这个阶段的代码量大概在单个项目5000行,能写一点点有意思的东西了。
研二,毕业第二年。开始真正的搞科研了。我的选题方向是图像标注,研究的是如何让机器给图像打上各种标签,比如标注图像中出现的物体。有点像模式识别的东西,涉及的内容就是底层特征提取和机器学习算法。底层特征提取比如图像中的形状颜色纹理等,这个没有作为重点。不过我喜欢说自己“假装”在搞机器学习,因为说实话我并不喜欢。机器学习对概率论和线性代数方面的数学基础要求挺高的,而我则更多是希望做一个工程师而不是科学家。直到研究生毕业,才觉得对机器学习可以算懂了一点皮毛了。往大的讲,无非就是分类,聚类,降维之类的。看懂一些精典算法,比如说K均值聚类,KNN,朴素贝叶斯,SVM,决策树,PCA等等。还有像混合高斯模型,梯度下降求解算法之类。当时好像用这本书《Machine Learning: A Probabilistic Perspective》,但是没有导师系统地教,自已看真是一个头大。什么偏方差分解,欠拟合过拟合,各种概率分布现在都只剩下一下模糊的印象了。随着科研的深入觉得自己的工程能力不断在下降。
研三一样是科研为主。不过中间插入了一段实习经历。被忽悠到百度打了一阵子酱油,在基础架构部(INF)的高性能计算(HPC)组。面试官以为我是真的搞机器学习的,居然同意了一个月的实习期,对此我倒有点歉意。一般百度实习都要求三个月以上,不然是不会通过的。对于我这种一个月实习生,不敢说后无来者,但是我猜测很可能前无古人。因为时间太短,期间的主要工作是做一个SVM的大规模求解的算法调研。我们部门会负责提供给其它部门基础设施,包括一些机器学习算法求解的接口。SVM最常用的求解算法是SMO,但这个算法没法处理海量样本的数据规模,数据量大到一定程度,就没法在可忍受的时间内计算出来了。为了寻找方案读了好多论文,找了一些开源库进行了性能测试和对比实验,最后是大致确定一个开源的库。也发现一些问题,求解过程中有一个计算中间结果是一个很大的矩阵,每个矩阵内的元素计算开销很大,如果不缓存就得每次都计算,但是硬件内存有限,缓存命中率很低,因为这个东西并不满足缓存的基础--局部性原理。做到那个时候实习期结束了。现在回想,其实如果用分布式缓存是有可能解决这个问题的,只是当时的视野还受限于单机内存。再回想那些公式推导,应该都没几个人看得懂的,反正现在我自己是看不懂了。这段经历中最大的收获是学习了好多百度内部的技术资料,能够一窥真实的大系统是怎么样的。我觉得自己是很适合做一些基础架构方面的工作的,对底层比较感兴趣。
当时云计算开始炒得很火。其实我觉得还是抛开概念炒作,还原本质比较好,分布式系统原理这个词就朴素得多。研究生课程学过一点,因为找工作缘故,这个时候便特意看一些。其实说分布式,无非是系统越来越大,单机的性能有限,摩尔定律又无法一直适用,所以现在必须要把许许多多的机器作为一个整体的系统去提供服务。单机提供的是哪些东西呢?存储能力,计算能力,连接存储和计算之间又需要消息通信。先说存储层面,传统的文件系统进化成了hadoop这类分布式的文件系统,数据库也从单机的数据库变成了各种NoSql的数据库。单机时代,因为硬盘速度有限,存储介质会需要磁盘和内存。对应到分布式时代,数据库速度不够,就需要分布式缓存。再说到计算,计算是建立在存储的基础之上的,因为计算需要使用数据。根据计算性质的不同,又有MPI这种并行计算模型,因为数据偶合性不便拆分,通过封装消息通信使上层看不到计算放在不同的机器上。还有MapReduce这种,通过将计算抽象成Map和Reduce几个过程,放到不同机器上计算后再将结果聚合起来。
要是当毕业当年能找到一份相关的工作,成长速度会快得多。因为按当时的情况,有一定的理论知识,缺乏的就是在大型的真实系统中去焠炼的机会。可惜皂滑弄人,进入了游戏行业,这时是毕业第四年。
老实说我应该是挺喜欢游戏的,但不是android小游戏。公司用的libgdx,一个很小众的Java的游戏引擎。这个引擎本身的代码还是挺不错的,就是周边的配套设施太差了。做游戏开发单有引擎是不够的,配套的工具都非常重要,像从合图,到界面编辑器,动画编辑器,关卡等等。自己动手丰衣足食,界面编辑器没有,就用gleed2d这类的开源编辑器做,生成xml文件了做解析,实现界面。游戏的界面跟App的界面不一样,主要是灵活性太高,不是用简单的控件和对齐方式就能描述的。动画工具没有,也是自己去写flash解析,让美术把flash资源导出了后解析成游戏内部的格式,在游戏中播放。libgdx学习起来很快,1个月就能上手了,3个月差不多比较熟练了。屏幕,场景,对象,各种层次组成一棵树形结构,然后就是点击事件的响应的消息路由。资源管理是比较操心的一块,大概无时无刻不在跟各种资源限制做斗争。使用的时候必须在那里了,而用不着的时候又要交换出去。所有会有各种loading过程,其实就是在切换资源。显存和包大小都是要控制的,我们要适配很低端的硬件,加载1024的素材大概最多7张的样子,超了就白屏,因为显存放不下了。老板的理论,包每增大1M就会减少多少用户量,小游戏必须限制包大小10M以内。再往后面走下去,就是接触OpenGL的东西了。像渲染流程,framebuffer,还有mesh,自己写shader什么的,接触到一点点。如果当时继续做这个方向,可能会深入一些。
不过还是觉得对后端更有兴趣一些,所以业余的都是看一些其它的资料。在Go语言1.0版本发布的时候,开始认真研究起这门语言。那个时候是研究这门语言本身,而不是工作中使用。主要是看源代码,学习Go的runtime的实现,还做了个开源项目把Go的这些内部实现相关的记录下来。核心的东西都是围绕着goroutine调度,内存管理,垃圾回收。其它的像channel实现,还有map,interface等等数据结构层面的东西。
这个阶段的代码量大概在单个项目3w行。最后统计自己项目的代码不算周边相关大概是3w多行。一年到头,平均每天其实写不到100行代码的。但是经过这个项目,真正的认识到了做工程跟做demo是不一样的,以前写的那些东西,都只能算是demo。目前我都还没有走这出个阶段,上次是一个人负责了整个游戏的开发工作,能写3w行代码的下一个项目还没出现。不过读代码倒是80w行代码的项目都有读过,并没有感觉到量变到质变般的差异。
离开游戏行业后做起了后端,这已经是第五个年头了。就拿游戏后端和web后端分开讲吧,其实还是有很大的区别的。游戏那边都是长连接,维护状态的,scalable困难得多。而web这边状态都在数据库层,上层无状态,所以更容易扩展。最终很多都是CURD的东西,但是并不表示问题没有了,而是从一个层面转移到另一个层面,压力都落到了数据库。游戏又有游戏别的特征,因为同时在线的人总是有限的,所以可以在登陆的时候把用户数据加载进来,下线时再写回去。对应于web,这是一个相当于缓存层命中率是100%的场景。web那边有太多开源的轮子,很多技术难点的地方,都有开源的轮子帮忙解决了,只需要自己根据场景组装。而游戏那边,则有更多需要定制自己的游戏框架机会,接触底层会更多,也更锻炼人。如果去看几个游戏源码,就会发现每个都有很不同的地方,因为游戏太灵活多变了。当然现在也开始有一些开源的构架,像skynet就写得蛮不错。
有一段时间去研究mmorpg,这应该是我比较喜欢的领域之一。其实很早的时候,大概刚读研那会儿,就有研究过曾经比较沉迷的一个游戏的源代码,只不过当时更多的放在封包和技能公式上面,原因大家懂的。还有,云风的博客绝对是非常好的学习资源,一遍一遍地读,每一次都会有新的收获。往上层架构讲,游戏会有分服分区设置。技术层面,登陆服务,网关,场景服务,数据库服务等等。往底层看,核心的都落到了场景服务中。多线程模型or多进程模型,io那边的封装,心跳的处理,有很多细节。看过一个比较渣的服务器代码,空着跑都占着七八十的CPU,为什么?我发现它完全没用epoll,而是将设置网络非阻塞,然后每次心跳对全部玩家连接做轮循!还有很多有意思有挑战的东西,像比较网络同步,aoi之类的。业务方面,寻路,战斗,AI,背包等等。总之,游戏后端是特别锻炼人的。
web这边,CURD会比较无聊,但是处理高流量大并发还是挺有意思的。写到现在的事情就不想再总结了。最后,放一张图吧,一图胜千言。


from http://www.zenlife.tk/5-years.md