<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>第 15 章 编译器流水线 on Go 语言原本</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/</link><description>Recent content in 第 15 章 编译器流水线 on Go 语言原本</description><generator>Hugo</generator><language>zh-cn</language><atom:link href="http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/index.xml" rel="self" type="application/rss+xml"/><item><title>15.1 词法与文法</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/parse/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/parse/</guid><description>&lt;h1 id="151-词法与文法"&gt;15.1 词法与文法&lt;/h1&gt;
&lt;p&gt;编译的第一站，是把源码文本变成结构化的&lt;strong&gt;抽象语法树&lt;/strong&gt;（AST）。这要经过&lt;strong&gt;词法分析&lt;/strong&gt;（把字符流
切成 token）与&lt;strong&gt;语法分析&lt;/strong&gt;（按文法把 token 组织成树）。&lt;a href="../../../part1overview/ch03life/compile"&gt;3.2&lt;/a&gt;
鸟瞰过整条流水线，这一节专看它的前端，以及 Go 的文法为何被设计得如此「好解析」。&lt;/p&gt;
&lt;p&gt;承担这两步的，是编译器里一个自成一体的包 &lt;code&gt;cmd/compile/internal/syntax&lt;/code&gt;。它由两件器物构成：
&lt;strong&gt;scanner&lt;/strong&gt;（词法器）按字符读入、吐出 token 流；&lt;strong&gt;parser&lt;/strong&gt;（语法器）以&lt;strong&gt;递归下降&lt;/strong&gt;的方式消费
token、建出语法树。这个包的注释甚至自豪地写明，它的几个文件 &lt;code&gt;scanner.go&lt;/code&gt;、&lt;code&gt;source.go&lt;/code&gt;、
&lt;code&gt;tokens.go&lt;/code&gt; 不依赖编译器其余部分，可单独编译成一个独立的库。词法与文法之所以能如此干净地切出来，
根子在 Go 文法本身的简单。&lt;/p&gt;
&lt;h2 id="1511-为快速解析而设计的文法"&gt;15.1.1 为快速解析而设计的文法&lt;/h2&gt;
&lt;p&gt;Go 的文法是&lt;strong&gt;刻意为快速解析而设计&lt;/strong&gt;的（&lt;a href="../../../part1overview/ch01intro/history"&gt;1.1&lt;/a&gt; 的编译速度
执念）。关键的一点是：它&lt;strong&gt;正则到足以被 LALR(1) 解析&lt;/strong&gt;，因而无需复杂的回溯。早年的 gc 编译器正是
用 yacc 喂一份 LALR(1) 文法（&lt;code&gt;go.y&lt;/code&gt;）来解析 Go 的，这份文法的存在本身，就是「Go 文法可被
单遍、确定地解析」的证据。换言之，解析一段 Go 代码，编译器读一遍 token 流、只看眼前一个 token
就能决定怎么走，不必回头重试，也不必在解析途中查询符号表。&lt;/p&gt;
&lt;p&gt;这与 C/C++ 形成鲜明对比。C 的文法里，&lt;code&gt;a * b;&lt;/code&gt; 究竟是「&lt;code&gt;a&lt;/code&gt; 乘 &lt;code&gt;b&lt;/code&gt;」还是「声明一个指向 &lt;code&gt;a&lt;/code&gt;
类型的指针 &lt;code&gt;b&lt;/code&gt;」，取决于 &lt;code&gt;a&lt;/code&gt; 此刻是不是一个类型名，而这要查符号表才知道。解析与语义于是纠缠在
一起，C++ 更因此背上了著名的 &lt;em&gt;most vexing parse&lt;/em&gt;：&lt;code&gt;Widget w(Thing());&lt;/code&gt; 会被解析成函数声明而非
对象构造。Go 的文法刻意回避了这类歧义，任何一段 token 的结构都由文法唯一确定，与名字的含义无关。
解析器因此又快又简单，也不必把类型信息回灌给词法器。&lt;/p&gt;</description></item><item><title>15.2 中间表示</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/ssa/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/ssa/</guid><description>&lt;h1 id="152-中间表示"&gt;15.2 中间表示&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本节内容对标 Go 1.26。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=".././parse"&gt;15.1&lt;/a&gt; 把源码变成了语法树（AST）。AST 忠实记录了程序员写下的结构：变量、作用域、
表达式的嵌套。可一旦要做优化，这套结构就显得太「高」了。考虑一句最不起眼的 &lt;code&gt;x = x + 1&lt;/code&gt;：
在 AST 里，左边的 &lt;code&gt;x&lt;/code&gt; 与右边的 &lt;code&gt;x&lt;/code&gt; 是同一个名字，编译器若想知道「这次用到的 &lt;code&gt;x&lt;/code&gt; 究竟是哪一次
赋值产生的值」，就得反复做作用域查找与数据流分析。名字会被覆盖，作用域会嵌套，赋值会把旧值
冲掉，这些都让「一个值从哪来、到哪去」这件本该最基本的事变得晦暗。优化器最想要的，恰恰是把
数据流摊在明面上。&lt;/p&gt;
&lt;p&gt;于是 Go 编译器在 AST 与机器码之间，插入了一层专门为优化而生的&lt;strong&gt;中间表示&lt;/strong&gt;（intermediate
representation, IR）。它先把 AST 下降（lower）为一种更接近指令、却仍与具体机器无关的形式，
再转换成本节的主角：&lt;strong&gt;静态单赋值形式&lt;/strong&gt;（static single assignment, SSA）。SSA 是当代优化编译器
近乎统一的中段表示，LLVM、HotSpot 的 C2、GCC 的 GIMPLE-SSA 都建在它之上。本节回答三个问题：
SSA 是什么，为什么它让优化变得直接，以及 Go 的 SSA 流水线如何把一个函数从「与机器无关」一路
打磨到「为某个架构生成的机器码」。&lt;/p&gt;
&lt;h2 id="1521-静态单赋值每个变量只赋值一次"&gt;15.2.1 静态单赋值：每个变量只赋值一次&lt;/h2&gt;
&lt;p&gt;SSA 的定义只有一句话：&lt;strong&gt;程序中每个变量恰好被赋值一次&lt;/strong&gt;。一旦某个名字被多次赋值，就给它编号，
拆成多个互不相同的版本。回到 &lt;code&gt;x = 1; x = x + 1&lt;/code&gt;，在 SSA 里它变成：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;x_1 = 1
x_2 = x_1 + 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;两次对 &lt;code&gt;x&lt;/code&gt; 的赋值成了 &lt;code&gt;x_1&lt;/code&gt; 与 &lt;code&gt;x_2&lt;/code&gt; 两个独立的、各自只被赋值一次的值。这一步看似只是机械改名，
带来的好处却是结构性的：当后面某处用到 &lt;code&gt;x_2&lt;/code&gt;，它的来源是&lt;strong&gt;唯一且显式&lt;/strong&gt;的，就是上面那条
&lt;code&gt;x_1 + 1&lt;/code&gt;，无需任何作用域查找或数据流推断。「定义」与「使用」之间于是连成一张明确的图（
use-def chain），值从哪来、被谁用，一望可知。常量传播、公共子表达式消除、死代码删除这些优化，
本质上都是在这张图上做的局部改写，SSA 把它们从「需要全局分析」降格成了「照着图改」。&lt;/p&gt;</description></item><item><title>15.3 优化器</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/optimize/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/optimize/</guid><description>&lt;h1 id="153-优化器"&gt;15.3 优化器&lt;/h1&gt;
&lt;p&gt;&lt;a href=".././ssa"&gt;15.2&lt;/a&gt; 把前端的语法树降到了 SSA 这层中间表示，并说明了 SSA 的「每个变量只赋值一次」
为何让优化遍写起来又准又快。这一节接着往下走：在这层表示上，编译器到底跑了哪些优化，又为什么
偏偏是这几样。&lt;/p&gt;
&lt;p&gt;读懂 Go 优化器，先要读懂它的取向。同样是把高级语言变成机器码，GCC 与 LLVM 愿意花上几秒甚至几十秒，
把一个函数反复揉搓，换取最后那几个百分点的运行性能。Go 走的是另一条路：它只做性价比最高的那一批
优化，把省下来的时间还给编译速度（&lt;a href="../../../part1overview/ch01intro/history"&gt;1.1&lt;/a&gt;）。这不是能力
不足，而是一道清醒的价值排序，本节最后会回到这条红线。我们先看 Go 愿意做的优化，再看 Go 1.21
引入的、把优化从「静态猜测」推向「数据驱动」的性能制导优化（PGO）。&lt;/p&gt;
&lt;h2 id="1531-内联一切优化的入口"&gt;15.3.1 内联：一切优化的入口&lt;/h2&gt;
&lt;p&gt;在 SSA 上做的诸多优化里，&lt;strong&gt;内联&lt;/strong&gt;（inlining）地位特殊。它本身只做一件朴素的事：把一个小函数的
函数体，直接展开到调用它的地方，省去一次函数调用的开销（建栈帧、传参、跳转、返回）。但它真正的
价值不在省这点开销，而在于它&lt;strong&gt;为其他优化创造了条件&lt;/strong&gt;。一次跨函数的调用，对优化器来说是一堵墙：
墙那边的代码长什么样、参数是不是常量、返回值会不会被用到，它一概不知。内联把这堵墙拆掉，调用点
两侧的代码合到一起，常量就能传播过去，分支就能被判定，死代码就能被消除。&lt;/p&gt;
&lt;p&gt;举一个最常见的例子。&lt;code&gt;sync.Once.Do&lt;/code&gt; 的快路径只是一次原子读，标准库把它写成一个独立的小方法：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; (o *Once) Do(f &lt;span style="color:#00f"&gt;func&lt;/span&gt;()) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;if&lt;/span&gt; atomic.LoadUint32(&amp;amp;o.done) == 0 {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; o.doSlow(f)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;如果不内联，每次 &lt;code&gt;once.Do(f)&lt;/code&gt; 都要付一次调用开销，仅仅为了读一个字段。内联之后，&lt;code&gt;atomic.LoadUint32&lt;/code&gt;
这一层包装也一并展开，整个快路径塌缩成几条指令，与手写一个内联的原子读没有区别。Go 标准库里大量
「一层薄包装」的设计，正是建立在「编译器会把它内联掉」这个假设之上。想知道某个调用到底有没有被
内联、为什么没有，可以用 &lt;code&gt;go build -gcflags=-m&lt;/code&gt;，编译器会逐条打印它的内联决策（&lt;code&gt;can inline&lt;/code&gt;、
&lt;code&gt;inlining call to&lt;/code&gt;，或是 &lt;code&gt;function too complex&lt;/code&gt; 这类拒绝理由）。&lt;/p&gt;</description></item><item><title>15.4 指针检查器</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/unsafe/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/unsafe/</guid><description>&lt;h1 id="154-指针检查器"&gt;15.4 指针检查器&lt;/h1&gt;
&lt;p&gt;Go 是一门内存安全的语言。在常规代码里，类型系统保证每个指针都指向它声明类型的合法对象，
垃圾回收（&lt;a href="../../part4memory/ch13gc"&gt;13&lt;/a&gt;）保证对象在仍被引用时不会被回收，运行时保证越界访问
被挡在边界检查里。这套保证不是免费的，它建立在「编译器始终知道每个值的类型与布局」之上。
可总有少数场景需要跳出这套体系：与 C 互操作（&lt;a href=".././cgo"&gt;15.6&lt;/a&gt;）要按 C 的内存布局解释一段字节，
对接操作系统的系统结构体要逐字节摆放，零拷贝地把 &lt;code&gt;[]byte&lt;/code&gt; 重解释成 &lt;code&gt;string&lt;/code&gt;
（&lt;a href="../../../part2lang/ch05data/slice"&gt;5.1&lt;/a&gt;）要让两个类型共享同一段底层内存。Go 为这些场景留了
一个逃生舱口：&lt;code&gt;unsafe&lt;/code&gt; 包。&lt;/p&gt;
&lt;p&gt;逃生舱口的代价是，一旦用它绕过类型系统，编译器与运行时原先提供的保证就部分失效，误用不再被
语言挡住，而要靠程序员自己遵守一组并不直观的规则。这一节先讲清 &lt;code&gt;unsafe.Pointer&lt;/code&gt; 的能力边界与
规范列出的合法模式，再讲清其中最隐蔽的一类陷阱（&lt;code&gt;uintptr&lt;/code&gt; 与垃圾回收的关系），最后讲编译器与
运行时如何用「指针检查器」（checkptr）把这类潜伏的误用变成当场报错。&lt;/p&gt;
&lt;h2 id="1541-unsafepointer绕过类型系统的四条特权"&gt;15.4.1 unsafe.Pointer：绕过类型系统的四条特权&lt;/h2&gt;
&lt;p&gt;普通指针 &lt;code&gt;*T&lt;/code&gt; 之间不能随意转换，类型系统不允许把 &lt;code&gt;*int&lt;/code&gt; 当作 &lt;code&gt;*float64&lt;/code&gt; 来读写。&lt;code&gt;unsafe.Pointer&lt;/code&gt;
是一种特殊指针，它在类型系统里开了一道口子，规范赋予它四条普通类型没有的特权：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任意类型的指针 &lt;code&gt;*T&lt;/code&gt; 都可以转换为 &lt;code&gt;unsafe.Pointer&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unsafe.Pointer&lt;/code&gt; 可以转换回任意类型的指针 &lt;code&gt;*T&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uintptr&lt;/code&gt; 可以转换为 &lt;code&gt;unsafe.Pointer&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unsafe.Pointer&lt;/code&gt; 可以转换为 &lt;code&gt;uintptr&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前两条合起来，意味着借道 &lt;code&gt;unsafe.Pointer&lt;/code&gt; 可以把任意 &lt;code&gt;*T1&lt;/code&gt; 转成任意 &lt;code&gt;*T2&lt;/code&gt;，从而以另一种类型解释
同一段内存，这正是类型系统本想禁止的事。后两条让指针与整数互转，从而能对地址做算术。规范因此
明确写道：&lt;code&gt;Pointer&lt;/code&gt; 允许程序破坏类型系统、对任意内存读写，使用应格外小心。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// unsafe 包对 Pointer 的定义（ArbitraryType 仅用于文档，表示任意类型）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;type&lt;/span&gt; ArbitraryType &lt;span style="color:#2b91af"&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;type&lt;/span&gt; Pointer *ArbitraryType
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;口子开得这么大，规范并不是说怎么用都行。它列出了若干「合法的转换模式」，承诺只有落在这些模式
里的用法才有定义，偏离则是未定义行为。&lt;code&gt;go vet&lt;/code&gt; 会检查代码是否落在这些模式内，没过 &lt;code&gt;go vet&lt;/code&gt; 的
&lt;code&gt;unsafe&lt;/code&gt; 代码不受任何保证。下面逐一过这些模式，它们覆盖了 &lt;code&gt;unsafe&lt;/code&gt; 几乎全部的正当用途。&lt;/p&gt;</description></item><item><title>15.5 逃逸分析</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/escape/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/escape/</guid><description>&lt;h1 id="155-逃逸分析"&gt;15.5 逃逸分析&lt;/h1&gt;
&lt;p&gt;Go 程序员从不手动决定一个变量放栈还是放堆，这件事由编译器的&lt;strong&gt;逃逸分析&lt;/strong&gt;（escape analysis）
自动完成。它是 Go 性能的隐形功臣：把尽可能多的对象留在栈上，能大幅减轻垃圾回收
（&lt;a href="../../part4memory/ch13gc"&gt;13 垃圾回收&lt;/a&gt;）的负担。这一节讲清它怎么判断、怎么实现、为何重要。&lt;/p&gt;
&lt;h2 id="1551-逃逸决定栈还是堆"&gt;15.5.1 逃逸：决定栈还是堆&lt;/h2&gt;
&lt;p&gt;核心问题：一个变量该分配在&lt;strong&gt;栈&lt;/strong&gt;上（随函数返回自动消失，零 GC 成本），还是&lt;strong&gt;堆&lt;/strong&gt;上
（生命周期不定，由 GC 管理）？判据是&lt;strong&gt;生命周期&lt;/strong&gt;：若一个变量的引用在函数返回后仍可能被用到，
它就不能放栈上（栈帧已随返回销毁），必须&lt;strong&gt;逃逸&lt;/strong&gt;到堆。逃逸分析就是静态地回答这个问题，
判断「这个变量的地址会不会跑出它所在函数的作用域」。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;go/cmd/compile/internal/escape&lt;/code&gt; 把这件事说成两条必须维持的不变量：（1）指向栈对象的指针
&lt;strong&gt;不能被存入堆&lt;/strong&gt;；（2）指向栈对象的指针&lt;strong&gt;不能活过该对象本身&lt;/strong&gt;，比如声明它的函数已经返回、
栈帧被销毁，或同一段栈空间在循环的不同迭代中被复用给了逻辑上不同的变量。只要一个变量的地址
有可能违反这两条，它就被判定逃逸，改为堆分配。&lt;/p&gt;
&lt;p&gt;最直接的观察手段是 &lt;code&gt;go build -gcflags=-m&lt;/code&gt;，它让编译器把每一处逃逸判断打印出来。把下面这段
喂给它（加 &lt;code&gt;-l&lt;/code&gt; 关掉内联，让输出聚焦在逃逸本身）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; ret() *&lt;span style="color:#2b91af"&gt;int&lt;/span&gt; { x := 42; &lt;span style="color:#00f"&gt;return&lt;/span&gt; &amp;amp;x } &lt;span style="color:#008000"&gt;// 返回局部变量的地址&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;$ go build -gcflags=&amp;#39;-m -l&amp;#39; demo.go
./demo.go:1:18: moved to heap: x
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;x&lt;/code&gt; 本是个普通局部变量，但它的地址被 &lt;code&gt;return&lt;/code&gt; 带出了函数，调用方拿到的指针必须在 &lt;code&gt;ret&lt;/code&gt; 返回
后依然有效，于是 &lt;code&gt;x&lt;/code&gt; 被「搬到堆上」（moved to heap）。这就是最典型的一类逃逸：&lt;code&gt;return &amp;amp;x&lt;/code&gt;。&lt;/p&gt;</description></item><item><title>15.6 cgo</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/cgo/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/cgo/</guid><description>&lt;h1 id="156-cgo"&gt;15.6 cgo&lt;/h1&gt;
&lt;p&gt;「cgo is not Go.」这是 Rob Pike 在一篇博客里给 cgo 下的判词。它道破了一件容易被忽略的事：
当你在 Go 源文件里 &lt;code&gt;import &amp;quot;C&amp;quot;&lt;/code&gt;，写下一行 &lt;code&gt;C.foo()&lt;/code&gt; 时，你已经踏出了 Go 这门语言为你
划定的世界，进入了另一个由 C 的 ABI、C 的栈、C 的内存模型构成的世界。cgo 是这两个世界之间的
桥，桥很有用，但过桥要付通行费，而且费用不低。&lt;/p&gt;
&lt;p&gt;这一节不逐行翻译 &lt;code&gt;runtime/cgocall.go&lt;/code&gt;，而是回答三个问题：为什么从 Go 调一个 C 函数会比调一个
Go 函数昂贵一两个数量级；运行时为这一次跨界究竟做了哪些事；以及由此衍生出的那条「Go 指针不可
被 C 长期持有」的规则从何而来。读完它，「cgo is not Go」这句话的分量就具体了。&lt;/p&gt;
&lt;h2 id="1561-两个世界的落差"&gt;15.6.1 两个世界的落差&lt;/h2&gt;
&lt;p&gt;要理解 cgo 的代价，先要看清桥的两端有多么不同。Go 与 C 在四件根本的事情上各行其是。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;调用约定不同。&lt;/strong&gt; Go 有自己的寄存器调用约定（&lt;a href="../../../part1overview/ch02asm/callconv"&gt;2.2&lt;/a&gt;），
参数与返回值的传递、栈帧的布局、哪些寄存器由调用方保存，都与 C 在该平台上的 ABI 不一致。
跨界调用必须先把参数按 C 的 ABI 重新摆放，把栈指针对齐到 C 要求的边界。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;栈不同。&lt;/strong&gt; Go 的 goroutine 栈是&lt;strong&gt;可增长&lt;/strong&gt;的（&lt;a href="../../part4memory/ch14stack"&gt;14&lt;/a&gt;），初始只有
几 KB，靠编译器在函数入口插入的栈检查在需要时拷贝、搬迁整条栈。C 函数对此一无所知，它假定自己
跑在一条固定的、足够大的系统栈上，绝不会被搬走。让 C 代码跑在一条随时可能被移动的 goroutine
小栈上是灾难性的，因此跨界前必须切换到 M 的系统栈 &lt;code&gt;g0&lt;/code&gt;，那是一条由操作系统分配、不会增长也
不会搬迁的栈。&lt;/p&gt;</description></item><item><title>15.7 过去、现在与未来</title><link>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/future/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part5toolchain/ch15compile/future/</guid><description>&lt;h1 id="157-过去现在与未来"&gt;15.7 过去、现在与未来&lt;/h1&gt;
&lt;p&gt;编译器是 Go 工具链里改动最频繁、却对用户最透明的部分。同一份源码一行不改，换一个新版本重新
编译，往往就更快、更小、更优,你甚至不知道中间发生了什么。这一节把镜头拉远，回看编译器自身
走过的路，再看它当下在做什么、接下来会往哪里走。贯穿始终的，是一条不变的价值排序:&lt;strong&gt;编译速度
优先，生成代码质量其次，而两者都要在「可工程化」的前提下取舍&lt;/strong&gt;
（&lt;a href="../../../part1overview/ch01intro/history"&gt;1.1&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;值得先说清楚的是，本节谈的「变」全部发生在引擎盖之下。Go 对语言与工具的兼容性承诺，意味着这些
重构不该惊动任何一行用户代码。下面要讲的两次大重写，正是在这个约束下完成的。&lt;/p&gt;
&lt;h2 id="1571-过去从-c-到-go从-plan-9-到-ssa"&gt;15.7.1 过去:从 C 到 Go，从 Plan 9 到 SSA&lt;/h2&gt;
&lt;p&gt;编译器自身经历了两次伤筋动骨的大变，方向却截然不同:一次换的是实现语言，一次换的是后端架构。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其一，实现语言:C → Go。&lt;/strong&gt; 最早（到 Go 1.4 为止）的 &lt;code&gt;gc&lt;/code&gt; 编译器是用 C 写的，沿用了 Plan 9
工具链的代码风格与构建方式。Go 1.5 完成了&lt;strong&gt;自举&lt;/strong&gt;(bootstrap):编译器被机器翻译成 Go，从此
是「用 Go 写的 Go 编译器」。这件事的细节见 &lt;a href="../../../part1overview/ch03life/bootstrap"&gt;3.3&lt;/a&gt;,
这里只强调它的意义。换语言不是为了赶时髦:C 版本无法利用 Go 自己的并发、内存安全与丰富的标准
库，也无法让 Go 社区用熟悉的语言去读、去改编译器。自举之后，编译器才真正成为「社区可维护的
Go 程序」，这为后续一切重构铺平了路。代价是引入了一个&lt;strong&gt;自举链&lt;/strong&gt;:要编译当前版本的 Go，先得有
一个能跑的旧版本 Go，工具链因此需要小心维护这条向后的依赖。&lt;/p&gt;
&lt;p&gt;顺带澄清一个常被混淆的命名:编译器叫 &lt;code&gt;gc&lt;/code&gt;，是 &amp;ldquo;Go compiler&amp;rdquo; 的缩写，与垃圾回收（大写 GC，
garbage collection）毫无关系。&lt;code&gt;cmd/compile/README&lt;/code&gt; 开篇就专门提醒了这一点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其二，后端架构:Plan 9 风格 → SSA。&lt;/strong&gt; 自举只是换了笔，没换骨架。Go 1.5/1.6 的后端仍大体
沿用 Plan 9 编译器的传统设计:基于一种较低层的指令表示直接做有限的优化与指派。这套后端能用，
但难以承载现代优化,它的中间表示不便于做数据流分析，加一个新优化往往要在多处特判，扩展性差。&lt;/p&gt;</description></item></channel></rss>