财新传媒
位置:博客 > 万战勇 > 我在谷歌弄啥咧之十二——姐夫丁数

我在谷歌弄啥咧之十二——姐夫丁数

声明:本篇的写作采用了高级的第三人称手法,有较大可能会引发读者不适。特此为告,请大家慎重阅读。

1

夜。

初夏的西雅图,淅沥沥沥的雨滴打在玻璃窗上,是切分的节奏。

新开张的某庭春餐馆,食客云集,等座的号已经排到了两百开外。他们大多是附近高科技公司的码工,在一周的劳作之后,带着麻木的灵魂到这里寻找一些肉体的慰藉。角落里,几个格子衬衫的中年肿脸男人正在觥筹交错。

“你,知道啥是姐夫丁数吗?”

老万拈起一块水煮鱼,三下两下扒拉掉上面炸得黑亮的花椒粒,塞进嘴里,一边嚼着一边嘟哝道。

“没听说过。”谦哥的脸在灯下泛着红光,看起来有点喝大了。“似不似,姐夫家有几个男娃的意思啊?”

“去!这么多年了你还是不长进。”老万对他的不屑毫不掩饰。“姐夫丁是我歌的资深院士,著名计算机科学家,带领群众高歌猛进的领路人,新时代开天辟地的总设计师,all in 人工智能的舵把子……”

“你说的是杰夫•迪恩(Jeff Dean)啊!你这英文我就不说你了。”眼看老万的口水快要溅到自己心爱的口水鸡上,小邓忙插了一句。

“我做重要讲话的时候不要插嘴!”老万忙把水煮鱼咽下去,口齿顿时清楚了很多。“你听我说,这杰夫•迪恩数,就是说如果杰夫•迪恩审查了张三的代码,那张三就算被杰夫亲自摸过顶,他的杰夫•迪恩数就是一。要是杰夫没有审查李四的代码但是张三审查了呢,李四的数就是二,以此类推。一个人的杰夫•迪恩数越小,说明他的杰夫摸顶辈份越高。

“哦,这不就是论资排辈吗。”小邓趁老万喘气,赶紧抢救了两大块口水鸡到碗里。

“这个我懂,你说的是图论里面的,从特定节点出发的,最短有向路径问题,似不似?Dijkstra 算法可以,在,多项式时间内,求解。”谦哥不动声色地冒了个泡。

“可以啊!”老万夹起一筷子蒜苗腊肉,对他的老搭档有点刮目相看:舌头都伸不直了还记得 Dijkstra,这老谦还真是个战斗机啊。

“那杰夫•迪恩自己的数就是零。要是一个人从来没有被跟杰夫•迪恩有摸顶关系的人摸过顶,也就是说他不在杰夫•迪恩的摸顶传递闭包里,他的杰夫•迪恩数就是无穷大。”小邓不失时机地补充。

看见群众如此上道,老万来了兴致,又戳向一块五更肠旺:“跟你们讲,我司有人开发了一个内部APP,可以算每个人的姐夫丁数。从此江湖上就掀起了一场血雨腥风啊!多少人为了排位,不惜父子反目,兄弟相残,没有代码也要创造代码给人审查,争得是二桃三士狼奔豕突犬牙交错,苍生不得安宁。我再问你们,知道为啥我们组其他人的姐夫丁数都是二吗?”

“因为你们组的人都很二?”小邓一脸坏笑。

“一边呆着去!因为,”老万放下筷子,停止咀嚼肌群运动,目光深邃像是要穿过你的干锅牛腩上正在徐徐升起的氤氲的我的手。

“我,就是那个杰夫•迪恩数为一的人。”

像是听到了一个暗号,满屋食客们都停杯投箸不能食,肃然起敬。他们都在等老万的下一句话。

“且听我从头道来。”

2

20多年以前老万就已经叫老万了。虽然那时候他看上去还没有那么老,但是他自己不遗余力到处标榜自己是老万,大家就这么叫开了。老万刚到谷歌的时候,公司刚在纳斯达克挂牌不久,前途未卜。那时候,天,总是望不穿的蓝;路,总是走不尽的远;想要的,总得不到;而得到的,却没有想象中的甜……好像有点扯歪了。我要说的是,因为那时公司人没那么多,院士占的比例比较大,在邮件表上一杆子还能打着一两个。总之,因缘际会,老万有一次在邮件表上和公司的大牛杰夫•迪恩唠上了。

说到杰夫,那必须得提到同样是谷歌资深院士的三杰。这三杰跟我朝的三宝老师和八大山人一样,都是一个人:他的全名叫三杰•格玛瓦(Sanjay Ghemawat)。他和姐夫丁几乎同时进入谷歌,也同样是开发系统软件的顶级高手。他们英雄惜英雄,成了亲密无间的好基友。在公司早期,他俩十指紧扣,出双入对,搁置争议,共同开发,捣饬出了一大堆到今天还在广泛使用的 Google 系统,像对业界影响深远的 MapReduce, BigTable, Spanner就是他们俩主导发明的。

据他们自己讲,他俩特喜欢在一起捉对开发(pair programming)。早期办公室空间紧张,他俩就一个人坐在键盘前面,另一个人盯着屏幕。这个说,我脚得这里要加一个回调函数。那个说,而且这个函数应该返回一个哈希表。这个又说,你看这段代码这样重构一下好不好。那个说,哥你这么写实在妙!然后两人相视一笑: 哦哈哈哈哈哈!LGTM(Looks good to me)了!憋愣了,快交代码吧!于是又一块新功能上线了。总之,他俩犹如杨振宁和李政道,马克思和恩格斯,列侬和麦卡尼,郭德纲和于谦,惺惺相惜,你跑我追,共创了乌泱乌泱的一大片传奇。三杰和杰夫产能如此之高,以至于有一段时间据说我歌代码库里面他俩的代码占了一多半。

那老万和杰夫聊些啥呢?原来,老万推荐了一位朋友来公司应聘,结果连面试的机会都没见着就直接被毙了。老万问天问猎头:Why? Why? Tell me why! 猎头回复:在这位同学的简历上搜不到 Windows 和 Linux,恐怕工作经验不合适啊。这样的回答,老万是无法接受的。以他做码工的观念,在简历上罗列操作系统跟在征婚启事上写“会五笔字型”一样,绝对是一种很 low 的行为。于是,他在公司内部交流面经的邮件表上抛出了这个疑问:

人生的路,难道真的要被两个E文单词腰斩?还有没有天理?还有没有王法?!

这时候姐夫丁登场了。他先是对老万朋友的遭遇深表同情,然后指出这种按关键词匹配活人的做法大谬不然。虽然近年来随着大公司招人程序的流水线化,很多人不得不在简历里硬塞进不少热门技术词,但是他在审查简历时看到这种操作是皱眉头的。尤其是有的人恨不得把他用过的所有软件包都列出来,甚至把 HTML 都硬塞进编程语言一栏。他看了绝对是负分。老万得到公司元老的支持,好像那旱地里下了一场及时雨啊!

又一次,老万有个朋友面试后自我感觉良好,然而还是悲剧了。他怀疑是不是面试官自己的答案错了。老万把这情况在邮件表上一晒,杰夫又回应了。这一次他详细列举了几条改进公司招人程序的建议,还提出:第一,老万可以去招人委员会旁听几次,了解一下工作流出;第二,再给老万朋友一个电话面试的机会,以免遗珠之憾。

老万在前东家那里习惯了牛人都是高不可攀,鼻孔朝天,猛地到了一个院士可以屈尊和小兵谈笑风生的环境,简直怀疑自己所在的并非人间,而是天上人间。从此他主人翁精神病爆发,逢人就安利新东家,暂且按下不表。

3

众所周知,谷歌的内部开发环境艳绝人伦。那是今天。早年间曾经也惨绝人寰。你能想象吗?当年全部编译都是在本地进行,代码也没有索引,查找引用基本靠 grep,写代码如同老牛拉破车。上世纪80年代有首歌是这样描述谷歌工程师的:

长长的站台,寂寞的等待。哦!

我的芯在等待,永远在等待!

我的芯在等待,永远在等待!

我的芯在等待,永远在等待!

我的芯在等待,在等待!!!

这样的问题,你我可能熟视无睹。但在杰夫丁看来,这是奇耻大辱。一个搜索公司,可以在亿万文档中搜关键词如探囊取物,却不能在自己的代码库中找到一个心仪的函数?No, no, no! 杰夫丁可以说不。

于是他和三杰写了一个内部代码搜索工具 gsearch,把代码搜索时间从十几分钟缩短到不到一秒。这就是我司今天的代码搜索原型。

吃水不忘挖井人。老万作为用户一员,斗胆向杰夫提出了一个要求:可不可以支持 Windows 和 Mac 的代码搜索?杰夫马上回信了:还有人也有这个需求,那就上吧!两天以后见。

这就是杰夫•迪恩速度。

4

大家可能还记得,老万刚搞出来C++测试系统 gtest 的时候,面临一个艰巨的任务:那就是如何说服八竿子打不着的同事们开始使用 gtest。老万的歪点子是定期扫描代码库,看谁谁谁新写的测试没有用 gtest,然后自动写一封劝进信,历数 gtest 的种种好处,语重心长地指出:已经有XXX人在用 gtest 了,而且交口称赞。天下大势顺之者昌逆之者亡,要不下次试试?自然,这封信也找到了三杰和杰夫。他俩在百忙之中双双回了信,说这东西看起来解决的问题比较幼稚,他们一般不犯那些低级错误,所以这对他们帮助有限。总之,不打算用了。

竟然无法反驳!

撞了南墙就回头?这不是老万的个性。过了半年,眼看杰夫还是没有用 gtest,老万的小程序憋不住了,又自动发了一封信,提醒他光阴似箭已过半年,日月如梭试试再说?杰夫又回信了,这次他旗帜鲜明地指出:以后别再给他发这种垃圾信了。老万怅然若失,把杰夫拉进了小程序的黑名单。

然而,历史的滚滚洪流是无法抗拒的,哪怕你是神,哪怕你是杰夫丁。虽然高层路线受到了挫折,老万不当回事,继续他的农村包围城市策略,从基层程序员开始,各个击破。他还和测试小分队配合,洗脑从新人抓起,在入职培训中加入了 gtest 环节。星星之火可以燎原,终于有一天,杰夫丁发现不懂 gtest 竟然连代码都审查不了了。无奈之下,不等老万再催,他也开始用 gtest 了。

这正是:走自己的路,让杰夫丁无路可走!

虽然杰夫丁一开始对 gtest 持怀疑态度,开用之后,他慢慢体会到了好处,还不带成见地贡献了一个功能,让用户可以在命令行选择运行哪些测试。这也让老万有机会审查(学习)了杰夫的代码。

5

老万的另一个测试系统 gMock 有类似的经历。gMock 的测试断言是通过匹配器(matcher)的形式来书写的。假定我们把星期一的菜单放在一个叫 monday_menu 的字符串里,要验证星期一有红烧肉的话,可以这么写

EXPECT_THAT(monday_menu,

    HasSubstr("红烧肉"));

这里 HasSubstr("红烧肉") 就是一个匹配器,负责检查一个字符串是不是包含了特定的子串“红烧肉”。此外,这个匹配器还负责在匹配失败的时候生成一个得体的错误消息。比方说上面这个测试失败了,你看到的结果可能是这样的:

Value of: monday_menu

Expected: has a substring "红烧肉"

Actually: "咸菜,窝窝头,自来水"

问题一目了然。那么问题来了:要是一个星期每天都要有红烧肉呢?

一种做法是把菜单放进一个数组 weekly_menu,每天对应一个元素,再用一个循环搞定:

for (const string& menu : weekly_menu) {

  EXPECT_THAT(menu, HasSubstr("红烧肉"));

}

但是朋友,你有没有想到过,要是失败了的话你看到的会是什么?这样的:

Value of: menu

Expected: has a substring "红烧肉"

Actually: "咸菜,窝窝头,自来水"

Value of: menu

Expected: has a substring "红烧肉"

Actually: "猪油酱油饭,涮锅汤"

你要如何分辨到底是哪一天的伙食没有达标(大膘)呢?信息不足啊!如果这个测试是别人写的,而且在你值班时挂了,你是不是有杀一个程序员祭天的冲动呢?

当然,这是难不倒牛人的。加一段代码就好了:

for (int i = 0; i < weekly_menu.size(); ++i) {

  const string& menu = weekly_menu[i];

  EXPECT_THAT(menu, HasSubstr("红烧肉"))

     << " on day #" << i;

}

如此一来,错误信息就会变成

Value of: menu

Expected: has a substring "红烧肉"

Actually: "咸菜,窝窝头,自来水" on day #0

Value of: menu

Expected: has a substring "红烧肉"

Actually: "猪油酱油饭,涮锅汤" on day #3

原来,星期天吃的是窝窝头,星期三是猪油饭啊。

只是,你不觉得大好的青春用在手写循环是一种赤裸裸的暴殄天物吗?我们程序员的前肢是可以用来产生更大价值的啊!

这个问题 gMock 的解决办法是组合拳。简单地说:

没有什么测试问题是一个匹配器解决不了的。如果有,那就两个。

上面这个循环,可以用匹配器 Each 和 HasSubstr 双管齐下,精简为一行:

EXPECT_THAT(weekly_menu,

    Each(HasSubstr("红烧肉")));

这哪里是C++,简直就是加标点的E文啊!

再来看出错的结果:

Value of: weekly_menu

Expected: each element has a substring "红烧肉"

Actually: {"咸菜,窝窝头,自来水", "毛氏红烧肉", "秘制红烧肉", "猪油酱油饭,涮锅汤", "还是红烧肉", "又来红烧肉", "红烧肉汤面"} (element #0 doesn’t match; element #3 doesn’t match)

是不是该有的全都有了?

某年,老万想给杰夫丁做的一个库加一些匹配器,方便用这个库的人写测试。杰夫十动然拒了,理由是:这个东西有点过于神奇了,谁知道靠不靠谱。他情愿用一些简单明了的东西,虽然啰嗦一点,但是知根知底啊。三杰也来帮腔:

叫一声杰夫哥朕的好兄弟!

匹配器我也觉得要从长计议!

切不可吃撑了贪那小的便宜,

加一个咱哥俩看不懂的东西!

要是有一天这玩意竟然嗝儿屁,

debug 的难道还得是个PhD?

C++已经是复杂得慌的一比,

gMock 雪上加霜啊殊不可取!

呀哈哈哈呀咿呀咿呀~~~

当然在老万看来,杰夫和三杰的这个观点他是不太赞成的。别的不说了,他们俩为了简化系统软件就做出了很多神奇的东西啊,为啥别人就不行?

其实,再大的领导,再牛的专家,也不可能在任何事情上都一贯伟大,一贯光荣,一贯正确。凡是宣传自己永远伟光正的,最后都会被打脸。专家是什么意思?那是在特定的(专业)领域内,他比别人思考得更深入更周密,所以才是专家。出了这个领域,他就不是专家。甚至在同一个领域的某个具体问题上,专家的意见也未必比一个比他花了多得多的时间去思考的人更靠谱。

人类在认知的过程中,会有不自觉的打标签行为:一件事情上来,先根据自己以往的经验先给这个事情定个性,抓住主要矛盾,忽略次要矛盾。然后快速下个判断。应该说这种做事方式通常来说是一件好事,得出的结论也多半是靠谱的,不会差得太远。如果不依靠过往经验快速分类,人类就会患决策焦虑症,一事无成。

举个例子:早上出门,看见下雨,决定带伞。这个决策一气呵成。但是如果抛弃一切成见,一定要从头分析,就要考虑降水概率,降雨量,降雨时间,覆盖面积,带伞还是带雨衣,带折叠伞还是长伞,什么颜色,放在什么包里,用左手还是右手......你看,是不是没有半个小时出不了门?

所以我们作为初中生牛犊在撞上大牛的时候,固然要尊重他们,但是也不能唯他们牛首是瞻。一件事情如果经过了深思熟虑还是觉得自己是正确的,那么在能够承担后果的前提下,追寻自己的内心,勇敢地对大牛说:对不起,我想好了,还是觉得应该这么做。这时候,或许会有意外的惊喜。

虽然杰夫和三杰一开始对 gMock 是抗拒的,他们并没有运用自己的影响力把它扼杀在襁褓之中,而是给了老万一条生路。我们以后做领导的时候,也要注意疑人不用用人不疑,不要把自己的意志强加于人。我们可以在讨论的时候提出自己的观点,但是如果不能说服小弟小妹,只要他们的决策不会造成难以挽回的后果,那么就让做事的人自己决断吧。最坏的结果,是他把一件事情搞砸了,那就再从头收拾旧河山朝天阙,又有什么呢?所谓纸上得来终觉浅,绝知此事要犯错。而且,也有可能是你错了,小弟是正确的。这种时候你就应该庆幸自己没有搞一言堂。

即便在这件事情上杰夫判断失误,也丝毫无损他作为一位计算机科学家和撬动世界的程序员的伟大。大牛的时间是昂贵的,要用在主导公司和业界的航向上,我们不应当要求他们在一个小问题上也要考虑得万无一失 ---- 那是对资源的极大浪费。而且,他们的脑回路不同,要他们体会到初级程序员的一些苦恼也是强人所难啊,所以,他们对软件开发工具的关注点可能根本就跟常人不同,不是我们设计测试框架的目标用户群。

每天都有人发明一些新的系统,它们大多数都经不起时间的考验,悄悄地来,又悄悄地去了。做为久经沙场的老程序员,对新生事物持保留态度才是正确的打开方式。盲目跟风追热点,就会陷入人民战争的泥潭不能自拔。等到大浪淘沙之后再跟进,避免无谓的浪费,降低学习成本,才可以把火热的青春投放在最需要的地方。

牛人之所以是牛人,不在于他们不犯错或者少犯错。相反,他们在不值得纠结的地方大胆犯错。但是在关键问题上,他们看准了方向就全力以赴。他们和老万们的区别,除了战术上技高一筹,更多的是战略上的洞见。

要知道,杰夫丁设计和实现的那些开创性的系统,老万是万万不能的。

6

我司员工热爱杰夫丁,以至于写了一堆关于他如何牛叉的段子。这些段子后来流传到了社会上,广为人知。举几个例子:

杰夫在斯坦福大学做报告,结果人山人海,一代宗师教父高德纳(Don Knuth)都不得不坐在地上听报告。(这个是真的。)

杰夫丁有回咬了一只蜘蛛一口,结果这只蜘蛛获得了超能力和C++的可读性资格认证。(这个是编的。)

杰夫丁不是一个人。他其实是杰夫丁创造的AI。(这个我也不知道是真的假的。)

12年前老万也贡献了一个:

杰夫丁只是想让系统快一点,结果一不留心同时修好了一个长期 bug。

这个是真的,而且这件事是老万发现的。老万籍此机会将自己的姐夫丁数变成了一。

是这样的:在很早很早以前,C++的模版(template)系统有很多毛病,其中之一是模版的类型参数不能是一个匿名枚举(anonymous enum)。如果你胆敢用匿名枚举去实例化一个模版,编译器就会吐得一塌糊涂,扔出一堆天书一样的错误,只有专门钻研计算机语言的律师看得懂。公司有一个断言宏(assertion macro)是用模版实现的,它的注释专门说明了这一局限性。

然而有一天,老万发现这个宏在匿名枚举上工作完全正常啊。这是怎么回事?好奇心驱使老万做了一番调研。最后他发现,这个注释最初确实是正确的,编译器确实会挂。直到有一天,杰夫丁想让这个断言对整型参数更快一些,于是他针对整型重载(overload)了这个断言的实现函数。没想到啊没想到,这一来这个宏可以顺带处理匿名枚举了。这一点,连杰夫本人都没有意识到。

发现这个秘密之后,老万加了几个测试验证这一发现,又修正了这一过时的注释。这个 CL 老万送给杰夫丁审查,获得了被亲自摸顶的机会。

7

夜深。

食客们从老万的故事中清醒过来,陆陆续续起身,向某庭春作别。After all, tomorrow is another day,又要开始早九晚九的生活。在滚滚的码工洪流之中,你我谁又不是一块人肉干电池,添一个不多,减一个不少。那姐夫丁数大一点小一点,又有什么区别呢?

角落里的那几个中年肿脸的汉子,也挥手自兹没入了茫茫的夜色。其中一人,把头仰向冷雨,踟蹰不前。雨水和着泪水,顺着他粗糙的脸皮流下,消逝不见,像那些古早的英雄事迹。

推荐 9