;
WebAssembly 系列(二)JavaScript Just-in-time (JIT) 工作原理
{"debug":false,"apiRoot":"","paySDK":"https://pay.zhihu.com/api/js","name":"production","instance":"column","tokens":{"X-XSRF-TOKEN":null,"X-UDID":null,"Authorization":"oauth c3cef7c66a1843f8b3a9e6a1e3160e20"}}
{"database":{"Post":{"25669120":{"title":"WebAssembly 系列(二)JavaScript Just-in-time (JIT) 工作原理","author":"hu-zi-da-ha","content":"<blockquote>作者:Lin Clark <br>翻译原文:<a href=\"http://link.zhihu.com/?target=http%3A//huziketang.com/blog/posts/detail%3FpostId%3D58c12f36a6d8a07e449fdd22\" class=\" external\" target=\"_blank\" rel=\"nofollow noreferrer\"><span class=\"invisible\">http://</span><span class=\"visible\">huziketang.com/blog/pos</span><span class=\"invisible\">ts/detail?postId=58c12f36a6d8a07e449fdd22</span><span class=\"ellipsis\"></span><i class=\"icon-external\"></i></a><br>英文原文:<a href=\"http://link.zhihu.com/?target=https%3A//hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">A crash course in just-in-time (JIT) compilers<i class=\"icon-external\"></i></a></blockquote><p>本文是关于 WebAssembly 系列的第二篇文章<b>(本系列共六篇文章)</b>。如果你没有读先前文章的话,建议<a href=\"http://link.zhihu.com/?target=http%3A//huziketang.com/blog/posts/detail%3FpostId%3D58ce8036a6d8a07e449fdd27\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">先读这里<i class=\"icon-external\"></i></a>。如果对 WebAssembly 没概念,建议<a href=\"http://link.zhihu.com/?target=http%3A//blog.csdn.net/wulixiaoxiao1/article/details/60581397\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">先读这里<i class=\"icon-external\"></i></a>。</p><br><p>JavaScript 的启动比较缓慢,但是通过 JIT 可以使其变快,那么 JIT 是如何起作用的呢?</p><h2><b>JavaScript 在浏览器中是如何运行的?</b></h2><p>如果是你一个开发者,当你决定在你的页面中使用 JavaScript 的时候,有两个要考虑的事情:目标和问题。</p><p><strong>目标</strong>:告诉计算机你想做什么。</p><p><strong>问题</strong>:你和计算机说不同的语言,无法沟通。</p><p>你说的是人类的语言,而计算机用的是机器语言。机器语言也是一种语言,只是 JavaScript 或者其他高级编程语言机器能看得懂,而人类不用他们来交流罢了。它们是基于人类认知而设计出来的。</p><p>所以呢,JavaScript 引擎的工作就是把人类的语言转换成机器能看懂的语言。</p><p>这就像电影<a href=\"http://link.zhihu.com/?target=http%3A//baike.baidu.com/link%3Furl%3DnlfNXe4vUxMlHF2qbxp6E0iombRcXCHYbFemmfWkxMHsbJ31SnMxi2d61FNZe0hmLKvWX13NRa3-zQSgy1ptUf4Ow_mqpmqxmQkx4hbcay3\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">《降临》<i class=\"icon-external\"></i></a>中,人类和外星人的互相交流一样。</p><img src=\"http://pic3.zhimg.com/v2-ba1812d24cd3bd3fd5ce2f976c683722_b.png\" data-rawwidth=\"500\" data-rawheight=\"286\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic3.zhimg.com/v2-ba1812d24cd3bd3fd5ce2f976c683722_r.png\"><p>在电影里面,人类和外星人不仅仅是语言不同,两个群体看待世界的方式都是不一样的。其实人类和机器也是类似(后面我会详细介绍)。</p><p>那么翻译是如何进行的呢?</p><p>在代码的世界中,通常有两种方式来翻译机器语言:解释器和编译器。</p><p><strong>如果是通过解释器,翻译是一行行地边解释边执行</strong></p><img src=\"http://pic2.zhimg.com/v2-3e3d960264590f2496a2b12afe5b5bb9_b.png\" data-rawwidth=\"500\" data-rawheight=\"291\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic2.zhimg.com/v2-3e3d960264590f2496a2b12afe5b5bb9_r.png\"><p><strong>编译器是把源代码整个编译成目标代码,执行时不再需要编译器,直接在支持目标代码的平台上运行。</strong></p><img src=\"http://pic2.zhimg.com/v2-0b601888e312f2d362ba64cb5ee502c5_b.png\" data-rawwidth=\"500\" data-rawheight=\"297\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic2.zhimg.com/v2-0b601888e312f2d362ba64cb5ee502c5_r.png\"><p>这两种翻译的方式都各有利弊。</p><h3><b>解释器的利弊</b></h3><p>解释器启动和执行的更快。你不需要等待整个编译过程完成就可以运行你的代码。从第一行开始翻译,就可以依次继续执行了。</p><p>正是因为这个原因,解释器看起来更加适合 JavaScript。对于一个 Web 开发人员来讲,能够快速执行代码并看到结果是非常重要的。</p><p>这就是为什么最开始的浏览器都是用 JavaScript 解释器的原因。</p><p>可是当你运行同样的代码一次以上的时候,解释器的弊处就显现出来了。比如你执行一个循环,那解释器就不得不一次又一次的进行翻译,这是一种效率低下的表现。</p><h3><b>编译器的利弊</b></h3><p>编译器的问题则恰好相反。</p><p>它需要花一些时间对整个源代码进行编译,然后生成目标文件才能在机器上执行。对于有循环的代码执行的很快,因为它不需要重复的去翻译每一次循环。</p><p>另外一个不同是,编译器可以用更多的时间对代码进行优化,以使的代码执行的更快。而解释器是在 runtime 时进行这一步骤的,这就决定了它不可能在翻译的时候用很多时间进行优化。</p><h2><b>Just-in-time 编译器:综合了两者的优点</b></h2><p>为了解决解释器的低效问题,后来的浏览器把编译器也引入进来,形成混合模式。</p><p>不同的浏览器实现这一功能的方式不同,不过其基本思想是一致的。在 JavaScript 引擎中增加一个监视器(也叫分析器)。监视器监控着代码的运行情况,记录代码一共运行了多少次、如何运行的等信息。</p><p>起初,监视器监视着所有通过解释器的代码。</p><img src=\"http://pic2.zhimg.com/v2-e8f36d5ee87844d0798528a605d00dd5_b.png\" data-rawwidth=\"500\" data-rawheight=\"365\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic2.zhimg.com/v2-e8f36d5ee87844d0798528a605d00dd5_r.png\"><p>如果同一行代码运行了几次,这个代码段就被标记成了 “warm”,如果运行了很多次,则被标记成 “hot”。</p><h3><b>基线编译器</b></h3><p>如果一段代码变成了 “warm”,那么 JIT 就把它送到编译器去编译,并且把编译结果存储起来。</p><img src=\"http://pic3.zhimg.com/v2-d72f6d9ff61b3e121d53bc228ac9bdb2_b.png\" data-rawwidth=\"500\" data-rawheight=\"368\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic3.zhimg.com/v2-d72f6d9ff61b3e121d53bc228ac9bdb2_r.png\"><p>代码段的每一行都会被编译成一个“桩”(stub),同时给这个桩分配一个以“行号 + 变量类型”的索引。如果监视器监视到了执行同样的代码和同样的变量类型,那么就直接把这个已编译的版本 push 出来给浏览器。</p><p>通过这样的做法可以加快执行速度,但是正如前面我所说的,编译器还可以找到更有效地执行代码的方法,也就是做优化。</p><p>基线编译器可以做一部分这样的优化(下面我会给出例子),不过基线编译器优化的时间不能太久,因为会使得程序的执行在这里 hold 住。</p><p>不过如果代码确实非常 “hot”(也就是说几乎所有的执行时间都耗费在这里),那么花点时间做优化也是值得的。</p><h3><b>优化编译器</b></h3><p>如果一个代码段变得 “very hot”,监视器会把它发送到优化编译器中。生成一个更快速和高效的代码版本出来,并且存储之。</p><img src=\"http://pic2.zhimg.com/v2-60cdb546dce91244124ffbb3058671e9_b.png\" data-rawwidth=\"500\" data-rawheight=\"365\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic2.zhimg.com/v2-60cdb546dce91244124ffbb3058671e9_r.png\"><p>为了生成一个更快速的代码版本,优化编译器必须做一些假设。例如,它会假设由同一个构造函数生成的实例都有相同的形状——就是说所有的实例都有相同的属性名,并且都以同样的顺序初始化,那么就可以针对这一模式进行优化。</p><p>整个优化器起作用的链条是这样的,监视器从他所监视代码的执行情况做出自己的判断,接下来把它所整理的信息传递给优化器进行优化。如果某个循环中先前每次迭代的对象都有相同的形状,那么就可以认为它以后迭代的对象的形状都是相同的。可是对于 JavaScript 从来就没有保证这么一说,前 99 个对象保持着形状,可能第 100 个就少了某个属性。</p><p>正是由于这样的情况,所以编译代码需要在运行之前检查其假设是不是合理的。如果合理,那么优化的编译代码会运行,如果不合理,那么 JIT 会认为做了一个错误的假设,并且把优化代码丢掉。</p><img src=\"http://pic4.zhimg.com/v2-efd2a9f349375276e831f78ab14b19a3_b.png\" data-rawwidth=\"500\" data-rawheight=\"361\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic4.zhimg.com/v2-efd2a9f349375276e831f78ab14b19a3_r.png\"><p>这时(发生优化代码丢弃的情况)执行过程将会回到解释器或者基线编译器,这一过程叫做<strong>去优化</strong>。</p><p>通常优化编译器会使得代码变得更快,但是一些情况也会引起一些意想不到的性能问题。如果你的代码一直陷入优化<->去优化的怪圈,那么程序执行将会变慢,还不如基线编译器快。</p><p>大多数的浏览器都做了限制,当优化/去优化循环发生的时候会尝试跳出这种循环。比如,如果 JIT 做了 10 次以上的优化并且又丢弃的操作,那么就不继续尝试去优化这段代码了桩。</p><h3><b>一个优化的例子:类型特化(Type specialization)</b></h3><p>有很多不同类型的优化方法,这里我介绍一种,让大家能够明白是如何优化的。优化编译器最成功一个特点叫做类型特化,下面详细解释。</p><p>JavaScript 所使用的动态类型体系在运行时需要进行额外的解释工作,例如下面代码:</p><div class=\"highlight\"><pre><code class=\"language-js\"><span></span><span class=\"kd\">function</span> <span class=\"nx\">arraySum</span><span class=\"p\">(</span><span class=\"nx\">arr</span><span class=\"p\">)</span> <span class=\"p\">{</span>\n <span class=\"kd\">var</span> <span class=\"nx\">sum</span> <span class=\"o\">=</span> <span class=\"mi\">0</span><span class=\"p\">;</span>\n <span class=\"k\">for</span> <span class=\"p\">(</span><span class=\"kd\">var</span> <span class=\"nx\">i</span> <span class=\"o\">=</span> <span class=\"mi\">0</span><span class=\"p\">;</span> <span class=\"nx\">i</span> <span class=\"o\"><</span> <span class=\"nx\">arr</span><span class=\"p\">.</span><span class=\"nx\">length</span><span class=\"p\">;</span> <span class=\"nx\">i</span><span class=\"o\">++</span><span class=\"p\">)</span> <span class=\"p\">{</span>\n <span class=\"nx\">sum</span> <span class=\"o\">+=</span> <span class=\"nx\">arr</span><span class=\"p\">[</span><span class=\"nx\">i</span><span class=\"p\">];</span>\n <span class=\"p\">}</span>\n<span class=\"p\">}</span>\n</code></pre></div><p>+= 循环中这一步看起来很简单,只需要进行一步计算,但是恰恰因为是用动态类型,他所需要的步骤要比你所想象的更复杂一些。</p><p>我们假设 arr 是一个有 100 个整数的数组。当代码被标记为 “warm” 时,基线编译器就为函数中的每一个操作生成一个桩。sum += arr[i]会有一个相应的桩,并且把里面的 += 操作当成整数加法。</p><p>但是,sum 和 arr[i] 两个数并不保证都是整数。因为在 JavaScript 中类型都是动态类型,在接下来的循环当中,arr[i] 很有可能变成了string 类型。整数加法和字符串连接是完全不同的两个操作,会被编译成不同的机器码。</p><p>JIT 处理这个问题的方法是编译多基线桩。如果一个代码段是单一形态的(即总是以同一类型被调用),则只生成一个桩。如果是多形态的(即调用的过程中,类型不断变化),则会为操作所调用的每一个类型组合生成一个桩。</p><p>这就是说 JIT 在选择一个桩之前,会进行多分枝选择,类似于决策树,问自己很多问题才会确定最终选择哪个,见下图:</p><img src=\"http://pic3.zhimg.com/v2-1f05c0e29543722fc08df55e8552beae_b.png\" data-rawwidth=\"500\" data-rawheight=\"257\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic3.zhimg.com/v2-1f05c0e29543722fc08df55e8552beae_r.png\"><p>正是因为在基线编译器中每行代码都有自己的桩,所以 JIT 在每行代码被执行的时候都会检查数据类型。在循环的每次迭代,JIT 也都会重复一次分枝选择。</p><img src=\"http://pic4.zhimg.com/v2-eda86cdd0b7e0b00c876bf3d053b440f_b.png\" data-rawwidth=\"500\" data-rawheight=\"323\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic4.zhimg.com/v2-eda86cdd0b7e0b00c876bf3d053b440f_r.png\"><p>如果代码在执行的过程中,JIT 不是每次都重复检查的话,那么执行的还会更快一些,而这就是优化编译器所需要做的工作之一了。</p><p>优化编译器中,整个函数被统一编译,这样的话就可以在循环开始执行之前进行类型检查。</p><img src=\"http://pic3.zhimg.com/v2-b7387c3dc140753eb60482aaede0c046_b.png\" data-rawwidth=\"500\" data-rawheight=\"318\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic3.zhimg.com/v2-b7387c3dc140753eb60482aaede0c046_r.png\"><p>一些浏览器的 JIT 优化更加复杂。比如在 Firefox 中,给一些数组设定了特定的类型,比如里面只包含整型。如果 arr 是这种数组类型,那么 JIT 就不需要检查 arr[i] 是不是整型了,这也意味着 JIT 可以在进入循环之前进行所有的类型检查。</p><h2><b>总结</b></h2><p>简而言之 JIT 是什么呢?它是使 JavaScript 运行更快的一种手段,通过监视代码的运行状态,把 hot 代码(重复执行多次的代码)进行优化。通过这种方式,可以使 JavaScript 应用的性能提升很多倍。</p><p>为了使执行速度变快,JIT 会增加很多多余的开销,这些开销包括:</p><ul><li>优化和去优化开销</li><li>监视器记录信息对内存的开销</li><li>发生去优化情况时恢复信息的记录对内存的开销</li><li>对基线版本和优化后版本记录的内存开销</li></ul><p>这里还有很大的提升空间:即消除开销。通过消除开销使得性能上有进一步地提升,这也是 WebAssembly 所要做的事之一。</p><p><b>转载请注明出处,保留原文链接以及作者信息</b><br></p><p>欢迎大家关注我的<a href=\"https://zhuanlan.zhihu.com/qianduandaha\" class=\"internal\">前端大哈 - 知乎专栏</a>,定期发布高质量前端文章。</p><p>点击《<a href=\"http://link.zhihu.com/?target=http%3A//huziketang.com/blog/posts/detail%3FpostId%3D58c12f36a6d8a07e449fdd22\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">WebAssembly 系列(二)JavaScript Just-in-time (JIT) 工作原理<i class=\"icon-external\"></i></a>》阅读原文。</p>","updated":"2017-03-09T10:17:44.000Z","canComment":false,"commentPermission":"anyone","commentCount":20,"collapsedCount":0,"likeCount":268,"state":"published","isLiked":false,"slug":"25669120","lastestTipjarors":[{"isFollowed":false,"name":"梁少峰","headline":"博客:https://github.com/youngwind/blog","avatarUrl":"https://pic3.zhimg.com/4484be17569981f5f45cbc3097ea8f52_s.jpg","isFollowing":false,"type":"people","slug":"liang-shao-feng-18-12","profileUrl":"https://www.zhihu.com/people/liang-shao-feng-18-12","bio":"前端一枚","hash":"aa880ea607f6eba406695e100ad109cc","uid":611109558924611600,"isOrg":false,"description":"博客:https://github.com/youngwind/blog","isOrgWhiteList":false,"avatar":{"id":"4484be17569981f5f45cbc3097ea8f52","template":"https://pic3.zhimg.com/{id}_{size}.jpg"}},{"isFollowed":false,"name":"黄之昊","headline":"腾讯、阿里,当前在哔哩哔哩。","avatarUrl":"https://pic4.zhimg.com/ad2a69933_s.jpg","isFollowing":false,"type":"people","slug":"heavenhuang","profileUrl":"https://www.zhihu.com/people/heavenhuang","bio":"手艺人/前端","hash":"e06f64003d42b8bfb72a0dee6da92082","uid":26754487943168,"isOrg":false,"description":"腾讯、阿里,当前在哔哩哔哩。","isOrgWhiteList":false,"avatar":{"id":"ad2a69933","template":"https://pic4.zhimg.com/{id}_{size}.jpg"}},{"isFollowed":false,"name":"ko lala","headline":"啦啦啦,寻找生活方向ing。懒惰成性,虚度好几年光阴,早该辍学","avatarUrl":"https://pic1.zhimg.com/0c57f0934_s.jpg","isFollowing":false,"type":"people","slug":"ko-lala","profileUrl":"https://www.zhihu.com/people/ko-lala","bio":null,"hash":"248074182a56fe6060bc695da5c74697","uid":31583805374464,"isOrg":false,"description":"啦啦啦,寻找生活方向ing。懒惰成性,虚度好几年光阴,早该辍学","isOrgWhiteList":false,"avatar":{"id":"0c57f0934","template":"https://pic1.zhimg.com/{id}_{size}.jpg"}}],"isTitleImageFullScreen":false,"rating":"none","titleImage":"https://pic3.zhimg.com/v2-ba1812d24cd3bd3fd5ce2f976c683722_r.png","links":{"comments":"/api/posts/25669120/comments"},"reviewers":[],"topics":[{"url":"https://www.zhihu.com/topic/19608032","id":"19608032","name":"编译器"},{"url":"https://www.zhihu.com/topic/19572307","id":"19572307","name":"即时编译(JIT)"},{"url":"https://www.zhihu.com/topic/19552521","id":"19552521","name":"JavaScript"}],"titleImageSize":{"width":500,"height":286},"href":"/api/posts/25669120","excerptTitle":"","column":{"slug":"qianduandaha","name":"前端大哈"},"tipjarState":"activated","tipjarTagLine":"真诚赞赏,手留余香","sourceUrl":"","pageCommentsCount":20,"tipjarorCount":3,"snapshotUrl":"","publishedTime":"2017-03-09T18:17:44+08:00","url":"/p/25669120","lastestLikers":[{"profileUrl":"https://www.zhihu.com/people/fftang-tang","bio":"web前端设计/开发","hash":"c7f782dfeb9767fd8c63910e61e3f9a6","uid":38477102055424,"isOrg":false,"description":"和喜欢的一切在一起","isOrgWhiteList":false,"slug":"fftang-tang","avatar":{"id":"v2-18fc832d05f591333d8f65158c5f408c","template":"https://pic1.zhimg.com/{id}_{size}.jpg"},"name":"Mani"},{"profileUrl":"https://www.zhihu.com/people/ASeeder","bio":"哲学","hash":"d7157044daecd1c2d9a6f6cc3e69c291","uid":561278180497883140,"isOrg":false,"description":"","isOrgWhiteList":false,"slug":"ASeeder","avatar":{"id":"188fc8eb32f09f19668251bfe8e49ece","template":"https://pic3.zhimg.com/{id}_{size}.jpg"},"name":"Andy Seeder"},{"profileUrl":"https://www.zhihu.com/people/jokers-45","bio":null,"hash":"1d7e167cb4ed5e5d731382d75200d54c","uid":595706793456963600,"isOrg":false,"description":"","isOrgWhiteList":false,"slug":"jokers-45","avatar":{"id":"e3a72f336aff5d80849756eda2423a8e","template":"https://pic3.zhimg.com/{id}_{size}.jpg"},"name":"Jokers"},{"profileUrl":"https://www.zhihu.com/people/tu-zi-58-82","bio":null,"hash":"632bfbbe11996b3991b73e8b96ea5cff","uid":598029346863321100,"isOrg":false,"description":"","isOrgWhiteList":false,"slug":"tu-zi-58-82","avatar":{"id":"da8e974dc","template":"https://pic1.zhimg.com/{id}_{size}.jpg"},"name":"Mab Eugene"},{"profileUrl":"https://www.zhihu.com/people/wang-zhe-95","bio":"前端码农 / 顺道折腾下后端 / 果粉","hash":"471643a64674d4aea9aecf820720e536","uid":27011732996096,"isOrg":false,"description":"","isOrgWhiteList":false,"slug":"wang-zhe-95","avatar":{"id":"bfc50ee1f8bc0f98a48c20b682e831e2","template":"https://pic3.zhimg.com/{id}_{size}.jpg"},"name":"王哲"}],"summary":"<img src=\"http://pic3.zhimg.com/v2-ba1812d24cd3bd3fd5ce2f976c683722_200x112.png\" data-rawwidth=\"500\" data-rawheight=\"286\" class=\"origin_image inline-img zh-lightbox-thumb\" data-original=\"http://pic3.zhimg.com/v2-ba1812d24cd3bd3fd5ce2f976c683722_r.png\">作者:Lin Clark 翻译原文:<a href=\"http://huziketang.com/blog/posts/detail?postId=58c12f36a6d8a07e449fdd22\" data-title=\"WebAssembly 系列(二)JavaScript Just-in-time (JIT) 工作原理\" class=\"\" data-editable=\"true\">http://huziketang.com/blog/posts/detail?postId=58c12f36a6d8a07e449fdd22</a> 英文原文:<a href=\"https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/\" class=\"\" data-editable=\"true\" data-title=\"A crash course in just-in-time (JIT) compilers\">A crash course in just-in-time (JIT) compilers</a>本文是关于 WebAssembly 系列的第二篇文章<b>(本系列共六篇文章)</b>。如果你没有读先前文章的话,建议<a href=\"http://huziketang.com/blog/posts/detail?postId=58ce8036a6d8a07e449fdd27\" data-editable=\"true\" data-title=\"先读这里\" class=\"\">先读这里</a>。如果对 WebAssembly 没概念,建议<a href=\"http://blog.csdn.net/wulixiaoxiao1/article/details/60581397\" data-editable=\"true\" data-title=\"先读这里(中文文章)\" class=\"\">先…</a>","reviewingCommentsCount":0,"meta":{"previous":{"isTitleImageFullScreen":false,"rating":"none","titleImage":"https://pic3.zhimg.com/v2-d83cdeadf45f4a2e9fdd580d697588d2_r.png","links":{"comments":"/api/posts/25596667/comments"},"topics":[{"url":"https://www.zhihu.com/topic/19588535","id":"19588535","name":"HTTP"},{"url":"https://www.zhihu.com/topic/19564998","id":"19564998","name":"缓存"},{"url":"https://www.zhihu.com/topic/19566617","id":"19566617","name":"CDN"}],"href":"/api/posts/25596667","excerptTitle":"","author":{"profileUrl":"https://www.zhihu.com/people/hu-zi-da-ha","bio":"全家都是做前端的","hash":"27cfd0278c2ebc9ce70a15ad82083895","uid":806125621271998500,"isOrg":false,"description":"行文紧凑强迫症。\n前端货物倾倒机。\n本业数据挖掘机。\n\n最好的 React.js 教程 《React.js 小书》: http://react.huziketang.com","isOrgWhiteList":false,"slug":"hu-zi-da-ha","avatar":{"id":"v2-91e59f73a4975cb81bcf18566ac586a2","template":"https://pic3.zhimg.com/{id}_{size}.jpg"},"name":"胡子大哈"},"column":{"slug":"qianduandaha","name":"前端大哈"},"content":"<p>原文链接:<a href=\"http://link.zhihu.com/?target=http%3A//huziketang.com/blog/posts/detail%3FpostId%3D58bd4dd1204d50674934c3b0\" class=\" external\" target=\"_blank\" rel=\"nofollow noreferrer\"><span class=\"invisible\">http://</span><span class=\"visible\">huziketang.com/blog/pos</span><span class=\"invisible\">ts/detail?postId=58bd4dd1204d50674934c3b0</span><span class=\"ellipsis\"></span><i class=\"icon-external\"></i></a></p><p><strong>转载请注明出处,保留原文链接以及作者信息</strong><br></p><p><strong>CDN类的网站曾经一度雄踞 Alexa 域名排行的前 100。以前一些小网站不需要使用 CDN 或者根本负担不起其价格,不过这一现象近几年发生了很大的变化,CDN 市场上出现了很多按次付费,非公司性的提供商,这使得 CDN 变成人人都能负担的起的一种服务了。本文讲述的就是如何使用这种简单易用的缓存服务。</strong></p><p>上篇文章<a href=\"http://link.zhihu.com/?target=http%3A//huziketang.com/blog/posts/detail%3FpostId%3D58b77935204d50674934c3ad\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">掌握 HTTP 缓存--从请求到响应过程的一切(上) - 知乎专栏<i class=\"icon-external\"></i></a>中,我们讨论了关于利用 HTTP 头来解决缓存问题,这篇文章我们将介绍缓存和 Cookie之间的关系。</p><h2><b>Cookies</b></h2><p>你已经知道了缓存头是如何起作用的,现在我们来看下在缓存里面 cookie 起了什么作用。首先, Cookie 的设定也在 HTTP 响应头中,名字是 Set-Cookie。设置一个 cookie 的目的是标识这个用户,就是说你需要为每个用户设置一个 cookie。</p><p>想象一下缓存的场景,你是否会缓存一个包含了Set-Cookie的 HTTP 响应,在缓存时间内,每个人都会得到相同的 cookie 和同样的用户 session?你肯定不想这样。</p><p>另外,用户 session 状态的改变可能会影响到响应内容的变化。一个简单的场景:电商购物车。你给用户要么提供一个空购物车,要么是用户自己选了很多物品的购物车。同样的道理,你不希望这个也被缓存,毕竟每个用户都应该有自己的购物车。</p><p>一个解决方法是在运行时通过 JavaScript 设置 Cookie,比如 Google Analytics。GA 通过 JS 设置 cookie,但这个 cookie 既不影响渲染,也不设置 Set-Cookie 头。GA 会在目标网站上添加类似于 \"you are tracked via Google Analytics\" 的图标,<em>但是只要这些改变都是在运行时添加进去的,就都没有问题</em>。</p><h2><b>正确处理 cookie 和缓存</b></h2><p>首先你需要知道你网站的 cookie 的工作原理。cookie 是不是只在特定时间使用(如在用户登录过程中使用)?原则上,cookie 是不是会被注入到所有响应?</p><p>正如上一节所说的,不论何时服务器返回了一个带有Set-Cookie 的响应,你都希望能够保证它不会被缓存。那么问题就转化成为,当你返回一个带有“用户特性”内容的响应时(如购物车),CDN /代理服务器,会作何操作?</p><ul><li>如果没设置 Set-Cookie,是不是允许缓存呢?</li><li>如果设置了 Set-Cookie,是不是自动丢弃所有Cache-Control 头呢?</li></ul><p>其实,如果从应用层面来讲,你尽管可以去实现你所喜欢的 web 应用就可以了,至于 cookie 和 CDN 都是自动设置的。还是用 Apache 的 .htaccess 来作为例子来解释:</p><div class=\"highlight\"><pre><code class=\"language-text\"><span></span># 1) 如果 cookie 没设置,允许缓存\nHeader set Cache-Control \"public max-age=3600\" \"expr=-z resp('Set-Cookie')\n\n# 2) 如果 cookie 被设置,不允许缓存\nHeader always remove Cache-Control \"expr=-n resp('Set-Cookie')\n\n# 2a) 第二条的另一种形式,如果设置了 cookie,缓存时间设置成0\nHeader set Cache-Control \"no-cache max-age=0 must-revalidate\" \"expr=-n resp('Set-Cookie')\n</code></pre></div><ul><li>规则1:如果没设置 Set-Cookie,则给Cache-Control 设置一个默认值;</li><li>规则2:如果设置了 Set-Cookie,则忽略Cache-Control;</li><li>规则2a:是规则2的另一种表示形式,设置最大缓存时间是 0。</li></ul><h3><b>无 cookie 的访问路径</b></h3><p>一些 CMS / 框架还在使用一种暴力的方式种 cookie。而实际上,决定是否种 cookie 取决于不同的因素,比如会话时间因素。如果你有一个很高安全性的 web 应用,设置会话时间是 5 分钟,那么为每个响应设置一个新 cookie 都不过分。而假设你的应用连“用户特性”都没有,也就是说所有的东西对所有用户都是公用的,那么设置任何形式的 cookie 都是没有道理的。</p><p>所以下面这个例子是否适合你自己,很大程度上依赖于你的应用到底是什么类型的。我们来一起看一下,我先给一下这个例子的上下文关系:假设你有个新网站,你的所有文章都在 <a href=\"http://link.zhihu.com/?target=http%3A//www.foobar.tld/news/item/\" class=\" external\" target=\"_blank\" rel=\"nofollow noreferrer\"><span class=\"invisible\">http://www.</span><span class=\"visible\">foobar.tld/news/item/</span><span class=\"invisible\"></span><i class=\"icon-external\"></i></a> 这个路径下面。现在你希望能够保证,所有访问 /news/item/<ID> 的路径都不包含 Set-Cookie,因为你确定不需要 cookie。</p><div class=\"highlight\"><pre><code class=\"language-js\"><span></span><span class=\"err\">#</span> <span class=\"nx\">通用</span> <span class=\"nx\">PHP</span> <span class=\"nx\">重定向做法</span><span class=\"err\">,</span><span class=\"nx\">将</span><span class=\"s2\">\"?path=$1\"</span><span class=\"nx\">写到重定向规则里</span>\n<span class=\"nx\">RewriteCond</span> <span class=\"o\">%</span><span class=\"p\">{</span><span class=\"nx\">REQUEST_FILENAME</span><span class=\"p\">}</span> <span class=\"o\">!-</span><span class=\"nx\">d</span>\n<span class=\"nx\">RewriteCond</span> <span class=\"o\">%</span><span class=\"p\">{</span><span class=\"nx\">REQUEST_FILENAME</span><span class=\"p\">}</span> <span class=\"o\">!-</span><span class=\"nx\">f</span>\n<span class=\"nx\">RewriteRule</span> <span class=\"o\">^</span><span class=\"p\">(.</span><span class=\"o\">*</span><span class=\"p\">)</span><span class=\"nx\">$</span> <span class=\"nx\">index</span><span class=\"p\">.</span><span class=\"nx\">php</span><span class=\"o\">?</span><span class=\"nx\">path</span><span class=\"o\">=</span><span class=\"nx\">$1</span> <span class=\"p\">[</span><span class=\"nx\">NC</span><span class=\"p\">,</span><span class=\"nx\">L</span><span class=\"p\">,</span><span class=\"nx\">QSA</span><span class=\"p\">]</span>\n<span class=\"nx\">RewriteRule</span> <span class=\"o\">^</span><span class=\"nx\">$</span> <span class=\"nx\">index</span><span class=\"p\">.</span><span class=\"nx\">php</span> <span class=\"p\">[</span><span class=\"nx\">NC</span><span class=\"p\">,</span><span class=\"nx\">L</span><span class=\"p\">,</span><span class=\"nx\">QSA</span><span class=\"p\">]</span>\n\n<span class=\"err\">#</span> <span class=\"nx\">利用</span> <span class=\"nx\">query</span> <span class=\"nx\">中的</span> <span class=\"nx\">path</span><span class=\"o\">=</span> <span class=\"nx\">来判断</span>\n<span class=\"o\"><</span><span class=\"nx\">If</span> <span class=\"s2\">\"%{QUERY_STRING} =~ m#path=news/item/[^&]+#\"</span><span class=\"o\">></span>\n <span class=\"nx\">Header</span> <span class=\"nx\">always</span> <span class=\"nx\">unset</span> <span class=\"nx\">Set</span><span class=\"o\">-</span><span class=\"nx\">Cookie</span>\n<span class=\"o\"><</span><span class=\"err\">/If></span>\n</code></pre></div><p>通过这样的设置,你就可以保证所有访问/news/item/<ID> 的路径都不包含 Set-Cookie。而到底是否应该设置 cookie,需要你根据你自己的应用特点来判断。</p><h3><b>设计出来的缓存能力</b></h3><p>有很多设计方案可以使你的 web 应用具有高缓存性。鉴于本文仅仅是一篇文章而不是一本书,我不可能每个点都深入的来讲,但是我可以着重提一下通用的方法。</p><p>我还用电商作为例子。假设电商网站首页的 top 位置上展示了正在出售的物品,生成这些物品需要进行若干次的数据库操作,代价比较大,因此希望把它们缓存起来。但是,问题在于购物车,它是为那些登陆用户准备的,所以希望得到的结果是: top 物品是一样的,而针对登陆用户展示购物车。</p><p>那么优化策略首先要为每个用户提供一个和登陆状态无关的“通用”页。然后通过 JavaScript 为已经生成的网页提供购物车。站在用户的视角,最终展示形式是一样的。那么现在你有了两个请求(整个网页请求 + 购物车请求),而不是一个请求(整个网页请求,包含购物车)。ok,现在你可以把代价很大的部分,即 top 物品分离出来,把它们缓存起来了。</p><p>这种方法或者其延伸方法,不适合已经开发好的项目。因为它可能会改变很多接口和视图层(MVC 架构)的内容。最好你在一开始就设计好。</p><h2><b>缓存失效:busting 和 purging</b></h2><p>使用 max-age 和 s-maxage 你已经可以很好地控制一个指定的响应被缓存多长时间。但是这不足以适用于所有的情况。这些设置都是在返回响应时预设的,而现实情况往往是并不知道一个响应应该设置多久期满。回想一下刚才电商首页的例子:假设它包含了展示在 top 位置的 10 个实体。你设置了 max-age=900给这个首页以保证每15分钟刷新一次。现在,其中 1 个实体由于发布了太久了要被撤销,那么你就需要把之前的缓存响应删掉,这时候其实还没到 15 分钟,那么该怎么办?</p><p>不要担心,这是一个常见的问题,有很多方法解决。首先我们先来解释一下术语:</p><ul><li><strong>缓存 busting</strong>,是用来解决浏览器长期缓存问题,它通过版本标识来告诉浏览器该文件有一个新的版本。这时浏览器将不会从本地缓存取内容,而从源服务器请求新版本的文件。缓存 busting的详细介绍在这里:<a href=\"http://link.zhihu.com/?target=https%3A//www.keycdn.com/support/what-is-cache-busting/\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">What is Cache Busting?<i class=\"icon-external\"></i></a>。<br></li><li><strong>缓存 purging</strong>,表示直接从缓存中删除内容(即响应),以使得缓存可以立马得到更新。</li></ul><h3><b>用于版本管理的缓存 busting</b></h3><p>这种方法经常使用在 CSS 文件、JS 文件上。通常一个确切的版本号、一串哈希或者时间戳都可以用作标识,如下面的例子:</p><ul><li>数字版本号:style-v1.css,style.css?v=1</li><li>哈希串版本:style.css?d3b07384d113edec49eaa6238ad5ff00</li><li>时间戳版本:styles.css?t=1486398121</li></ul><p>这时候在发布程序的时候,你只要注意文件的版本就可以了。举个例子,一个 HTML 网页通过<link rel=\"stylesheet\" href=\"..\"> 这种形式包含了一个 CSS 文件。CSS 文件将会被缓存起来,这时如果你想让你的新 CSS 文件起作用,那么用最新的版本号命名它就可以。如果不做任何变化的话,即便你更新了文件,这个 HTML 还会使用缓存中的旧 CSS 文件。</p><h3><b>缓存 purging</b></h3><p>不同 CDN 供应商清除缓存的方式不一样。很多供应商都是基于开源软件 <a href=\"http://link.zhihu.com/?target=https%3A//varnish-cache.org/\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">Varnish<i class=\"icon-external\"></i></a> 来构建自己的 CDN 服务,所以一个通用的做法是在 HTPP 请求中使用 PURGE 结构,如:</p><div class=\"highlight\"><pre><code class=\"language-text\"><span></span>PURGE /news/item/i-am-obsolete HTTP/1.1\nHost: www.foobar.tld\n</code></pre></div><p>使用这个请求通常需要权限认证,或者是源确认(即 IP 白名单),不过不同供应商的要求也不一样。</p><p>清除一个或几个缓存项比较容易,但是在某些场景下,却不是这么简单。举个例子,一个博客的场景,博客里面都有关于作者的部分,现在你要改变关于作者的一些内容,那么你需要手动清理所有包含了作者信息的页面。你确实可以一个一个手动清理,但是假设你有成千上万个网页被影响了,那问题就变得麻烦了。</p><p>下面介绍一个解决方案。</p><h3><b>代理标签</b></h3><p>“代理标签” 这个名字来源于 CDN 供应商 <a href=\"http://link.zhihu.com/?target=https%3A//www.fastly.com/\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">Fastly<i class=\"icon-external\"></i></a>,不同供应商给它起的名字不一样,比如还有叫它“缓存标签”的,Varnish 叫它 <a href=\"http://link.zhihu.com/?target=http%3A//book.varnish-software.com/4.0/chapters/Cache_Invalidation.html%23hashtwo-xkey-varnish-software-implementation-of-surrogate-keys\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">Hashtwo/Xkey<i class=\"icon-external\"></i></a>,这里我就不详细介绍其他供应商的情况了。</p><p>不论它叫什么,它们的目的都是一样的:给响应打标签。这样你就可以轻松地从缓存中删除相关的标签就可以,甚至都不用知道缓存的到底是什么东西。</p><p>还是拿<客户端-代理-源端>来举例子,源端返回一个含有代理标签的响应:</p><div class=\"highlight\"><pre><code class=\"language-text\"><span></span>HTTP/1.1 200 OK\nContent-Type: text/html\nContent-Length: 123\nSurrogate-Key: top-10 company-acme category-foodstuff\n</code></pre></div><p>这个例子中的标签为:top-10, company-acme,和category-foodstuff。这里给一个电商的实际场景来理解其含义:这个响应包含了电商首页的前 10 个物品,这些物品由 ACME 公司提供,并且其目录类别都设定为食品类。</p><p>设置了标签以后,当物品发生了变化以后,你只需要删除包含有 company-acme 和 top-10 的标签就可以了。是不是很简单?</p><p>同样,具体如何清除缓存的操作方法,不同 CDN 供应商是不一样的。</p><h2><b>写在最后</b></h2><p>上面讨论的更多的是理论上的做法,还有很多文章专门介绍不同的 CDN 的使用。如果你想深入了解的话,下面的资料每篇可能都是你需要的。</p><ul><li><a href=\"http://link.zhihu.com/?target=https%3A//developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching%3Fhl%3Den%23cache-control\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">谷歌开发者:HTTP 缓存<i class=\"icon-external\"></i></a></li><li><a href=\"http://link.zhihu.com/?target=http%3A//www.whoishostingthis.com/blog/2010/06/30/cdns-push-vs-pull/\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">Push CDN 和 Pull CDN<i class=\"icon-external\"></i></a></li><li><a href=\"http://link.zhihu.com/?target=http%3A//www.the-toffee-project.org/index.php%3Fpage%3D32-cdn-content-delivery-networks-types\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">CDN 类型(管理员视角)<i class=\"icon-external\"></i></a></li><li><a href=\"http://link.zhihu.com/?target=https%3A//www.keycdn.com/support/http-caching-headers/\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">缓存头概览<i class=\"icon-external\"></i></a></li><li><a href=\"http://link.zhihu.com/?target=https%3A//developer.mozilla.org/en-US/docs/Web/HTTP/Caching\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">缓存详解(Mozilla)<i class=\"icon-external\"></i></a></li><li><a href=\"http://link.zhihu.com/?target=https%3A//developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">ETag头详解(Mozilla)<i class=\"icon-external\"></i></a></li><li><a href=\"http://link.zhihu.com/?target=https%3A//developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">Cace-Control 头详解(Mozilla)<i class=\"icon-external\"></i></a></li><li><a href=\"http://link.zhihu.com/?target=https%3A//developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">If-None-Match 头详解(Mozilla)<i class=\"icon-external\"></i></a></li><li><a href=\"http://link.zhihu.com/?target=https%3A//docs.fastly.com/guides/purging/getting-started-with-surrogate-keys\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">Fastly:代理标签<i class=\"icon-external\"></i></a></li><li><a href=\"http://link.zhihu.com/?target=https%3A//www.keycdn.com/support/purge-cdn-cache/\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">KeyCDN:缓存标签<i class=\"icon-external\"></i></a></li></ul><p>欢迎大家关注我的<a href=\"https://zhuanlan.zhihu.com/qianduandaha\" class=\"internal\">前端大哈 - 知乎专栏</a>,定期发布高质量前端文章。</p><p>点击《<a href=\"http://link.zhihu.com/?target=http%3A//huziketang.com/blog/posts/detail%3FpostId%3D58bd4dd1204d50674934c3b0\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">掌握 HTTP 缓存——从请求到响应过程的一切(下)<i class=\"icon-external\"></i></a>》阅读原文。</p>","state":"published","sourceUrl":"","pageCommentsCount":0,"canComment":false,"snapshotUrl":"","slug":25596667,"publishedTime":"2017-03-06T18:08:08+08:00","url":"/p/25596667","title":"掌握 HTTP 缓存——从请求到响应过程的一切(下)","summary":"原文链接:<a href=\"http://huziketang.com/blog/posts/detail?postId=58bd4dd1204d50674934c3b0\" class=\"\" data-editable=\"true\" data-title=\"掌握 HTTP 缓存--从请求到响应过程的一切(下)\">http://huziketang.com/blog/posts/detail?postId=58bd4dd1204d50674934c3b0</a><strong>转载请注明出处,保留原文链接以及作者信息</strong> <strong>CDN类的网站曾经一度雄踞 Alexa 域名排行的前 100。以前一些小网站不需要使用 CDN 或者根本负担不起其价格,不过这一现象近几年发生了很大的变化,CDN 市场上出现了很多按次付费,…</strong>","reviewingCommentsCount":0,"meta":{"previous":null,"next":null},"commentPermission":"anyone","commentsCount":0,"likesCount":0},"next":{"isTitleImageFullScreen":false,"rating":"none","titleImage":"https://pic3.zhimg.com/v2-cd2cfd2a3f5f0c8a2130a8b141df7d26_r.png","links":{"comments":"/api/posts/25718411/comments"},"topics":[{"url":"https://www.zhihu.com/topic/19608032","id":"19608032","name":"编译器"},{"url":"https://www.zhihu.com/topic/20024261","id":"20024261","name":"WebAssembly"},{"url":"https://www.zhihu.com/topic/19552521","id":"19552521","name":"JavaScript"}],"href":"/api/posts/25718411","excerptTitle":"","author":{"profileUrl":"https://www.zhihu.com/people/hu-zi-da-ha","bio":"全家都是做前端的","hash":"27cfd0278c2ebc9ce70a15ad82083895","uid":806125621271998500,"isOrg":false,"description":"行文紧凑强迫症。\n前端货物倾倒机。\n本业数据挖掘机。\n\n最好的 React.js 教程 《React.js 小书》: http://react.huziketang.com","isOrgWhiteList":false,"slug":"hu-zi-da-ha","avatar":{"id":"v2-91e59f73a4975cb81bcf18566ac586a2","template":"https://pic3.zhimg.com/{id}_{size}.jpg"},"name":"胡子大哈"},"column":{"slug":"qianduandaha","name":"前端大哈"},"content":"<blockquote>本文作者:Lin Clark <br>翻译原文:<a href=\"http://link.zhihu.com/?target=http%3A//huziketang.com/blog/posts/detail%3FpostId%3D58c55a3ba6d8a07e449fdd23\" class=\" external\" target=\"_blank\" rel=\"nofollow noreferrer\"><span class=\"invisible\">http://</span><span class=\"visible\">huziketang.com/blog/pos</span><span class=\"invisible\">ts/detail?postId=58c55a3ba6d8a07e449fdd23</span><span class=\"ellipsis\"></span><i class=\"icon-external\"></i></a><br>英文原文:<a href=\"http://link.zhihu.com/?target=https%3A//hacks.mozilla.org/2017/02/a-crash-course-in-assembly/\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">A crash course in assembly<i class=\"icon-external\"></i></a></blockquote><p><i>本文是关于 WebAssembly 系列的第三篇文章</i><b>(本系列共六篇文章)</b><i>。如果你没有读先前文章的话,建议</i><a href=\"http://link.zhihu.com/?target=http%3A//huziketang.com/blog/posts/detail%3FpostId%3D58ce8036a6d8a07e449fdd27\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">先读这里<i class=\"icon-external\"></i></a><i>。如果对 WebAssembly 没概念,建议</i><a href=\"http://link.zhihu.com/?target=http%3A//blog.csdn.net/wulixiaoxiao1/article/details/60581397\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">先读这里<i class=\"icon-external\"></i></a><i>。</i></p><br><p>理解什么是汇编,以及编译器如何生成它,对于理解 WebAssembly 是很有帮助的。</p><p>在上一篇<a href=\"http://link.zhihu.com/?target=http%3A//huziketang.com/blog/posts/detail%3FpostId%3D58c12f36a6d8a07e449fdd22\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">关于 JIT<i class=\"icon-external\"></i></a> 的文章中,我介绍了和计算机打交道,就像同外星人打交道一样。</p><img src=\"http://pic4.zhimg.com/v2-405c65cbcffedcce6761a6981fb98dcf_b.png\" data-rawwidth=\"500\" data-rawheight=\"286\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic4.zhimg.com/v2-405c65cbcffedcce6761a6981fb98dcf_r.png\"><p>现在来思考一下“外星人”的大脑是如何工作的——机器的“大脑”是如何对我们输入给它的内容进行分析和理解的。</p><p>“大脑”中,有一部分负责思考——处理加法、减法或者逻辑运算。还有其他的部分分别负责短暂记忆和长期记忆的。</p><p>这些不同的部分都有自己的名字:</p><ul><li>负责思考的部分叫做算数逻辑单元(ALU)</li><li>寄存器提供短暂记忆功能</li><li>随机存取存储器(RAM)提供长期记忆功能</li></ul><img src=\"http://pic1.zhimg.com/v2-78d0e2419d5e59bf29409e136ad9cb18_b.png\" data-rawwidth=\"500\" data-rawheight=\"302\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic1.zhimg.com/v2-78d0e2419d5e59bf29409e136ad9cb18_r.png\"><p>机器代码中的语句称作指令。</p><p>那么在指令进入“大脑”以后都发生了什么呢?它们会被切分为不同的部分传送到不同的单元进行处理。</p><p>“大脑”切分指令通过不同连接线路进行。举个例子,“大脑”会将指令最开始的 6 比特通过管道送到 ALU 中。而 ALU 会通过 0 和 1 的位置来决定对两个数做加法。</p><p>这串 01 串就叫做“操作码”,它告诉了 ALU 要执行什么样的操作。</p><img src=\"http://pic1.zhimg.com/v2-0baca951eba892c2e56d9dd05c721608_b.png\" data-rawwidth=\"500\" data-rawheight=\"354\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic1.zhimg.com/v2-0baca951eba892c2e56d9dd05c721608_r.png\"><p>然后“大脑”会取后面两个连续的 3 比特 01 串来确定把哪两个数加到一起,而这 3 比特指的是寄存器的地址。<br><img src=\"http://pic1.zhimg.com/v2-cb22ad704ae01e92314689c2256a47dc_b.png\" data-rawwidth=\"500\" data-rawheight=\"352\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic1.zhimg.com/v2-cb22ad704ae01e92314689c2256a47dc_r.png\"></p><p>注意看上面机器码的注释:“ADD R1 R2”,这对于人类来讲很容易理解其含义。这就是汇编,也叫符号机器码,它使人类也能看懂机器代码的含义。<br></p><p>可以看到汇编和这台机器的机器码之间有直接的映射关系。正是因为如此,拥有不同机器结构的计算机会有不同的汇编系统。如果你有一个机器,它有自己的内部结构,那么它就需要它所独有的汇编语言。</p><p>从上面的分析可以知道我们进行机器码的翻译并不是只有一种,不同的机器有不同的机器码,就像我们人类也说各种各样的语言一样,机器也“说”不同的语言。</p><p>人类和外星人之间的语言翻译,可能会从英语、德语或中文翻译到外星语 A 或者外星语 B。而在程序的世界里,则是从 C、C++ 或者 JAVA 翻译到 x86 或者 ARM。</p><p>你想要从任意一个高级语言翻译到众多汇编语言中的一种(依赖机器内部结构),其中一种方式是创建不同的翻译器来完成各种高级语言到汇编的映射。</p><img src=\"http://pic4.zhimg.com/v2-f79052d5f7cff1791a2d929b4eb1bbf3_b.png\" data-rawwidth=\"500\" data-rawheight=\"308\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic4.zhimg.com/v2-f79052d5f7cff1791a2d929b4eb1bbf3_r.png\"><p>这种翻译的效率实在太低了。为了解决这个问题,大多数编译器都会在中间多加一层。它会把高级语言翻译到一个低层,而这个低层又没有低到机器码这个层级。这就是中间代码( intermediate representation,IR)。</p><img src=\"http://pic3.zhimg.com/v2-cd2cfd2a3f5f0c8a2130a8b141df7d26_b.png\" data-rawwidth=\"500\" data-rawheight=\"317\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic3.zhimg.com/v2-cd2cfd2a3f5f0c8a2130a8b141df7d26_r.png\"><p>这就是说编译器会把高级语言翻译到 IR 语言,而编译器另外的部分再把 IR 语言编译成特定目标结构的可执行代码。</p><p>重新总结一下:<strong>编译器的前端把高级语言翻译到 IR,编译器的后端把 IR 翻译成目标机器的汇编代码。</strong></p><img src=\"http://pic4.zhimg.com/v2-815d2ddeb6e1a84d82918a29ae79736b_b.png\" data-rawwidth=\"500\" data-rawheight=\"306\" class=\"origin_image zh-lightbox-thumb\" width=\"500\" data-original=\"http://pic4.zhimg.com/v2-815d2ddeb6e1a84d82918a29ae79736b_r.png\"><br><h2><b>总结</b></h2><p>本文介绍了什么是汇编以及编译器是如何把高级语言翻译成汇编语言的,在<a href=\"http://link.zhihu.com/?target=http%3A//huziketang.com/blog/posts/detail%3FpostId%3D58c77641a6d8a07e449fdd24\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">下一篇<i class=\"icon-external\"></i></a>文章中,我们来介绍 WebAssembly 的工作原理。</p><p><b>转载请注明出处,保留原文链接以及作者信息</b><br></p>欢迎大家关注我的<a href=\"https://zhuanlan.zhihu.com/qianduandaha\" class=\"internal\">前端大哈 - 知乎专栏</a>,定期发布高质量前端文章。<p>点击《<a href=\"http://link.zhihu.com/?target=http%3A//huziketang.com/blog/posts/detail%3FpostId%3D58c55a3ba6d8a07e449fdd23\" class=\" wrap external\" target=\"_blank\" rel=\"nofollow noreferrer\">WebAssembly 系列(三)编译器如何生成汇编<i class=\"icon-external\"></i></a>》阅读原文。</p>","state":"published","sourceUrl":"","pageCommentsCount":0,"canComment":false,"snapshotUrl":"","slug":25718411,"publishedTime":"2017-03-12T19:36:07+08:00","url":"/p/25718411","title":"WebAssembly 系列(三)编译器如何生成汇编","summary":"本文作者:Lin Clark 翻译原文:<a href=\"http://huziketang.com/blog/posts/detail?postId=58c55a3ba6d8a07e449fdd23\" data-title=\"WebAssembly 系列(三)编译器如何生成汇编\" class=\"\" data-editable=\"true\">http://huziketang.com/blog/posts/detail?postId=58c55a3ba6d8a07e449fdd23</a> 英文原文:<a href=\"https://hacks.mozilla.org/2017/02/a-crash-course-in-assembly/\" data-editable=\"true\" data-title=\"A crash course in assembly\" class=\"\">A crash course in assembly</a><i>本文是关于 WebAssembly 系列的第三篇文章</i><b>(本系列共六篇文章)</b><i>。如果你没有读先前文章的话,建议</i><a href=\"http://huziketang.com/blog/posts/detail?postId=58ce8036a6d8a07e449fdd27\" data-editable=\"true\" data-title=\"先读这里\" class=\"\">先读这里</a><i>。如果对 WebAssembly 没概念,建议</i><a href=\"http://link.zhihu.com/?target=http%3A//blog.csdn.net/wulixiaoxiao1/article/details/60581397\" data-editable=\"true\" data-title=\"先读这里(中文文章)\" class=\"\">先读这里</a><i>。</i> 理解什么…","reviewingCommentsCount":0,"meta":{"previous":null,"next":null},"commentPermission":"anyone","commentsCount":0,"likesCount":0}},"commentsCount":20,"likesCount":268,"FULLINFO":true}},"User":{"hu-zi-da-ha":{"isFollowed":false,"name":"胡子大哈","headline":"行文紧凑强迫症。\n前端货物倾倒机。\n本业数据挖掘机。\n\n最好的 React.js 教程 《React.js 小书》: http://react.huziketang.com","avatarUrl":"https://pic3.zhimg.com/v2-91e59f73a4975cb81bcf18566ac586a2_s.jpg","isFollowing":false,"type":"people","slug":"hu-zi-da-ha","bio":"全家都是做前端的","hash":"27cfd0278c2ebc9ce70a15ad82083895","uid":806125621271998500,"isOrg":false,"description":"行文紧凑强迫症。\n前端货物倾倒机。\n本业数据挖掘机。\n\n最好的 React.js 教程 《React.js 小书》: http://react.huziketang.com","profileUrl":"https://www.zhihu.com/people/hu-zi-da-ha","avatar":{"id":"v2-91e59f73a4975cb81bcf18566ac586a2","template":"https://pic3.zhimg.com/{id}_{size}.jpg"},"isOrgWhiteList":false,"badge":{"identity":null,"bestAnswerer":null}}},"Comment":{},"favlists":{}},"me":{},"global":{},"columns":{"qianduandaha":{"following":false,"canManage":false,"href":"/api/columns/qianduandaha","name":"前端大哈","creator":{"slug":"hu-zi-da-ha"},"url":"/qianduandaha","slug":"qianduandaha","avatar":{"id":"v2-5443d3a6b20ed56e4fb6dcdeca958019","template":"https://pic2.zhimg.com/{id}_{size}.jpg"}}},"columnPosts":{},"postComments":{},"postReviewComments":{"comments":[],"newComments":[],"hasMore":true},"favlistsByUser":{},"favlistRelations":{},"promotions":{}}