为什么移动Web应用很慢?

前些日子,看到Herb Sutter在自己的博客中推荐了一篇文章《Why mobile web apps are slow》,在推荐里他这样写道:


“I don’t often link to other articles, but this one is worth reading.”

  我不经常链接到其它文章,但是这篇文章的确值得一读。

“He offers data (imagine!) to justly debunk many common memes and “easy answers” that routinely litter HN/Reddit/Slashdot comment threads.”

  这句话挺难翻译的,大概意思应该是作者使用了确切的数据来支持自己的观点,而不像其他很多人一样只是随意地发出毫无根据的评论。

“Don’t be distracted by the author’s viewpoint and emphasis on “iOS and Javascript” development – the article covers lots of important ground, including:

  • developing for ARM vs. x86;
  • developing for desktop vs. mobile;
  • managed vs. native code performance;
  • JIT issues vs. inherent language design tensions;
  • why garbage collection is not at all the panacea it’s often billed to be and often needs to be emphatically avoided (did you realize Apple already jettisoned GC?); and
  • as many of you know already, why if you’re serious about performance you’ll be seriously serious about memory usage and access patterns as a first-order issue.”

  不要被作者的观点以及iOS和Javascript开发等字眼分散注意力——这篇文章包含了很多重要的基础知识,包括:

  • ARM平台编程和x86平台编程比较;
  • 桌面环境编程和移动设备编程比较;
  • 托管代码和原生代码性能比较;
  • JIT相关话题和语言内在的设计张力;
  • 为什么垃圾回收不是宣传中所说的万能药,而且经常被强调要避免使用(你意识到苹果公司已经抛弃了GC吗?);
  • 就像你们中的很多人已经知道的那样,为什么如果你很在乎性能,那么你就应该认真严肃地将内存使用和访问模式作为最优先需要考虑的问题。

既然Sutter大神如此推荐,我就好好把这篇文章看下来了,的确收获颇丰,所以特意把这篇文章翻译下来,一方面加深理解,另一方面跟大家分享。我翻译的首要目标是可读性和流畅性,并不一定拘泥于字眼;难以翻译和习惯用英文表达的词汇会保留。我可以保证理解作者95%以上的意思(毕竟是技术类文章),但是作者的一些幽默我很可能没法传神地翻译出来,还请大家包涵。

(提示:这是一篇非常长的文章,认真读下来可能需要一段时间。下面是正文翻译。)


我写过不少文章来讨论为什么移动Web应用程序很慢,这也引起了不少的讨论。但是不幸的是,这些讨论没有像我喜欢的那样的基于事实

所以我这篇文章的目地就是给这些问题带来一些真正的证据,而不是仅仅过来对骂。在这篇文章的中,你可以看到基准测试(benchmark),可以看到专家的观点,你甚至可以看到非常诚实(honest-to-God)的期刊文章。这篇文章有超过100个引用(不是开玩笑)。我不保证这篇文章能使你信服,甚至不保证这篇文章中的所有内容都是正确的(在这样大规模的文章中做到这一点几乎是不可能的),但是我可以保证这是一篇关于许多iOS开发者都抱有的想法——移动Web应用很慢并且会在可预计的未来继续如此——分析最完备和全面的文章。

现在我要警告你:这是一篇长得吓人的文章,差不多10000字。当然,这是我故意的。我更喜欢好文章,而不是流行的文章。我尝试使得这篇文章成为前者,同时宣扬我认同的风气:我们应该鼓励那些优秀的、基于证据的、有趣的讨论,不鼓励那些诙谐、哗众取宠的评论。

我写这篇的文章,在某种程度上是因为这是话题已经到了一种争论不休的地步。这不是另一篇争论的文章,如果你想看到30秒左右的对骂:“真的!Web应用很渣!”和“谁说的?Web程序挺好!”,那么这篇文章不适合你。另一方面,据我所知,到现在为止还没有一个关于这个话题全面的、正式的、理性的讨论。这篇文章中我尝试去理性地讨论这个激起千层浪的话题,尽管这可能是一个非常愚蠢的想法。这里我给自己辩护一下,我相信这个问题与那些本来可以更好地去讨论却没有这样做的人更有关系,而不是主题本身。

如果你想知道你那些原生代码(native code)程序员朋友为什么在如今开放的网络革命时期还在写着万恶的原生代码,那就把本页面加入书签吧,给自己倒杯咖啡,找出一个下午的时间,找到一个舒服的椅子,然后我们就正式开始吧!

简单回顾

上一篇博客中写道:基于SunSpider的benchmark给出的数据可以看出当今的移动Web应用很慢。

如果你认为“Web应用程序”就是“一个网页加上一两个按钮”,那么你就可以让那些花哨的benchmark——比如SunSpider——滚一边去。但是如果你认为“Web应用程序”是指“简单的文字处理,简单的照片编辑,本地存储和屏幕之间的切换动画”,那么除非你有想死的心,否则你永远不会愿意在ARM上写Web应用程序。

你应该先读一下那篇文章,但是我还是在这边给你看下benchmark:

关于这个benchmark,主要三种主要的批评:

1. JS比原生代码要慢并不是什么新鲜事了,每个人在上第一学期的计算机基础课时讨论编译型语言、JIT语言和解释型语言是时候就知道了。问题是JS是不是慢到已经成为你现在所写软件的大问题了,但是像这样的benchmark并不能说明这个问题。

2. JS是很慢,这也的确是个问题,但是它在变得越来越快,所以在不久的未来,我们可以发现它不会那么慢了。所以大家一起学JS吧。

3. 我是Python/PHP/Ruby的服务器端的开发者,我不知道你们在说什么。我知道我的服务器比你们的移动设备快,但是如果我可以自信地保证使用真正的解释型语言写出支持上千个用户的代码,你们难道不能用一个带有高性能JIT的语言写出一个支持单个用户的代码吗?真的有那么难吗?

我有一个相当高的目标,那就是反驳以上所有观点:是的,JS的确是慢到一定程度了;不,它在不久的未来不会变得有多快;不,你在服务器端的编程经验不能正确地映射到移动应用中。

但是真正的问题在于,在所有讨论这个话题的文章里面,基本上没有人真正量化JS到底有多慢,或者提供某种真正有用的比较标准(相对于什么来说慢)。为了纠正这样的情况,我在这篇文章中提出了三种(不仅仅是一种)比较JavaScript性能的办法。我不会说“JS在什么情况下都慢”,而是真正量化它慢的程度,并且将它跟我们再平常编程经验中的事情做对比,这样你就可以根据这个结果结合自己的编程平台做出决定,你也可以自己计算下看看是否JavaScript适合你自己的特定问题。

OK,但是JS的性能相比于原生代码到底如何?

这是一个好问题。为了回答这个问题,我从Benchmark Game中随意抓取了一个基准测试。然后我找到了一个做同样benchmark的较老的C程序(老到不像很多新程序有一些x86特性)。我在自己的iPhone 4S上分别测试Nitro和LLVM。所有的代码已经传到了Github

这是一个随机的测试,正如日常生活中运行的代码一样。如果你想要一个更好的实验,可以自己运行。我运行这个实验还有另外一个原因,就是因为其它的实验都不存在LLVM和Nitro的对比。

在这个综合的基准测试中,LLVM一致地比Nitro快4.5倍:

Screen Shot 2013-05-14 at 5.32.06 AM

如果你在想“如果是计算密集型(CPU-bound)的功能,本地代码比Nitro JS快多少呢”,那么答案是差不多5倍。这个结果大致上和Benchmark Game在x86/GCC/V8上面的结果一致,那里面的GCC/x86通常比V8/x86快2到9倍。所以结果大致上是正确的,无论是ARM还是x86。

但是1/5的性能对每个人来说还不够好吗?

在x86上是足够好了。当渲染一个电子表格的时候,CPU的计算能有多密集呢?其实并不是那么难。问题是,ARM不是x86。

根据GeekBench的结果,最新的MacBook Pro的性能是最新的iPhone性能的10倍。这其实不算太大问题——电子表格没那么复杂。我们可以忍受10%的性能。但是我还要把它除以5?好家伙!我们现在只有桌面性能的2%了。

OK,但是文字处理到底有多难?我们可不可以用一个m68k芯片加上一个协处理器来搞定呢?这是一个可以回答的问题。你可能记不起来,Google Doc的实时协作之前事实上还不是一个正式的功能,后来他们进行了大规模的重写并且在2010年4月份加入到Google Doc里面。我们来看一下2010年浏览器的性能:

BrowserCompChart1 9-6-10[7]

从图中可以清晰地看到,iPhone 4S在Google Docs的实时协作方面完全不是桌面网页浏览器的对手。当然了,它还是可以跟IE8比上一比的。恭喜iPhone 4S,可喜可贺。

我们再看看另外一个正经的JavaScript应用:Google Wave。Wave从来没有支持IE8,因为它实在是太慢了。

Notice how all these browsers bench faster than the iPhone 4S?

看到这些浏览器比iPhone 4S快多少了吗?

注意,所有支持的浏览器的得分都低于1000,其中那个得分3800的因为太慢了而被忽略了。iPhone得分为2400。差不多和IE8一样,太慢几乎无法运行。

这边要说明的是,在移动设备上实现实时协作是可能的,只是不太可能用JavaScript来实现。原生代码和Web应用的性能差距基本上和Firefox与IE8的性能差距差不多,这么大的差距足以影响正常的工作。

但是我感觉V8或者是现代JS已经有了接近C的性能了?

这取决于你怎么理解“接近”了。如果你的C程序运行了10ms,那么一个运行50ms的JavaScript程序差不多是接近C的速度了。如果你的C程序运行了10s,那么一个运行了50s的JavaScript程序对于大多数正常人来说很可能就不是接近C的速度了。

硬件角度

1/5的速度在x86上是没问题的,毕竟x86起点就比ARM快10倍,你还有很多上升空间。解决方案显然是让ARM变成10倍快,这样就可以跟x86竞争了,然后我们就可以不用做任何工作就可以得到桌面环境的JS性能了。

这个方法行不行得通取决于你是否相信摩尔定律,以及给每个芯片配置一个3盎司的电池是否可行。我不是一个硬件工程师,但是我曾经为一家大型半导体公司工作过,那里的人告诉我说当今硬件的性能基本上是制作工艺(process)起的作用。iPhone 5令人印象深刻的性能主要是因为其芯片工艺从45nm做到了32nm,减少了差不多1/3。但是如果想继续这么做,苹果就要达到22nm的工艺。

顺便提一下,Intel22nm工艺的Atom处理器现在还没上市。而且Intel不得不重新发明全新的半导体,毕竟原来的半导体在22nm级别已经不适用了。他们会把工艺授权给ARM?再好好想想吧。如今22nm的产品少之又少,而且大部分被Intel掌控着。

事实上,ARM似乎已经在着手在明年尝试28nm了(看看A7),同时Intel正在尝试22nm甚至在稍微晚些时候尝试20nm。从纯硬件的角度,我感觉具有x86级别性能的x86芯片很可能远远比具有x86性能的ARM芯片更早登录智能手机市场。

看一个前Intel工程师给我发的邮件:

我是一个前Intel工程师,刚开始从事于移动微处理器的工作,后来工作转向了Atom处理器。无论如何,我有一个很偏激的观点,即x86从较大的核心转向手机市场的难度远比ARM从头开始设计技术细节以达到x86的性能级别的难度要低很多。

再看一个机器人领域的工程师给我发的邮件:

你说得非常对,这些(译注:指的是ARM的发展)不会带来多大的性能提升,Intel可能在近几年之内就会有更高性能的移动处理器。事实上,移动处理器当前和桌面处理器面临着同样的问题,即工作频率达到3GHz左右的时候,再提高时钟速度就不可避免地使得功耗大大增加。这种情况同样会发生在下一代工艺上,尽管IPC(Instruction per Clock,即CPU每一时钟周期内所执行的指令多少)会得到一些提高(差不多10%-20%)。在面临这种限制的情况下,桌面处理器开始向双核和四核方向变化,但是移动处理器现在已经是双核和四核了,所以想提高性能不是那么容易。

摩尔定律无论怎么说都可能是正确的,但是这需要整个移动生态环境向x86环境转变。这并非完全不可能,毕竟曾经有人做过这样的事。但那是在移动处理器一年才卖出去100万个的时候做的,不像现在,每个季度就可以卖出6200万个芯片。那个时候现成的虚拟化环境可以模拟出老架构的60%的速度,而按照现在的研究来看,虚拟化系统上运行优化过的(O3)ARM代码的速度已经接近27%了。

如果你坚信JavaScript的性能最终会到达一个合理的水平,那么硬件性能的提升绝对是最好的方式。要么Intel会在5年之内开发出可行的iPhone芯片(这是有可能的),并且苹果迅速转向x86架构(这是不太可能的),或者ARM能够在未来的10年之内得到性能的飞跃。但是在我看来,10年是一个很长的时间,长到足够使某件事情可能成功

恐怕我的硬件的知识只能分析到这里了。我可以告诉你的是,如果你相信ARM可以在未来的5年之内填补与x86之间的性能差距,那么第一步就是找到一个在ARM或者x86上工作的人(也就是真正懂硬件的人),让他同意你的看法。我写这篇文章之前,曾近咨询过很多有很高资质的硬件工程师,他们所有人都拒绝公开发表这个观点,这让我感觉这个观点不是很靠谱。

软件角度

这是一个很多优秀软件工程师犯错误的地方。他们的思路是这样的:JavaScript已经变得更快了,并且它会变得更快。

这个观点的前一部分是正确的,JavaScript的确变得快很多。但是我们现在已经达到了JavaScript性能的顶点了,它不可能变得更快多少。

为什么?其实前一部分JavaScript的性能提升从某种程度上是硬件的原因,正如Jeff Atwood写道:

我感觉从1996到2006之间JavaScript的性能变快了100倍。如果Web 2.0主要建立在JavaScript上的话,这很可能主要是因为摩尔定律所带来的硬件性能提升。

如果我们把JS的性能提升总结为硬件性能提升的话,那么JS的已有的硬件性能提升不能预测未来的软件性能提升。这就是为什么如果你相信JS会变得更快的话,最有可能的方式就是硬件变得更快,因为历史趋势就是如此。

那么JIT如何呢?V8,Nitro/SFX,TraceMonkey/IonMonkey,Chakra等等?当然,当他们刚刚问世的时候,的确是很了不起的(但或许不像你认为的那么了不起)。V8在2008年9月发布,我找到了一份差不多那个时候同期的Firefox 3.0.3,看看它的性能:

Screen Shot 2013-05-14 at 6.41.48 PM

不要误解我的意思,9倍的性能提升的确值得称赞,毕竟这差不多是ARM和x86之间的性能差距了。即便如此,Chrome 8和Chrome 26之间的性能却呈现出了水平线,因为自从2008开始,几乎没有什么重大的事情发生。其他浏览器厂商都已经赶上来了,有些快有些慢,但是没人真正提高过JavaScript的性能了。

JavaScript性能在提升吗?

Screen Shot 2013-05-14 at 3.59.04 AM

这是我Mac上的Chrome 8(可运行的最早版本,2010年12月份的版本)和Chrome 26

看不出差别?因为根本没有差别。JavaScript的性能最近根本没有得到大的提升

如果你感觉现在的浏览器比2010年的浏览器跑的快的话,那很可能是因为你有了一台更快的电脑,但是这与Chrome的性能提升没什么关系。

更新:有些聪明的人指出SunSpider现在不是一个好的benchmark(并且拒绝提供任何实际的数字或其它什么)。为了能够可以理性地讨论,我在一些旧版本的Chrome上面运行Octane(一个Google的benchmark),的确显示出了一些性能提升:

Octane on V8, 2011 to 2013

在我看来,在这个期间的性能提升还是太小,不足以支撑JS马上就会足够快这样的论调。然而,要说我过分强调这个情况也没错,毕竟JavaScript的计算密集型操作的确在发生变化。但是推我来说,这些数字可以得出更大的推断:这些性能提升的幅度还不足以在一定时间之内使得JavaScript的速度赶上原生代码。你需要性能达到2-9倍才能跟LLVM竞争。这些提升是好的,但还不足够好。更新结束

问题是,让JavaScript采用JIT技术是一个60年前就有的想法,并且这60年来一直有人在研究。数以千计的你可能想到的编程语言对JIT的实现都证明这是一个好主意。但是既然我们已经做到了,我们已经用完了这个60年前的想法。伙计们,就是这样的,表演结束了。或许我们可以在未来的60年之内想到另一个好办法。

但是Safari恐怕比以前要快吧?

Is Safari 7 3.8x faster than the other guys?

Safari 7 是不是比其它的浏览器快3.8倍?

这个结论或许对苹果来说很容易得到,但是这个版本的Safari在NDA协议之下,所以没有人能够公开关于Safari性能的独立参数。但是我可以仅仅根据现在已经得到的信息来做一些分析。

我发现一些现象很有意思。第一、苹果官方在公开的JSBench上的数据要比在他们在较老的benchmark(如SunSpider)上给出的数据高出不少。现在JSBench背后有一些非常酷的名字,包括JavaScript之父Brenden Eich。但是和传统的benchmark不一样,JSBench的工作方式并不是通过编写大整数分解或类似的程序,它反而自动为Amazon、Facebook和Twitter提供的内容进行优化,而且根据它们提供的内容来建立benchmark。

如果你在写一个多数人用来浏览Facebook的浏览器,我可以理解用一个只测试Facebook性能的benchmark是很有用的。但是从另外的角度讲,如果你在写一个电子表格的程序,或者游戏,或是一个图像过滤应用,在我看来传统的benchmark(注重整数运算和md5哈希)会比分析Facebook性能的benchmark更能够准确地帮你预测出代码有多快。

另一个重要的事实是,苹果声称的在SunSpider上性能的提升并不能代表其它东西的提升,Eich et al在这篇提到苹果所偏爱的benchmark的文章中写道:

图中清楚地显示出了Firefox的3.6版本比1.5版本在SunSpider的benchmark上性能提高了13倍。但是当我们看它在amazon的benchmark上的性能表现时,发现只有较适度的3倍的提升。更有意思的是,在过去的两年时间,在amazon的benchmark上的性能提升几乎已经不存在了。这意味着在SunSpider上做的一些优化几乎对amazon没有太大作用。

在这篇文章中,JavaScript之父和Mozilla的首席架构师之一曾公开承认在过去的两年之内Amazon的JavaScript性能几乎没有提升,没有发生过什么特别重要的事情。从这一点你也可以看出来,那些营销人员这些年都在过分夸大自己产品的性能。

他们继续争辩道:对于那些人们用来浏览Amazon网页的浏览器来说,运行Amazon的benchmark比运行其它benchmark要更能准确地预测出浏览器的性能(这是当然了……),但是这些手段不会帮助你更好地写出一个照片处理程序。

但是无论怎么说,从我可以看到的公开信息来看,苹果声称的3.8倍的性能提升对你来说几乎没有什么太有用的东西。我可以告诉你,如果我有一些能够反驳苹果声称击败Chrome的benchmark的话,我将不被允许发布它们。

所以,我们总结一下这一节,如果有一些人拿出一个柱状图来显示网页浏览器变得更快了,那并不能真正说明整个JS变得更快了。

但是还有一个更大的问题。

并非为性能而设计

JavaScript-the-good-parts

下面这段话出自于Herb Sutter,现代C++中最著名的人物之一:

在过去的20年里,有一种很难根除的文化基因——只要等到下一代的(包括JIT和静态)编译器出来,托管语言就会变得和原生语言一样高效。是的,我完全希望C#和Java编译器能够不断提高,包括JIT和类NGEN的静态编译器。但是,它们永远不会消除与原生代码之间的效率差距,有两个原因:一、JIT编译不是主要问题。根本原因更为基本:托管语言在编程人员的开发效率(当时的确是个问题)和程序的运行效率之间从设计上做了故意的妥协。特别的,托管语言选择选择在所有的程序上添加额外的性能开销,尽管你根本没有用到一个特性,你都会受到这个特性带来的额外的性能开销。主要的例子是assumption/reliance、垃圾回收、虚拟运行环境和元数据等功能在托管语言中是默认打开。当然还有其它的例子,比如托管代码中函数默认是virtual的,而C++代码中的函数是默认inline的。1盎司(译注:12盎司=1磅)的内联阻止(inlining prevention)抵得上1磅的去虚拟化优化(devirtualization optimization cure)。

下面这段话出自于Mono项目组的Miguel de Icaza,他是为数不多的“维护着一个主流JIT编译器的人”。他说道:

关于主流托管语言(.NET、Java和JavaScript)的虚拟机之间的差异,有一个比较准确的说法。托管语言的设计者在他们设计一门语言的过程中更倾向于安全性,而不是性能。

或者你可以找Alex Gaynor谈一谈,他负责维护和优化Ruby的JIT编译器,并且也为Python的JIT的优化工作做出了贡献:

这是加在这些具有高生产力的动态语言身上的诅咒。它们使得创建一个哈希表十分容易。这是一件非常好的事情。我认为C程序员多数不太会使用哈希表,因为对他们来说用哈希表实在是是一件痛苦的事情。原因有二:第一,你没有一个内置的哈希表;第二、当你尝试去使用的时候,你会左右碰壁。对比来看,Python、Ruby和JavaScript程序员都过度使用哈希表了,因为使用它们实在太容易了……所以大家都不在乎。

Google似乎意识到了JavaScript正面临着性能的瓶颈:

复杂的Web应用(这是Google比较擅长的)在某些平台上正在面临着不小的挣扎,主要是因为这些应用用到了一些不能被性能调优的语言,这些语言有内在的性能问题。

最后,我们听听权威人士的意见。我的一个读者向我指出这段Brenden Eich的评论。正如你所知,他是JavaScript之父。

有一点Mike没有强调:得到一个更简单的语言。Lua比JS简单得多。这意味着你可以写出一个简单的解释器使得它跑得足够快,同时能够保持对trace-JIT代码的尊重(这和JS不同)。