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