SegmentFault Golang 攻略最新的文章
2020-09-24T15:19:25+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
如何提升代码质量
https://segmentfault.com/a/1190000024571695
2020-09-24T15:19:25+08:00
2020-09-24T15:19:25+08:00
ronniesong
https://segmentfault.com/u/sxssxs
7
<h2>何谓代码质量?</h2><h3>代码是给人看的</h3><p><strong>1. 书写规范:遵照自己公司制定的编程语言书写规范。</strong> <br><strong>2. 易阅读。</strong><br><strong>3. 易修改。</strong> <br><strong>4. 易测试。</strong></p><h3>代码是给机器运行的</h3><p><strong>1. 安全</strong><br><strong>2. 快速</strong><br><strong>3. 稳定</strong></p><h2>代码质量的标准?</h2><h5>对于机器来说,标准是恒定的,但不可兼得。</h5><p>比如:</p><ul><li><p>锁机制:</p><ul><li>安全、慢</li></ul></li><li><p>指针:</p><ul><li>快、不稳定</li></ul></li><li><p>改内存地址:</p><ul><li>快、不安全</li></ul></li></ul><p>总的来说,这就像 CAP 理论一样,不同场景下的需求不一样,根据当下业务需求去做出取舍即可。</p><h5>对于人来说,标准是变化的,因为习惯不同、工期不同、目的不同。</h5><h2>易阅读</h2><h3>表意明确</h3><h4>名词要准确</h4><p>类、结构体、变量、常量等名词要能直观地描述这是个什么东西,一般 1-5 个单词组成为宜。</p><p>英文单词里没有官方缩写的就尽量不用缩写,像 result 就 6 个字母,也有人给缩写成 res、ret,temp 缩写成 tmp,更有甚者写 cnt,它到底是 count 还是 content 呢?除了歧义,完全没有任何好处。</p><h4>动词要精简</h4><p>方法名、函数名等动词要能保证只做一件事。一个方法不写太长的前提是它的功能本身就不多。</p><h4>形容词要归约</h4><p>属性、校验等形容词要归约为方法,业务逻辑关键点大多在判断上,预留扩展点会让阅读难度不随着加需求而快速增大。</p><h3>单词统一</h3><p>不要有歧义,因为人是有思维惯性的,比如同样的业务逻辑,有的写 add,有的写 append,有的写 insert,会严重影响阅读效率。</p><h3>描述业务</h3><p>有意义的名字要专注于描述业务,使读者通过阅读代码理解业务逻辑。不要在起名中掺杂数据结构。</p><h5>例如这么几个场景:列表(集合)、配置映射</h5><ul><li>good case:users(用户集合),articles(文章列表)、siteNameToSiteId(映射)</li><li>bad case:userSet、articleList、siteMap</li></ul><p>加上类型,并不会对理解业务逻辑有帮助,读者看到 list,map 这些关键词还会联想到数据结构中,很容易打断思维。</p><h3>避免赘述</h3><p>具有包含关系或从属关系的时候,不要重复,不要表达累赘的语境。</p><h3>关注作用域和生命周期</h3><p>当一个变量的作用域很窄,或生命周期很短的时候,可以用单字母命名,一般来讲,单字母意味着临时使用,读者在了解逻辑的时候可以不用关注这部分。</p><p>变量要接近使用的地方声明,不要开头声明一堆变量,隔几十行才使用。</p><h3>写有用的注释</h3><p>“好的代码是自描述的” 即读代码就和读文章一样。注释不应该用来解释代码逻辑,而应该是用来说明为什么这么写。</p><h4>写什么样的注释</h4><ol><li>公共的、全局的变量和常量:说明用在哪,提供给谁用。</li><li>函数,方法:说明函数功能是什么。</li><li>行注释:xx 产品在 x 年 x 月 x 日提出什么需求,做此修改。</li></ol><h4>注释不是用来删代码的!!!</h4><p>代码不用了就彻底删除,怕以后还有用就从 git 里找回来,如果一个函数 100 行,其中 50 行都被注释掉了,这种会很容易分散读者的注意力。</p><h2>易修改</h2><h3>一值一用</h3><p>不要把一个变量重复赋值使用。虽然类型一样,但这样做会让修改的人非常头疼,所谓牵一发而动全身。例如:</p><ul><li>bad case:</li></ul><p><code>result, err := a.Get()</code><br><code>result, err = b.Get()</code></p><ul><li>good case:</li></ul><p><code>resultA, err := a.Get()</code><br><code>resultB, err = b.Get()</code></p><h3>少写参数</h3><p>当你发现一个函数的功能需要传入七八个参数才能完成的时候,一定是函数干的事太多了,逻辑写太长了。需要适当拆分。</p><h3>正确使用逻辑运算符</h3><h5>&&、||、!这些逻辑运算符是用来做逻辑判断的,不是用来控制执行流程的。</h5><p>例如这样一段逻辑:</p><pre><code class="go">if (isA()) {
doB()
}</code></pre><p>不要写成 isA() && doB(),尽管结果是一样的。</p><h5>适当化简</h5><pre><code class="go">if ((condition1() && condition2()) || !condition1()) {
return true
}
return false</code></pre><p>取反后化简为:</p><pre><code class="go">if(condition1() && !condition2()) {
return false
}
return true</code></pre><h3>降低圈复杂度</h3><h4>圈复杂度的定义:<a href="https://link.segmentfault.com/?enc=GAIpP9OhJdKK8N%2BFnYEhRQ%3D%3D.ISK1uba2DGCYx4%2FGFZoCXMwXzvtlr9PbEDYwHdfrY3MxElThH2U6I80hbkgHJ4mFdic6cQa0t4a9JwwnfiQiXObxg70EbNFzzBbLBrkUWeA%3D" rel="nofollow">https://zh.wikipedia.org/wiki/循环复杂度</a></h4><h4>增加圈复杂度的关键词:</h4><p>if、else、while、for、case、||、&& 等</p><h4>圈复杂度的合格标准:</h4><p>大部分标准在 10-20 之间。<br>这也是一个平均值,不是要求每一个函数都在 20 以下。<br>个别超标,是可以接受的。</p><h4>如何降低</h4><p><strong>1. 提炼函数</strong><br><strong>2. 抽象配置,使用 map</strong><br><strong>3. 合并返回值相同的函数</strong></p><h3>多写函数少写变量</h3><h5>实现同样的功能,并不是代码越少越好。</h5><p>因为代码越少往往意味着耦合度越高,修改扩展起来会更麻烦,就是爽了自己,给别人留坑。</p><h5>但是,每一行代码都要有价值。</h5><p>如果说逻辑节点之间需要一个东西来充当桥梁,变量就是独木桥,函数像隧道。<br><del>防杠精:有人说要考虑性能开销啊,多写一个函数比一个栈内的变量开销大啊,我觉得业务代码不差这一星半点的,自行斟酌。</del></p><h2>易测试</h2><h3>TDD</h3><blockquote>测试驱动开发:写一个函数之前先考虑写出来之后能不能测试,好不好测试。</blockquote><h4>实现方式</h4><p><img src="https://user-gold-cdn.xitu.io/2019/2/20/1690b2bcdd04caa8?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="1" title="1"></p><h5>第一步:先写单元测试。不必关心如何实现函数功能。</h5><h5>第二步:写目标函数,以刚好能通过单元测试的逻辑代码为目的。</h5><h5>第三步:重构函数,合理命名,优化结构,抽象设计。</h5><h4>如此循环,保证每次改动代码都能完好地通过所有测试用例。</h4><p>这样做的目的是让错误尽早的暴露出来,在 10 行代码中解决 bug 要比在 100 行代码中解决 bug 更加容易和快速。</p><h4>理想与现实</h4><p>看起来 TDD 的理论和可操作性还不错,但实际开发中,如果真的严格按照此理论去开发。对开发效率是一个比较大的影响。<br>而且一旦形成惯性思维和盲从依赖,会降低对代码的灵感和熟练度。<br>测试用例跑过了就没问题了吗?不一定。因为测试用例也是人写的,隐藏的坑才最致命。<br>其实,当做到易阅读和易修改之后,易测试就是水到渠成的事情。</p><h3>IoC 模式</h3><p>将对象、接口、非固定值(如系统时间、随机数)等作为依赖注入,先构造条件,再执行函数。而不是由函数内部去构造。</p><h3>全局变量单一写入方</h3><p>当有两个以上的函数控制同一个全局变量的时候,会相互影响,即局部形成了一个状态机,使测试难度陡升。</p><h3>封装外部依赖</h3><p>有外部依赖的,尤其涉及 IO 通信的,要单独封装,哪怕只有几行代码也要封装成一个独立的函数,不要把对外部依赖的调用混合在自身的逻辑代码中。</p><h2>最后</h2><p>阅读 → 修改 → 测试</p><p>这是一个递进的关系,环环相扣,并且前一步做好了都能有利于后一步的完善。</p>
Go 1.13 errors 基本用法
https://segmentfault.com/a/1190000020398774
2019-09-16T19:12:07+08:00
2019-09-16T19:12:07+08:00
ronniesong
https://segmentfault.com/u/sxssxs
23
<blockquote>Go 最新版本 1.13 中新增了 errors 的一些特性,有助于我们更优雅的处理业务逻辑中报错的问题。<br>本文主要展示 <code>errors</code> 包中新增方法的用法。</blockquote>
<h2><code>核心思想:套娃</code></h2>
<p>啥意思呢?这玩意就像套娃一样,从上往下扒,拿走一个还有一个,再拿走一个,诶还有一个,如果你愿意,可以一直扒到最底下没有了为止。</p>
<h2>基本用法</h2>
<h3>1. 创建一个被包装的 error</h3>
<h4>方式一:fmt.Errorf</h4>
<p>使用 <code>%w</code> 参数返回一个被包装的 error</p>
<pre><code class="go">err1 := errors.New("new error")
err2 := fmt.Errorf("err2: [%w]", err1)
err3 := fmt.Errorf("err3: [%w]", err2)
fmt.Println(err3)</code></pre>
<pre><code class="shell">// output
err3: [err2: [new error]]</code></pre>
<p><code>err2</code> 就是一个合法的<strong>被包装的 error</strong>,同样地,<code>err3</code> 也是一个<strong>被包装的 error</strong>,如此可以一直套下去。</p>
<h4>方式二:自定义 struct</h4>
<pre><code class="go">type WarpError struct {
msg string
err error
}
func (e *WarpError) Error() string {
return e.msg
}
func (e *WrapError) Unwrap() error {
return e.err
}</code></pre>
<p>之前看过源码的同学可能已经知道了,这就是 <code>fmt/errors.go</code> 中关于 warp 的结构。<br>就,很简单。自定义一个实现了 <code>Unwrap</code> 方法的 struct 就可以了。</p>
<h3>2. 拆开一个被包装的 error</h3>
<h4>errors.Unwrap</h4>
<pre><code class="go">err1 := errors.New("new error")
err2 := fmt.Errorf("err2: [%w]", err1)
err3 := fmt.Errorf("err3: [%w]", err2)
fmt.Println(errors.Unwrap(err3))
fmt.Println(errors.Unwrap(errors.Unwrap(err3)))</code></pre>
<pre><code class="shell">// output
err2: [new error]
new error</code></pre>
<h3>3. 判断被包装的 error 是否是包含指定错误</h3>
<h4>errors.Is</h4>
<p><strong>当多层调用返回的错误被一次次地包装起来,我们在调用链上游拿到的错误如何判断是否是底层的某个错误呢?</strong><br><strong><code>它递归调用 Unwrap 并判断每一层的 err 是否相等,如果有任何一层 err 和传入的目标错误相等,则返回 true。</code></strong></p>
<pre><code class="go">err1 := errors.New("new error")
err2 := fmt.Errorf("err2: [%w]", err1)
err3 := fmt.Errorf("err3: [%w]", err2)
fmt.Println(errors.Is(err3, err2))
fmt.Println(errors.Is(err3, err1))</code></pre>
<pre><code class="shell">// output
true
true</code></pre>
<h3>4. 提取指定类型的错误</h3>
<h4>errors.As</h4>
<p>这个和上面的 <code>errors.Is</code> 大体上是一样的,区别在于 <code>Is</code> 是严格判断相等,即两个 <code>error</code> 是否相等。<br><strong>而 <code>As</code> 则是判断类型是否相同,并提取第一个符合目标类型的错误,用来统一处理某一类错误。</strong></p>
<pre><code class="go">type ErrorString struct {
s string
}
func (e *ErrorString) Error() string {
return e.s
}
var targetErr *ErrorString
err := fmt.Errorf("new error:[%w]", &ErrorString{s:"target err"})
fmt.Println(errors.As(err, &targetErr))</code></pre>
<pre><code class="shell">// output
true</code></pre>
<h2>扩展</h2>
<h5>
<code>Is</code> <code>As</code> 两个方法已经预留了口子,可以由自定义的 error struct 实现并覆盖调用。</h5>
<h6>源码也没什么可说的,太简单了,一眼就能看懂的。</h6>
Golang - 调度剖析【第三部分】
https://segmentfault.com/a/1190000017333717
2018-12-11T13:13:47+08:00
2018-12-11T13:13:47+08:00
ronniesong
https://segmentfault.com/u/sxssxs
28
<blockquote>本篇是调度剖析的第三部分,将重点关注<strong>并发</strong>特性。<br>回顾:<br><a href="https://segmentfault.com/a/1190000016038785">第一部分</a><br><a href="https://segmentfault.com/a/1190000016611742">第二部分</a>
</blockquote>
<h2>简介</h2>
<p>首先,在我平时遇到问题的时候,特别是如果它是一个新问题,我一开始并不会考虑使用并发的设计去解决它。我会先实现顺序执行的逻辑,并确保它能正常工作。然后在可读性和技术关键点都 Review 之后,我才会开始思考并发执行的实用性和可行性。有的时候,并发执行是一个很好的选择,有时则不一定。</p>
<p>在本系列的第一部分中,我解释了<strong>系统调度</strong>的机制和语义,如果你打算编写多线程代码,我认为这些机制和语义对于实现正确的逻辑是很重要的。在第二部分中,我解释了<strong>Go 调度</strong>的语义,我认为它能帮助你理解如何在 Go 中编写高质量的并发程序。在这篇文章中,我会把<strong>系统调度</strong>和<strong>Go 调度</strong>的机制和语义结合在一起,以便更深入地理解什么才是并发以及它的本质。</p>
<h2>什么是并发</h2>
<p>并发意味着<code>乱序</code>执行。<strong>拿一组原来是顺序执行的指令,而后找到一种方法,使这些指令乱序执行,但仍然产生相同的结果。</strong>那么,顺序执行还是乱序执行?根本在于,针对我们目前考虑的问题,使用并发必须是有收益的!确切来说,是并发带来的性能提升要大于它带来的复杂性成本。当然有些场景,代码逻辑就已经约束了我们不能执行乱序,这样使用并发也就没有了意义。</p>
<h3>并发与并行</h3>
<p>理解<code>并发</code>与<code>并行</code>的不同也非常重要。<code>并行</code>意味着同时执行两个或更多指令,简单来说,只有多个CPU核心之间才叫<code>并行</code>。在 Go 中,至少要有两个操作系统硬件线程并至少有两个 Goroutine 时才能实现并行,每个 Goroutine 在一个单独的系统线程上执行指令。</p>
<h4>如图:</h4>
<p><img src="/img/bVbkROC?w=2586&h=792" alt="图片描述" title="图片描述"><br>我们看到有两个逻辑处理器<code>P</code>,每个逻辑处理器都挂载在一个系统线程<code>M</code>上,而每个<code>M</code>适配到计算机上的一个CPU处理器<code>Core</code>。<br>其中,有两个 Goroutine <code>G1</code> 和 <code>G2</code> 在<code>并行</code>执行,因为它们同时在各自的系统硬件线程上执行指令。<br>再看,在每一个逻辑处理器中,都有三个 Goroutine <code>G2 G3 G5</code> 或 <code>G1 G4 G6</code> 轮流共享各自的系统线程。看起来就像这三个 Goroutine 在同时运行着,没有特定顺序地执行它们的指令,并在系统线程上共享时间。<br>那么这就会发生<strong>竞争</strong>,有时候如果只在一个物理核心上实现并发则实际上会降低吞吐量。还有有意思的是,有时候即便利用上了并行的并发,也不会给你带来想象中更大的性能提升。</p>
<h2>工作负载</h2>
<p>我们怎么判断在什么时候并发会更有意义呢?我们就从了解当前执行逻辑的工作负载类型开始。在考虑并发时,有两种类型的工作负载是很重要的。</p>
<h3>两种类型</h3>
<p><strong>CPU-Bound:</strong>这是一种不会导致 Goroutine 主动切换上下文到等待状态的类型。它会一直不停地进行计算。比如说,计算 π 到第 N 位的 Goroutine 就是 CPU-Bound 的。</p>
<p><strong>IO-Bound:</strong>与上面相反,这种类型会导致 Goroutine 自然地进入到等待状态。它包括请求通过网络访问资源,或使用系统调用进入操作系统,或等待事件的发生。比如说,需要读取文件的 Goroutine 就是 IO-Bound。我把同步事件(互斥,原子),会导致 Goroutine 等待的情况也包含在此类。</p>
<p>在 <strong>CPU-Bound</strong> 中,我们需要利用并行。因为单个系统线程处理多个 Goroutine 的效率不高。而使用比系统线程更多的 Goroutine 也会拖慢执行速度,因为在系统线程上切换 Goroutine 是有时间成本的。上下文切换会导致发生<code>STW(Stop The World)</code>,意思是在切换期间当前工作指令都不会被执行。</p>
<p>在 <strong>IO-Bound</strong> 中,并行则不是必须的了。单个系统线程可以高效地处理多个 Goroutine,是因为Goroutine 在执行这类指令时会自然地进入和退出等待状态。使用比系统线程更多的 Goroutine 可以加快执行速度,因为此时在系统线程上切换 Goroutine 的延迟成本并不会产生<code>STW</code>事件。进入到IO阻塞时,CPU就闲下来了,那么我们可以使不同的 Goroutine 有效地复用相同的线程,不让系统线程闲置。</p>
<p>我们如何评估一个系统线程匹配多少 Gorountine 是最合适的呢?如果 Goroutine 少了,则会无法充分利用硬件;如果 Goroutine 多了,则会导致上下文切换延迟。这是一个值得考虑的问题,但此时暂不深究。</p>
<p>现在,更重要的是要通过仔细推敲代码来帮助我们准确识别什么情况需要并发,什么情况不能用并发,以及是否需要并行。</p>
<h2>加法</h2>
<p>我们不需要复杂的代码来展示和理解这些语义。先来看看下面这个名为<code>add</code>的函数:</p>
<pre><code class="go">1 func add(numbers []int) int {
2 var v int
3 for _, n := range numbers {
4 v += n
5 }
6 return v
7 }</code></pre>
<p>在第 1 行,声明了一个名为<code>add</code>的函数,它接收一个整型切片并返回切片中所有元素的和。它从第 2 行开始,声明了一个<code>v</code>变量来保存总和。然后第 3 行,线性地遍历切片,并且每个数字被加到<code>v</code>中。最后在第 6 行,函数将最终的总和返回给调用者。</p>
<p>问题:<code>add</code>函数是否适合并发执行?从大体上来说答案是适合的。可以将输入切片分解,然后同时处理它们。最后将每个小切片的执行结果相加,就可以得到和顺序执行相同的最终结果。</p>
<p>与此同时,引申出另外一个问题:应该分成多少个小切片来处理是性能最佳的呢?要回答此问题,我们必须知道它的工作负载类型。<br><code>add</code>函数正在执行 <strong>CPU-Bound</strong> 工作负载,因为实现算法正在执行纯数学运算,并且它不会导致 Goroutine 进入等待状态。这意味着每个系统线程使用一个 Goroutine 就可以获得不错的吞吐量。</p>
<h3>并发版本</h3>
<p>下面来看一下并发版本如何实现,声明一个 <code>addConcurrent</code> 函数。代码量相比顺序版本增加了很多。</p>
<pre><code class="go">1 func addConcurrent(goroutines int, numbers []int) int {
2 var v int64
3 totalNumbers := len(numbers)
4 lastGoroutine := goroutines - 1
5 stride := totalNumbers / goroutines
6
7 var wg sync.WaitGroup
8 wg.Add(goroutines)
9
10 for g := 0; g < goroutines; g++ {
11 go func(g int) {
12 start := g * stride
13 end := start + stride
14 if g == lastGoroutine {
15 end = totalNumbers
16 }
17
18 var lv int
19 for _, n := range numbers[start:end] {
20 lv += n
21 }
22
23 atomic.AddInt64(&v, int64(lv))
24 wg.Done()
25 }(g)
26 }
27
28 wg.Wait()
29
30 return int(v)
31 }</code></pre>
<p><strong>第 5 行:</strong>计算每个 Goroutine 的子切片大小。使用输入切片总数除以 Goroutine 的数量得到。<br><strong>第 10 行:</strong>创建一定数量的 Goroutine 执行子任务<br><strong>第 14-16 行:</strong>子切片剩下的所有元素都放到最后一个 Goroutine 执行,可能比前几个 Goroutine 处理的数据要多。<br><strong>第 23 行:</strong>将子结果追加到最终结果中。</p>
<p>然而,并发版本肯定比顺序版本更复杂,但和增加的复杂性相比,性能有提升吗?值得这么做吗?让我们用事实来说话,下面运行基准测试。</p>
<h3>基准测试</h3>
<p>下面的基准测试,我使用了1000万个数字的切片,并关闭了GC。分别有顺序版本<code>add</code>函数和并发版本<code>addConcurrent</code>函数。</p>
<pre><code class="go">func BenchmarkSequential(b *testing.B) {
for i := 0; i < b.N; i++ {
add(numbers)
}
}
func BenchmarkConcurrent(b *testing.B) {
for i := 0; i < b.N; i++ {
addConcurrent(runtime.NumCPU(), numbers)
}
}</code></pre>
<h4>无并行</h4>
<p>以下是所有 Goroutine 只有一个硬件线程可用的结果。顺序版本使用 <strong>1 Goroutine</strong>,并发版本在我的机器上使用<code>runtime.NumCPU</code>或 <strong>8 Goroutines</strong>。在这种情况下,并发版本实际正跑在没有并行的机制上。</p>
<pre><code class="go">10 Million Numbers using 8 goroutines with 1 core
2.9 GHz Intel 4 Core i7
Concurrency WITHOUT Parallelism
-----------------------------------------------------------------------------
$ GOGC=off go test -cpu 1 -run none -bench . -benchtime 3s
goos: darwin
goarch: amd64
pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/cpu-bound
BenchmarkSequential 1000 5720764 ns/op : ~10% Faster
BenchmarkConcurrent 1000 6387344 ns/op
BenchmarkSequentialAgain 1000 5614666 ns/op : ~13% Faster
BenchmarkConcurrentAgain 1000 6482612 ns/op</code></pre>
<p>结果表明:当只有一个系统线程可用于所有 Goroutine 时,顺序版本比并发快约10%到13%。这和我们之前的理论预期相符,主要就是因为并发版本在单核上的上下文切换和 Goroutine 管理调度的开销。</p>
<h4>有并行</h4>
<p>以下是每个 Goroutine 都有单独可用的系统线程的结果。顺序版本使用 <strong>1 Goroutine</strong>,并发版本在我的机器上使用<code>runtime.NumCPU</code>或 <strong>8 Goroutines</strong>。在这种情况下,并发版本利用上了并行机制。</p>
<pre><code class="go">10 Million Numbers using 8 goroutines with 8 cores
2.9 GHz Intel 4 Core i7
Concurrency WITH Parallelism
-----------------------------------------------------------------------------
$ GOGC=off go test -cpu 8 -run none -bench . -benchtime 3s
goos: darwin
goarch: amd64
pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/cpu-bound
BenchmarkSequential-8 1000 5910799 ns/op
BenchmarkConcurrent-8 2000 3362643 ns/op : ~43% Faster
BenchmarkSequentialAgain-8 1000 5933444 ns/op
BenchmarkConcurrentAgain-8 2000 3477253 ns/op : ~41% Faster</code></pre>
<p>结果表明:当为每个 Goroutine 提供单独的系统线程时,并发版本比顺序版本快大约41%到43%。这才也和预期一致,所有 Goroutine 现都在并行运行着,意味着他们真的在同时执行。</p>
<h2>排序</h2>
<p>另外,我们也要知道并非所有的 <strong>CPU-Bound</strong> 都适合并发。当切分输入或合并结果的代价非常高时,就不太合适。下面展示一个冒泡排序算法来说明此场景。</p>
<h3>顺序版本</h3>
<pre><code class="go">01 package main
02
03 import "fmt"
04
05 func bubbleSort(numbers []int) {
06 n := len(numbers)
07 for i := 0; i < n; i++ {
08 if !sweep(numbers, i) {
09 return
10 }
11 }
12 }
13
14 func sweep(numbers []int, currentPass int) bool {
15 var idx int
16 idxNext := idx + 1
17 n := len(numbers)
18 var swap bool
19
20 for idxNext < (n - currentPass) {
21 a := numbers[idx]
22 b := numbers[idxNext]
23 if a > b {
24 numbers[idx] = b
25 numbers[idxNext] = a
26 swap = true
27 }
28 idx++
29 idxNext = idx + 1
30 }
31 return swap
32 }
33
34 func main() {
35 org := []int{1, 3, 2, 4, 8, 6, 7, 2, 3, 0}
36 fmt.Println(org)
37
38 bubbleSort(org)
39 fmt.Println(org)
40 }</code></pre>
<p>这种排序算法会扫描每次在交换值时传递的切片。在对所有内容进行排序之前,可能需要多次遍历切片。</p>
<p>那么问题:<code>bubbleSort</code>函数是否适用并发?我相信答案是否定的。原始切片可以分解为较小的,并且可以同时对它们排序。但是!在并发执行完之后,没有一个有效的手段将子结果的切片排序合并。下面我们来看并发版本是如何实现的。</p>
<h3>并发版本</h3>
<pre><code class="go">01 func bubbleSortConcurrent(goroutines int, numbers []int) {
02 totalNumbers := len(numbers)
03 lastGoroutine := goroutines - 1
04 stride := totalNumbers / goroutines
05
06 var wg sync.WaitGroup
07 wg.Add(goroutines)
08
09 for g := 0; g < goroutines; g++ {
10 go func(g int) {
11 start := g * stride
12 end := start + stride
13 if g == lastGoroutine {
14 end = totalNumbers
15 }
16
17 bubbleSort(numbers[start:end])
18 wg.Done()
19 }(g)
20 }
21
22 wg.Wait()
23
24 // Ugh, we have to sort the entire list again.
25 bubbleSort(numbers)
26 }</code></pre>
<p><code>bubbleSortConcurrent</code>它使用多个 Goroutine 同时对输入的一部分进行排序。我们直接来看结果:</p>
<pre><code class="go">Before:
25 51 15 57 87 10 10 85 90 32 98 53
91 82 84 97 67 37 71 94 26 2 81 79
66 70 93 86 19 81 52 75 85 10 87 49
After:
10 10 15 25 32 51 53 57 85 87 90 98
2 26 37 67 71 79 81 82 84 91 94 97
10 19 49 52 66 70 75 81 85 86 87 93</code></pre>
<p>由于冒泡排序的本质是依次扫描,第 25 行对 <code>bubbleSort</code> 的调用将掩盖使用并发解决问题带来的潜在收益。结论是:在冒泡排序中,使用并发不会带来性能提升。</p>
<h2>读取文件</h2>
<p>前面已经举了两个 <strong>CPU-Bound</strong> 的例子,下面我们来看 <strong>IO-Bound</strong>。</p>
<h3>顺序版本</h3>
<pre><code class="go">01 func find(topic string, docs []string) int {
02 var found int
03 for _, doc := range docs {
04 items, err := read(doc)
05 if err != nil {
06 continue
07 }
08 for _, item := range items {
09 if strings.Contains(item.Description, topic) {
10 found++
11 }
12 }
13 }
14 return found
15 }</code></pre>
<p><strong>第 2 行:</strong>声明了一个名为 <code>found</code> 的变量,用于保存在给定文档中找到指定主题的次数。<br><strong>第 3-4 行:</strong>迭代文档,并使用<code>read</code>函数读取每个文档。<br><strong>第 8-11 行:</strong>使用 <code>strings.Contains</code> 函数检查文档中是否包含指定主题。如果包含,则<code>found</code>加1。</p>
<p>然后来看一下<code>read</code>是如何实现的。</p>
<pre><code class="go">01 func read(doc string) ([]item, error) {
02 time.Sleep(time.Millisecond) // 模拟阻塞的读
03 var d document
04 if err := xml.Unmarshal([]byte(file), &d); err != nil {
05 return nil, err
06 }
07 return d.Channel.Items, nil
08 }</code></pre>
<p>此功能以 <code>time.Sleep</code> 开始,持续1毫秒。此调用用于模拟在我们执行实际系统调用以从磁盘读取文档时可能产生的延迟。这种延迟的一致性对于准确测量<code>find</code>顺序版本和并发版本的性能差距非常重要。<br>然后在第 03-07 行,将存储在全局变量文件中的模拟 <code>xml</code> 文档反序列化为<code>struct</code>值。最后,将<code>Items</code>返回给调用者。</p>
<h3>并发版本</h3>
<pre><code class="go">01 func findConcurrent(goroutines int, topic string, docs []string) int {
02 var found int64
03
04 ch := make(chan string, len(docs))
05 for _, doc := range docs {
06 ch <- doc
07 }
08 close(ch)
09
10 var wg sync.WaitGroup
11 wg.Add(goroutines)
12
13 for g := 0; g < goroutines; g++ {
14 go func() {
15 var lFound int64
16 for doc := range ch {
17 items, err := read(doc)
18 if err != nil {
19 continue
20 }
21 for _, item := range items {
22 if strings.Contains(item.Description, topic) {
23 lFound++
24 }
25 }
26 }
27 atomic.AddInt64(&found, lFound)
28 wg.Done()
29 }()
30 }
31
32 wg.Wait()
33
34 return int(found)
35 }</code></pre>
<p><strong>第 4-7 行:</strong>创建一个<code>channel</code>并写入所有要处理的文档。<br><strong>第 8 行:</strong>关闭这个<code>channel</code>,这样当读取完所有文档后就会直接退出循环。<br><strong>第 16-26 行:</strong>每个 Goroutine 都从同一个<code>channel</code>接收文档,<code>read</code> 并 <code>strings.Contains</code> 逻辑和顺序的版本一致。<br><strong>第 27 行:</strong>将各个 Goroutine 计数加在一起作为最终计数。</p>
<h3>基准测试</h3>
<p>同样的,我们再次运行基准测试来验证我们的结论。</p>
<pre><code class="go">func BenchmarkSequential(b *testing.B) {
for i := 0; i < b.N; i++ {
find("test", docs)
}
}
func BenchmarkConcurrent(b *testing.B) {
for i := 0; i < b.N; i++ {
findConcurrent(runtime.NumCPU(), "test", docs)
}
}</code></pre>
<h4>无并行</h4>
<pre><code class="go">10 Thousand Documents using 8 goroutines with 1 core
2.9 GHz Intel 4 Core i7
Concurrency WITHOUT Parallelism
-----------------------------------------------------------------------------
$ GOGC=off go test -cpu 1 -run none -bench . -benchtime 3s
goos: darwin
goarch: amd64
pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/io-bound
BenchmarkSequential 3 1483458120 ns/op
BenchmarkConcurrent 20 188941855 ns/op : ~87% Faster
BenchmarkSequentialAgain 2 1502682536 ns/op
BenchmarkConcurrentAgain 20 184037843 ns/op : ~88% Faster</code></pre>
<p>当只有一个系统线程时,并发版本比顺序版本快大约87%到88%。与预期一致,因为所有 Goroutine 都有效地共享单个系统线程。</p>
<h4>有并行</h4>
<pre><code class="go">10 Thousand Documents using 8 goroutines with 8 core
2.9 GHz Intel 4 Core i7
Concurrency WITH Parallelism
-----------------------------------------------------------------------------
$ GOGC=off go test -run none -bench . -benchtime 3s
goos: darwin
goarch: amd64
pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/io-bound
BenchmarkSequential-8 3 1490947198 ns/op
BenchmarkConcurrent-8 20 187382200 ns/op : ~88% Faster
BenchmarkSequentialAgain-8 3 1416126029 ns/op
BenchmarkConcurrentAgain-8 20 185965460 ns/op : ~87% Faster</code></pre>
<p>有意思的来了,使用额外的系统线程提供并行能力,实际代码性能却没有提升。也印证了开头的说法。</p>
<h2>结语</h2>
<p>我们可以清楚地看到,使用 <strong>IO-Bound</strong> 并不需要并行来获得性能上的巨大提升。这与我们在 <strong>CPU-Bound</strong> 中看到的结果相反。当涉及像冒泡排序这样的算法时,并发的使用会增加复杂性而没有任何实际的性能优势。<br>所以,我们在考虑解决方案时,首先要确定它是否适合并发,而不是盲目认为使用更多的 Goroutine 就一定会提升性能。</p>
Go 语言编译器的 "//go:" 详解
https://segmentfault.com/a/1190000016743220
2018-10-19T19:09:33+08:00
2018-10-19T19:09:33+08:00
ronniesong
https://segmentfault.com/u/sxssxs
39
<h2>前言</h2>
<h3>C 语言的 #include</h3>
<p>一上来不太好说明白 Go 语言里 <code>//go:</code> 是什么,我们先来看下非常简单,也是几乎每个写代码的人都知道的东西:C 语言的 <code>#include</code>。<br>我猜,大部分人第一行代码都是 <code>#include</code> 吧。完整的就是<code>#include <stdio.h></code>。意思很简单,引入一个 <code>stdio.h</code>。谁引入?答案是<strong>编译器</strong>。那么,<code>#</code> 字符的作用就是给 <strong>编译器</strong> 一个 <strong>指示</strong>,让编译器知道接下来要做什么。</p>
<h3>编译指示</h3>
<p>在计算机编程中,<code>编译指示(pragma)</code>是一种语言结构,它指示编译器应该如何处理其输入。<code>指示</code>不是编程语言语法的一部分,因编译器而异。</p>
<blockquote>这里 <a href="https://link.segmentfault.com/?enc=L8xJhuycC98tZgybMh8rNw%3D%3D.EUaeRw6zh3DHAnzOzWn%2F3nuVxKh30cYIE94U8yySk8DJoUAYt00XYWXTOakAc9lLU1x7x%2FktUp7PK%2FSeAuFbGA%3D%3D" rel="nofollow">Wiki</a> 详细介绍了它,值得你看一下。</blockquote>
<h3>Go 语言的编译指示</h3>
<blockquote>官方文档 <a href="https://link.segmentfault.com/?enc=zT56aMmiCtiryzMlI5zkMw%3D%3D.wcoVhziWzskQ1fzl%2BF%2BkMDuFrSSKlyiBO3fWd6GFGYzd8hd9BybuXJADPcSCu7XdtfdrtVonNsXkoFlGFGQ6kw%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=ZejmMwbNfKx4vskEDIlI3g%3D%3D.Gmhu3%2FKTixNbtJbpzBL87TNzuDrxpZO0KHJOaJ9NjTvk1q0gdk2hJYS5JeB8D86DsTYffobdoImazn9IK%2FIyyA%3D%3D" rel="nofollow">https://golang.org/cmd/compil...</a>
</blockquote>
<p>形如 <code>//go:</code> 就是 Go 语言编译指示的实现方式。相信看过 Go SDK 的同学对此并不陌生,经常能在代码函数声明的上一行看到这样的写法。<br>有同学会问了,<code>//</code> 这不是注释吗?确实,它是以注释的形式存在的。</p>
<blockquote>
<a href="https://link.segmentfault.com/?enc=dk5ZXzZ8ZDawHXKiCbEdwA%3D%3D.hqA8Nh84iWYYDuiIMr%2BIHF62fFjCizx45ybXHnVzFbviwFZLQgnQkXH8iNL%2B333ljFsH%2ByWsDAOU8RyDPablHOQQmvk8s24a3oaj2tJnxIA%3D" rel="nofollow">编译器源码</a> 这里可以看到全部的指示,但是要注意,<code>//go:</code> 是连续的,<code>//</code> 和 <code>go</code> 之间并没有空格。</blockquote>
<h2>常用指示详解</h2>
<h3><code>//go:noinline</code></h3>
<p><strong><code>noinline</code> 顾名思义,不要内联。</strong></p>
<h4>Inline 内联</h4>
<blockquote>
<code>Inline</code>,是在编译期间发生的,将函数调用调用处替换为被调用函数主体的一种编译器优化手段。Wiki:<a href="https://link.segmentfault.com/?enc=cZLBQ7y%2Bowsoe7TdjIy1pg%3D%3D.0KrtjoWrXvuo5GSGQ62EYVI55F9fDNeJ6LY1W%2BYQRea%2B7aXhjoeivDvC1cae0Sik" rel="nofollow">Inline 定义</a>
</blockquote>
<h5>使用 <code>Inline</code> 有一些优势,同样也有一些问题。</h5>
<h6>优势:</h6>
<ul>
<li>减少函数调用的开销,提高执行速度。</li>
<li>复制后的更大函数体为其他编译优化带来可能性,如 <a href="https://link.segmentfault.com/?enc=8ih%2BZsP5jVArazQy23%2F67w%3D%3D.MbBoqpXLf%2BwaPqgplA0rmIwKu6qT2eP3VSX6eJAGKkIB8MrDQDaP1T%2Bkd4NH%2FpjaQt%2FiamSfQu5STv8ZsTmjEw%3D%3D" rel="nofollow">过程间优化</a>
</li>
<li>消除分支,并改善空间局部性和指令顺序性,同样可以提高性能。</li>
</ul>
<h6>问题:</h6>
<ul>
<li>代码复制带来的空间增长。</li>
<li>如果有大量重复代码,反而会降低缓存命中率,尤其对 CPU 缓存是致命的。</li>
</ul>
<p>所以,在实际使用中,对于是否使用内联,要谨慎考虑,并做好平衡,以使它发挥最大的作用。<br>简单来说,对于短小而且工作较少的函数,使用内联是有效益的。</p>
<h4>内联的例子</h4>
<pre><code class="go">func appendStr(word string) string {
return "new " + word
}</code></pre>
<p>执行 <code>GOOS=linux GOARCH=386 go tool compile -S main.go > main.S</code> <br>我截取有区别的部分展出它编译后的样子:</p>
<pre><code> 0x0015 00021 (main.go:4) LEAL ""..autotmp_3+28(SP), AX
0x0019 00025 (main.go:4) PCDATA $2, $0
0x0019 00025 (main.go:4) MOVL AX, (SP)
0x001c 00028 (main.go:4) PCDATA $2, $1
0x001c 00028 (main.go:4) LEAL go.string."new "(SB), AX
0x0022 00034 (main.go:4) PCDATA $2, $0
0x0022 00034 (main.go:4) MOVL AX, 4(SP)
0x0026 00038 (main.go:4) MOVL $4, 8(SP)
0x002e 00046 (main.go:4) PCDATA $2, $1
0x002e 00046 (main.go:4) LEAL go.string."hello"(SB), AX
0x0034 00052 (main.go:4) PCDATA $2, $0
0x0034 00052 (main.go:4) MOVL AX, 12(SP)
0x0038 00056 (main.go:4) MOVL $5, 16(SP)
0x0040 00064 (main.go:4) CALL runtime.concatstring2(SB)</code></pre>
<p>可以看到,它并没有调用 <code>appendStr</code> 函数,而是直接把这个函数体的功能内联了。</p>
<p>那么话说回来,如果你不想被内联,怎么办呢?此时就该使用 <code>go//:noinline</code> 了,像下面这样写:</p>
<pre><code class="go">//go:noinline
func appendStr(word string) string {
return "new " + word
}</code></pre>
<p>编译后是:</p>
<pre><code> 0x0015 00021 (main.go:4) LEAL go.string."hello"(SB), AX
0x001b 00027 (main.go:4) PCDATA $2, $0
0x001b 00027 (main.go:4) MOVL AX, (SP)
0x001e 00030 (main.go:4) MOVL $5, 4(SP)
0x0026 00038 (main.go:4) CALL "".appendStr(SB)</code></pre>
<p>此时编译器就不会做内联,而是直接调用 <code>appendStr</code> 函数。</p>
<h3><code>//go:nosplit</code></h3>
<p><code>nosplit</code> 的作用是:<strong>跳过栈溢出检测。</strong></p>
<h4>栈溢出是什么?</h4>
<p>正是因为一个 Goroutine 的起始栈大小是有限制的,且比较小的,才可以做到支持并发很多 Goroutine,并高效调度。<br><a href="https://link.segmentfault.com/?enc=Y0CcFAugNVajTWSUurR%2F3Q%3D%3D.3p%2BQfdDnWus8n8YZ8Fyl8KfLuYijouBAAsRwoPxtW25bTcFJdpD9G3tu4o8TEoEUrBv4S2ILu9IIC9j0fy9yUvmFVLkAy6wffgzTMULUKLI%3D" rel="nofollow">stack.go</a> 源码中可以看到,<code>_StackMin</code> 是 2048 字节,也就是 2k,它不是一成不变的,当不够用时,它会动态地增长。<br>那么,必然有一个检测的机制,来保证可以及时地知道栈不够用了,然后再去增长。<br>回到话题,<code>nosplit</code> 就是将这个跳过这个机制。</p>
<h4>优劣</h4>
<p>显然地,不执行栈溢出检查,可以提高性能,但同时也有可能发生 <code>stack overflow</code> 而导致编译失败。</p>
<h3><code>//go:noescape</code></h3>
<p><code>noescape</code> 的作用是:<strong>禁止逃逸,而且它必须指示一个只有声明没有主体的函数。</strong></p>
<h4>逃逸是什么?</h4>
<p>Go 相比 C、C++ 是内存更为安全的语言,主要一个点就体现在它可以自动地将超出自身生命周期的变量,从函数栈转移到堆中,逃逸就是指这种行为。</p>
<blockquote>请参考我之前的文章,<a href="https://segmentfault.com/a/1190000016354799#articleHeader2">逃逸分析</a>。</blockquote>
<h4>优劣</h4>
<p>最显而易见的好处是,GC 压力变小了。<br>因为它已经告诉编译器,下面的函数无论如何都不会逃逸,那么当函数返回时,其中的资源也会一并都被销毁。<br>不过,这么做代表会绕过编译器的逃逸检查,一旦进入运行时,就有可能导致严重的错误及后果。</p>
<h3><code>//go:norace</code></h3>
<p><code>norace</code> 的作用是:<strong>跳过竞态检测</strong><br>我们知道,在多线程程序中,难免会出现数据竞争,正常情况下,当编译器检测到有数据竞争,就会给出提示。如:</p>
<pre><code class="go">var sum int
func main() {
go add()
go add()
}
func add() {
sum++
}</code></pre>
<p>执行 <code>go run -race main.go</code> 利用 <code>-race</code> 来使编译器报告数据竞争问题。你会看到:</p>
<pre><code>==================
WARNING: DATA RACE
Read at 0x00000112f470 by goroutine 6:
main.add()
/Users/sxs/Documents/go/src/test/main.go:15 +0x3a
Previous write at 0x00000112f470 by goroutine 5:
main.add()
/Users/sxs/Documents/go/src/test/main.go:15 +0x56
Goroutine 6 (running) created at:
main.main()
/Users/sxs/Documents/go/src/test/main.go:11 +0x5a
Goroutine 5 (finished) created at:
main.main()
/Users/sxs/Documents/go/src/test/main.go:10 +0x42
==================
Found 1 data race(s)</code></pre>
<p>说明两个 goroutine 执行的 <code>add()</code> 在竞争。</p>
<h4>优劣</h4>
<p>使用 <code>norace</code> 除了减少编译时间,我想不到有其他的优点了。但缺点却很明显,那就是数据竞争会导致程序的不确定性。</p>
<h2>总结</h2>
<p><strong> 我认为绝大多数情况下,无需在编程时使用 <code>//go:</code> Go 语言的编译器指示,除非你确认你的程序的性能瓶颈在编译器上,否则你都应该先去关心其他更可能出现瓶颈的事情。</strong></p>
<h4>参考</h4>
<ul><li><a href="https://link.segmentfault.com/?enc=l6t268N4Jw3C0QZu3Y%2BG5A%3D%3D.lFv9JwlHYxyCS%2Bm4oTtPnRMRO6cSdBJts41%2FZwALqeikFvnUhHkAiBZrKKPKS6Y9SJNYyYAOo%2BRG0qA07WQcwg%3D%3D" rel="nofollow">https://dave.cheney.net/2018/...</a></li></ul>
Go Defer 高级实践
https://segmentfault.com/a/1190000016666245
2018-10-12T17:53:59+08:00
2018-10-12T17:53:59+08:00
ronniesong
https://segmentfault.com/u/sxssxs
18
<blockquote>
<code>defer</code> 是一个用起来非常简单的特性。<br>它的实现原理也不复杂。<br>本文主要介绍这个特性在实际项目中的利弊以及建议。</blockquote>
<h2>为什么要用 defer</h2>
<p>任何一个特性都有它的设计初衷,主要是被用来解决什么问题的,任何一个特性也都有它合适和不合适出现的地方,我们清楚地了解并正确合理地使用,是非常重要的。</p>
<h3>优势</h3>
<h4>提高安全性、健壮性</h4>
<h4>让代码更优雅</h4>
<h3>劣势</h3>
<h4>可读性、可维护性</h4>
<p><em>(注意:用 <code>defer</code> 当然肯定比不用有一定的性能开销,但我们可以忽略,因为影响确实很小。 换句话说,绝大部分情况下,考虑是否使用 <code>defer</code> 时,性能开销不应该是首先考虑的因素。但是!如果你的代码是微秒级别的,那还是要评估后再使用)</em></p>
<h2>defer 怎么用</h2>
<ol>
<li><a href="https://link.segmentfault.com/?enc=OlN1FFUQJ%2BJJMfJtGmqDGA%3D%3D.HvKneqnh0S7Q0iTPxKcqQMKAG9EuvHwKzQ%2F33BfqFySzA376lumxV79zqapI3%2FyD" rel="nofollow">官方文档,告诉你 defer 的基本用法</a></li>
<li>
<p>几乎所有其他文章里说 <code>defer</code> 如何如何有坑,<code>defer</code> 需要注意什么等等。。都是官方文档上讲到的三点,在此就不赘述了。下面我分成三部分,建议使用、中立和不建议。</p>
<ul>
<li>
<strong>建议使用</strong> 是官方 src 里都在用的,而且也是 <code>defer</code> 的设计初衷。</li>
<li>
<strong>中立</strong> 是工程实践中总结出来,平衡了代码优雅和可读性、可维护性后的结果。</li>
<li>
<strong>不建议</strong> 是弊大于利,得不偿失的用法,主要影响的就是降低可读性,可维护性。</li>
</ul>
</li>
</ol>
<h3>建议使用</h3>
<h4>Recover</h4>
<pre><code class="go">defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered", r)
}
}()</code></pre>
<h4>资源回收</h4>
<p>各种资源的使用,如果在用完之后不 close,就会造成资源的泄露,可能会严重影响程序运行,甚至造成程序死掉</p>
<h5>网络 I/O</h5>
<pre><code class="go">c, err := Dial("udp", raddr)
if err != nil {
return err
}
defer c.Close()</code></pre>
<h5>文件 I/O</h5>
<pre><code class="go">f, err := os.Open(filename)
if err != nil {
return
}
defer f.Close()</code></pre>
<h5>channel 关闭</h5>
<pre><code class="go">fd, _ := os.Open("txt")
errc := make(chan error, 1)
// 主动关闭,减小 GC 压力。
defer close(errc)
var buf [1]byte
n, err := fd.Read(buf[:1])
if n == 0 || err != nil {
errc <- fmt.Errorf("read byte = %d, err = %v", n, err)
}</code></pre>
<h4>避免死锁</h4>
<pre><code class="go">type A struct {
t int
sync.Mutex
}
func main() {
a := new(A)
for i := 0; i < 2000; i++ {
go a.incr()
}
time.Sleep(500 * time.Millisecond) // 此处用 sleep 简单模拟等待同步,实际这样写不严谨,可用 waitGroup、channel 等
fmt.Println(a.t)
}
func (a *A) incr() {
a.Lock()
defer a.Unlock()
// 模拟 ... 一堆逻辑
// 然后 ... 中间有好几个 return 出口
// 如果我们不用 defer,就要在每个 return 都写上 a.Unlock,不然就可能会造成死锁
a.t++
}</code></pre>
<h3>中立</h3>
<h4>函数返回时的打点</h4>
<h5>记日志</h5>
<p>这里可能稍微有一些复杂,我稍微讲一下<br><strong>第一步</strong>,会先执行 log("do") 调用 log 函数传入参数 “do”<br><strong>第二步</strong>,log 函数执行函数体即 <code>start := time.Now() fmt.Printf("enter %s\n", msg)</code>两行,然后给调用方 do 函数返回一个 func()<br><strong>第三步</strong>,这个 func() 被放到 defer 里,等到 do 函数返回时才会执行。</p>
<pre><code class="go">func main() {
do()
}
func do() {
defer log("do")()
// ... 一些逻辑
time.Sleep(1 * time.Second)
}
func log(msg string) func() {
start := time.Now()
fmt.Printf("enter %s\n", msg)
return func() { fmt.Printf("exit %s (%s)", msg, time.Since(start)) }
}</code></pre>
<h4>错误处理</h4>
<p>因为 go 自带的比较恶心的 err != nil 的判断,业务逻辑中可能会有大量的这种代码,而我们又要对出错进行一个统一的处理的时候,可以用。</p>
<h5>数据库事务的回滚操作</h5>
<pre><code class="go">tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
// ... 中间会发生多个数据库操作 ...
// 提交,那么在提交之前发生的任何错误,返回时都可利用之前注册的 defer 进行回滚
tx.Commit()</code></pre>
<h3>不建议</h3>
<p>不建议的用法就不给出代码示例了,怕你看了错误的代码示例反而记住了,就不好了。下面只说不建议的用法场景。</p>
<h4>不要直接在循环中使用 defer</h4>
<p>defer 是后定义的先执行,和栈类似。<br>如果在循环中调用 <code>defer</code>,可能会导致堆积了很多 defer,在循环结束后才会执行。<br>这中间如果有任何一个 <code>defer</code> 失败了怎么办?<br>多个 <code>defer</code> 执行的内容有没有依赖关系和冲突?<br>所以,除非万不得已,不要给自己增加复杂度。<br><strong>不这么用就好了。</strong></p>
<h4>不要在 defer 中传入体积很大的参数</h4>
<p>因为编译器的很多优化对它都不起作用,所以尽量不要传入体积很大的参数,当然我觉得也应该没有多少人会传入一堆参数来用 <code>defer</code> 的。</p>
<h4>不要用 receiver 调用 defer</h4>
<p>因为 <code>receiver</code> 是当做第一个参数传给调用函数的,也是值传递,除非你能时刻明确注意 <code>receiver</code> 是否是一个指针,否则最好不要用 <code>defer</code>,不然可能无法得到你想要的结果。</p>
<h3>未完待续。。。</h3>
<h2>defer 原理简述</h2>
<p><code>defer</code> 源码实现的位置:<a href="https://link.segmentfault.com/?enc=XBh90tKdqnmMqc%2F%2FXdVhPw%3D%3D.SHaZ50wV5KFxhk%2BuRPN1WSqwjDKvPSeSASM6vy4rWiTiEqJztTZZWnI4P5PIhy%2FI39vzw%2BfR4L0OnkASDd7ABhbTN%2BKcAgYqpNxOQ%2BVsOtw%3D" rel="nofollow">runtime/panic.go</a></p>
<p>看到这知道我在建议使用中第一个就写 <code>recover</code> 是为什么了吧。<br>这个特性最初的目的就是给 <code>recover</code> 用的。</p>
<p>编译器会把 <code>defer</code> 关键字转化为对此函数的调用:</p>
<pre><code class="go">func deferproc(siz int32, fn *funcval)</code></pre>
<p>然后当原函数 <code>return</code> 时,会调用:</p>
<pre><code class="go">func deferreturn(arg0 uintptr)</code></pre>
<p>看,它只有一个参数,就是 <code>arg0</code>,也就是 代码中 <code>defer</code> 后面跟着的函数。明显的,只有函数体本身会延迟执行,函数的参数在注册 defer 之前就已经执行完了。</p>
<h2>结语</h2>
<p>老老实实写代码,不要总想玩魔法。</p>
Golang - 调度剖析【第二部分】
https://segmentfault.com/a/1190000016611742
2018-10-08T16:43:16+08:00
2018-10-08T16:43:16+08:00
ronniesong
https://segmentfault.com/u/sxssxs
39
<blockquote>回顾本系列的<a href="https://segmentfault.com/a/1190000016038785">第一部分</a>,重点讲述了操作系统调度器的各个方面,这些知识对于理解和分析 Go 调度器的语义是非常重要的。<br>在本文中,我将从语义层面解析 Go 调度器是如何工作的,并重点介绍其高级特性。<br>Go 调度器是一个非常复杂的系统,我们不会过分关注一些细节,而是侧重于剖析它的设计模型和工作方式。<br>我们通过学习它的优点以便够做出更好的工程决策。</blockquote>
<h2>开始</h2>
<p><strong>当 Go 程序启动时,它会为主机上标识的每个虚拟核心提供一个逻辑处理器(P)</strong>。如果处理器每个物理核心可以提供多个硬件线程(超线程),那么每个硬件线程都将作为虚拟核心呈现给 Go 程序。为了更好地理解这一点,下面实验都基于如下配置的 MacBook Pro 的系统。</p>
<p><img src="/img/bVbhQkZ?w=484&h=294" alt="图片描述" title="图片描述"></p>
<p>可以看到它是一个 4 核 8 线程的处理器。这将告诉 Go 程序有 8 个虚拟核心可用于并行执行系统线程。</p>
<p>用下面的程序来验证一下:</p>
<pre><code class="go">package main
import (
"fmt"
"runtime"
)
func main() {
// NumCPU 返回当前可用的逻辑处理核心的数量
fmt.Println(runtime.NumCPU())
}</code></pre>
<p>当我运行该程序时,<code>NumCPU()</code> 函数调用的结果将是 <code>8</code> 。意味着在我的机器上运行的任何 Go 程序都将被赋予 8 个 <strong><code>P</code></strong>。</p>
<p><strong>每个 <code>P</code> 都被分配一个系统线程 <code>M</code> </strong> 。M 代表机器(machine),它仍然是由操作系统管理的,操作系统负责将线程放在一个核心上执行。这意味着当在我的机器上运行 Go 程序时,有 8 个线程可以执行我的工作,每个线程单独连接到一个 P。</p>
<p><strong>每个 Go 程序都有一个初始 <code>G</code></strong>。G 代表 Go 协程(Goroutine),它是 Go 程序的执行路径。Goroutine 本质上是一个 <a href="https://link.segmentfault.com/?enc=WfNkKdPF6XfcemWEUzhRNg%3D%3D.JtUen0vtkok6DuMTnBrELfnRO%2F0ZFutBGlFWVXzYsDknNegWXJLYTaFuA3f78%2BKw" rel="nofollow">Coroutine</a>,但因为是 Go 语言,所以把字母 “C” 换成了 “G”,我们得到了这个词。你可以将 Goroutines 看作是应用程序级别的线程,它在许多方面与系统线程都相似。正如系统线程在物理核心上进行上下文切换一样,Goroutines 在 <strong><code>M</code></strong> 上进行上下文切换。</p>
<p>最后一个重点是运行队列。Go 调度器中有两个不同的运行队列:<code>全局运行队列(GRQ)</code>和<code>本地运行队列(LRQ)</code>。<strong>每个 <code>P</code> 都有一个LRQ</strong>,用于管理分配给在<strong><code>P</code></strong>的上下文中执行的 Goroutines,这些 Goroutine 轮流被<strong><em>和<code>P</code>绑定的<code>M</code></em></strong>进行上下文切换。GRQ 适用于尚未分配给<strong><code>P</code></strong>的 Goroutines。其中有一个过程是将 Goroutines 从 GRQ 转移到 LRQ,我们将在稍后讨论。</p>
<p>下面图示展示了它们之间的关系:</p>
<p><img src="/img/bVbhQvV?w=1620&h=758" alt="图片描述" title="图片描述"></p>
<h2>协作式调度器</h2>
<p>正如我们在第一篇文章中所讨论的,OS 调度器是一个抢占式调度器。从本质上看,这意味着你无法预测调度程序在任何给定时间将执行的操作。由内核做决定,一切都是不确定的。在操作系统之上运行的应用程序无法通过调度控制内核内部发生的事情,除非它们利用像 <a href="https://link.segmentfault.com/?enc=dbqK2AJ%2FK1Z52JJISx3nbA%3D%3D.0Wc5NHeF5ldwB2oHy2DJyXXYDK0bk5Gv3cI8k0j5zLF%2BgBPQ1T%2FbPnNvcFtn84kP" rel="nofollow">atomic</a> 指令 和 <a href="https://link.segmentfault.com/?enc=sezNu5xEU4w%2Fc%2Bzhpd7u1A%3D%3D.NDhPSgnHQ8YGFXk6FQCDTIiulz%2FzCgbcjl3N0mZTq%2BuLotytntzLDR2%2FNAbzfdekF%2FK7SmBSPW3lL3aFhnNEDQ%3D%3D" rel="nofollow">mutex</a> 调用之类的同步原语。</p>
<p>Go 调度器是 Go 运行时的一部分,Go 运行时内置在应用程序中。这意味着 Go 调度器在内核之上的用户空间中运行。Go 调度器的当前实现不是抢占式调度器,而是协作式调度器。作为一个协作的调度器,意味着调度器需要明确定义用户空间事件,这些事件发生在代码中的安全点,以做出调度决策。</p>
<p>Go 协作式调度器的优点在于它看起来和感觉上都是抢占式的。你无法预测 Go 调度器将会执行的操作。这是因为这个协作调度器的决策不掌握在开发人员手中,而是在 Go 运行时。将 Go 调度器视为抢占式调度器是非常重要的,并且由于调度程序是非确定性的,因此这并不是一件容易的事。</p>
<h2>Goroutine 状态</h2>
<p>就像线程一样,Goroutines 有相同的三个高级状态。它们标识了 Go 调度器在任何给定的 Goroutine 中所起的作用。Goroutine 可以处于三种状态之一:<strong><code>Waiting</code>(等待状态)</strong>、<strong><code>Runnable</code>(可运行状态)</strong>或<strong><code>Executing</code>(运行中状态)</strong>。</p>
<p><strong><code>Waiting</code>:</strong>这意味着 Goroutine 已停止并等待一些事情以继续。这可能是因为等待操作系统(系统调用)或同步调用(原子和互斥操作)等原因。这些类型的延迟是性能下降的根本原因。</p>
<p><strong><code>Runnable </code>:</strong>这意味着 Goroutine 需要<strong><code>M</code></strong>上的时间片,来执行它的指令。如果同一时间有很多 Goroutines 在竞争时间片,它们都必须等待更长时间才能得到时间片,而且每个 Goroutine 获得的时间片都缩短了。这种类型的调度延迟也可能导致性能下降。</p>
<p><strong><code>Executing </code>:</strong>这意味着 Goroutine 已经被放置在<strong><code>M</code></strong>上并且正在执行它的指令。与应用程序相关的工作正在完成。这是每个人都想要的。</p>
<h2>上下文切换</h2>
<p>Go 调度器需要有明确定义的用户空间事件,这些事件发生在要切换上下文的代码中的安全点上。这些事件和安全点在函数调用中表现出来。函数调用对于 Go 调度器的运行状况是至关重要的。现在(使用 Go 1.11或更低版本),如果你运行任何未进行函数调用的<a href="https://link.segmentfault.com/?enc=JPirdagsN3Pb1h0rJ72NHA%3D%3D.ZHCUOd0ud%2Frbtr%2F5anLuTrFlCj%2BAGKR4zZuA0iVdCPzneT41GHWNiOCfMJ%2F8k%2B57" rel="nofollow">紧凑循环</a>,你会导致调度器和垃圾回收有延迟。让函数调用在合理的时间范围内发生是至关重要的。</p>
<p><em>注意:在 Go 1.12 版本中有一个提议被接受了,它可以使 Go 调度器使用非协作抢占技术,以允许抢占紧密循环。</em></p>
<p>在 Go 程序中有四类事件,它们允许调度器做出调度决策:</p>
<ul>
<li>使用关键字 <code>go</code>
</li>
<li>垃圾回收</li>
<li>系统调用</li>
<li>同步和<a href="https://link.segmentfault.com/?enc=%2FUxno6qU6lSb7oJAfQy79A%3D%3D.sC2KnuaR6a3GmphrrQZS5rephbVyRCfLHLdNbVGzTROALxgVx%2F0x4il6Wqk4HHBzIEIivsEVYypggZT6kw05VuuVyOspu9YKcgQp8FylcHs8MpgjMSDVeUY%2F2leci1WM" rel="nofollow">编配</a>
</li>
</ul>
<h3>使用关键字 <code>go</code>
</h3>
<p>关键字 <code>go</code> 是用来创建 Goroutines 的。一旦创建了新的 Goroutine,它就为调度器做出调度决策提供了机会。</p>
<h3>垃圾回收</h3>
<p>由于 GC 使用自己的 Goroutine 运行,所以这些 Goroutine 需要在 M 上运行的时间片。这会导致 GC 产生大量的调度混乱。但是,调度程序非常聪明地了解 Goroutine 正在做什么,它将智能地做出一些决策。</p>
<h3>系统调用</h3>
<p>如果 Goroutine 进行系统调用,那么会导致这个 Goroutine 阻塞当前<strong><code>M</code></strong>,有时调度器能够将 Goroutine 从<strong><code>M</code></strong>换出并将新的 Goroutine 换入。然而,有时需要新的<strong><code>M</code></strong>继续执行在<strong><code>P</code></strong>中排队的 Goroutines。这是如何工作的将在下一节中更详细地解释。</p>
<h3>同步和编配</h3>
<p>如果原子、互斥量或通道操作调用将导致 Goroutine 阻塞,调度器可以将之切换到一个新的 Goroutine 去运行。一旦 Goroutine 可以再次运行,它就可以重新排队,并最终在<strong><code>M</code></strong>上切换回来。</p>
<h2>异步系统调用</h2>
<p>当你的操作系统能够异步处理系统调用时,可以使用称为网络轮询器的东西来更有效地处理系统调用。这是通过在这些操作系统中使用 kqueue(MacOS),epoll(Linux)或 iocp(Windows)来实现的。</p>
<p>基于网络的系统调用可以由我们今天使用的许多操作系统异步处理。这就是为什么我管它叫网络轮询器,因为它的主要用途是处理网络操作。通过使用网络轮询器进行网络系统调用,调度器可以防止 Goroutine 在进行这些系统调用时阻塞<strong><code>M</code></strong>。这可以让<strong><code>M</code></strong>执行<strong><code>P</code></strong>的 LRQ 中其他的 Goroutines,而不需要创建新的<strong><code>M</code></strong>。有助于减少操作系统上的调度负载。</p>
<p>下图展示它的工作原理:<strong><code>G1</code></strong>正在<strong><code>M</code></strong>上执行,还有 3 个 Goroutine 在 LRQ 上等待执行。网络轮询器空闲着,什么都没干。</p>
<p><img src="/img/bVbhQOl?w=1700&h=890" alt="图片描述" title="图片描述"></p>
<p>接下来,情况发生了变化:<strong><code>G1</code></strong>想要进行网络系统调用,因此它被移动到网络轮询器并且处理异步网络系统调用。然后,<strong><code>M</code></strong>可以从 LRQ 执行另外的 Goroutine。此时,<strong><code>G2</code></strong>就被上下文切换到<strong><code>M</code></strong>上了。</p>
<p><img src="/img/bVbhQOK?w=1700&h=898" alt="图片描述" title="图片描述"></p>
<p>最后:异步网络系统调用由网络轮询器完成,<strong><code>G1</code></strong>被移回到<strong><code>P</code></strong>的 LRQ 中。一旦<strong><code>G1</code></strong>可以在<strong><code>M</code></strong>上进行上下文切换,它负责的 Go 相关代码就可以再次执行。这里的最大优势是,执行网络系统调用不需要额外的<strong><code>M</code></strong>。网络轮询器使用系统线程,它时刻处理一个有效的事件循环。</p>
<p><img src="/img/bVbhQPs?w=1700&h=902" alt="图片描述" title="图片描述"></p>
<h2>同步系统调用</h2>
<p>如果 Goroutine 要执行同步的系统调用,会发生什么?在这种情况下,网络轮询器无法使用,而进行系统调用的 Goroutine 将阻塞当前<strong><code>M</code></strong>。这是不幸的,但是没有办法防止这种情况发生。需要同步进行的系统调用的一个例子是基于文件的系统调用。如果你正在使用 CGO,则可能还有其他情况,调用 C 函数也会阻塞<strong><code>M</code></strong>。</p>
<p><em>注意:Windows 操作系统确实能够异步进行基于文件的系统调用。从技术上讲,在 Windows 上运行时,可以使用网络轮询器。</em></p>
<p>让我们来看看同步系统调用(如文件I/O)会导致<strong><code>M</code></strong>阻塞的情况:<strong><code>G1</code></strong>将进行同步系统调用以阻塞<strong><code>M1</code></strong>。</p>
<p><img src="/img/bVbhQWw?w=1322&h=784" alt="图片描述" title="图片描述"></p>
<p>调度器介入后:识别出<strong><code>G1</code></strong>已导致<strong><code>M1</code></strong>阻塞,此时,调度器将<strong><code>M1</code></strong>与<strong><code>P</code></strong>分离,同时也将<strong><code>G1</code></strong>带走。然后调度器引入新的<strong><code>M2</code></strong>来服务<strong><code>P</code></strong>。此时,可以从 LRQ 中选择<strong><code>G2</code></strong>并在<strong><code>M2</code></strong>上进行上下文切换。</p>
<p><img src="/img/bVbhQX7?w=1666&h=784" alt="图片描述" title="图片描述"></p>
<p>阻塞的系统调用完成后:<strong><code>G1</code></strong>可以移回 LRQ 并再次由<strong><code>P</code></strong>执行。如果这种情况需要再次发生,M1将被放在旁边以备将来使用。</p>
<p><img src="/img/bVbhQZA?w=1680&h=792" alt="图片描述" title="图片描述"></p>
<h2>任务窃取(负载均衡思想)</h2>
<p>调度器的另一个方面是它是一个任务窃取的调度器。这有助于在一些领域保持高效率的调度。首先,你最不希望的事情是<strong><code>M</code></strong>进入等待状态,因为一旦发生这种情况,操作系统就会将<strong><code>M</code></strong>从内核切换出去。这意味着<strong><code>P</code></strong>无法完成任何工作,即使有 Goroutine 处于可运行状态也不行,直到一个<strong><code>M</code></strong>被上下文切换回核心。任务窃取还有助于平衡所有<strong><code>P</code></strong>的 Goroutines 数量,这样工作就能更好地分配和更有效地完成。</p>
<p>看下面的一个例子:这是一个多线程的 Go 程序,其中有两个<strong><code>P</code></strong>,每个<strong><code>P</code></strong>都服务着四个 Goroutine,另在 GRQ 中还有一个单独的 Goroutine。如果其中一个<strong><code>P</code></strong>的所有 Goroutines 很快就执行完了会发生什么?</p>
<p><img src="/img/bVbhQ1g?w=1720&h=844" alt="图片描述" title="图片描述"></p>
<p>如你所见:<strong><code>P1</code></strong>的 Goroutines 都执行完了。但是还有 Goroutines 处于可运行状态,在 GRQ 中有,在<strong><code>P2</code></strong>的 LRQ 中也有。<br>这时<strong><code>P1</code></strong>就需要窃取任务。</p>
<p><img src="/img/bVbhQ3S?w=1646&h=844" alt="图片描述" title="图片描述"></p>
<p>窃取的规则在这里定义了:<a href="https://link.segmentfault.com/?enc=aGhHU4Chbsd3oLHdtR00FQ%3D%3D.oMOfW3%2B53FWHgjzU2tmOqgV5ExMEsJG%2FZWz6tajny4ASJ8SZLNix%2BU2EKF0ggeYL" rel="nofollow">https://golang.org/src/runtim...</a></p>
<pre><code class="go">if gp == nil {
// 1/61的概率检查一下全局可运行队列,以确保公平。否则,两个 goroutine 就可以通过不断地相互替换来完全占据本地运行队列。
if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
lock(&sched.lock)
gp = globrunqget(_g_.m.p.ptr(), 1)
unlock(&sched.lock)
}
}
if gp == nil {
gp, inheritTime = runqget(_g_.m.p.ptr())
if gp != nil && _g_.m.spinning {
throw("schedule: spinning with local work")
}
}
if gp == nil {
gp, inheritTime = findrunnable()
}</code></pre>
<p>根据规则,<strong><code>P1</code></strong>将窃取<strong><code>P2</code></strong>中一半的 Goroutines,窃取完成后的样子如下:</p>
<p><img src="/img/bVbhQ96?w=1702&h=844" alt="图片描述" title="图片描述"></p>
<p>我们再来看一种情况,如果<strong><code>P2</code></strong>完成了对所有 Goroutine 的服务,而<strong><code>P1</code></strong>的 LRQ 也什么都没有,会发生什么?</p>
<p><img src="/img/bVbhRaR?w=1716&h=786" alt="图片描述" title="图片描述"></p>
<p><strong><code>P2</code></strong>完成了所有任务,现在需要窃取一些。首先,它将查看<strong><code>P1</code></strong>的 LRQ,但找不到任何 Goroutines。接下来,它将查看 GRQ。<br>在那里它会找到<strong><code>G9</code></strong>,<strong><code>P2</code></strong>从 GRQ 手中抢走了<strong><code>G9</code></strong>并开始执行。以上任务窃取的好处在于它使<strong><code>M</code></strong>不会闲着。在窃取任务时,<strong><code>M</code></strong>是自旋的。这种自旋还有其他的好处,可以参考 <a href="https://link.segmentfault.com/?enc=Pk6J1wQFXhAaZrl9NO24Pg%3D%3D.HcmlTbPI0mNJ6Q6tO%2BKAKvfPp8AzWCRXi6RiCsTMPME%3D" rel="nofollow">work-stealing</a> 。</p>
<p><img src="/img/bVbhRbn?w=1720&h=782" alt="图片描述" title="图片描述"></p>
<h2>实例</h2>
<p>有了相应的机制和语义,我将向你展示如何将所有这些结合在一起,以便 Go 调度程序能够执行更多的工作。设想一个用 C 编写的多线程应用程序,其中程序管理两个操作系统线程,这两个线程相互传递消息。</p>
<p>下面有两个线程,线程 <code>T1</code> 在内核 <code>C1</code> 上进行上下文切换,并且正在运行中,这允许 <code>T1</code> 将其消息发送到 <code>T2</code>。</p>
<p><img src="/img/bVbhRnY?w=920&h=850" alt="图片描述" title="图片描述"></p>
<p>当 <code>T1</code> 发送完消息,它需要等待响应。这将导致 <code>T1</code> 从 <code>C1</code> 上下文换出并进入等待状态。<br>当 <code>T2</code> 收到有关该消息的通知,它就会进入可运行状态。<br>现在操作系统可以执行上下文切换并让 <code>T2</code> 在一个核心上执行,而这个核心恰好是 <code>C2</code>。接下来,<code>T2</code> 处理消息并将新消息发送回 <code>T1</code>。</p>
<p><img src="/img/bVbhRpQ?w=966&h=852" alt="图片描述" title="图片描述"></p>
<p>然后,<code>T2</code> 的消息被 <code>T1</code> 接收,线程上下文切换再次发生。现在,<code>T2</code> 从运行中状态切换到等待状态,<code>T1</code> 从等待状态切换到可运行状态,再被执行变为运行中状态,这允许它处理并发回新消息。</p>
<p>所有这些上下文切换和状态更改都需要时间来执行,这限制了工作的完成速度。<br>由于每个上下文切换可能会产生 50 纳秒的延迟,并且理想情况下硬件每纳秒执行 12 条指令,因此你会看到有差不多 600 条指令,在上下文切换期间被停滞掉了。并且由于这些线程也在不同的内核之间跳跃,因 <a href="https://link.segmentfault.com/?enc=9iijMs8qkU5uM%2FfiWYXnLg%3D%3D.g9n722%2B2S8X2qiqb9qvjTnqUfgWqkJK2EvKFtzWQ80Ds5ZXlK%2F6o%2B0EBMptr94De" rel="nofollow">cache-line</a> 未命中引起额外延迟的可能性也很高。</p>
<p><img src="/img/bVbhRqP?w=968&h=842" alt="图片描述" title="图片描述"></p>
<p>下面我们还用这个例子,来看看 Goroutine 和 Go 调度器是怎么工作的:<br>有两个goroutine,它们彼此协调,来回传递消息。<strong><code>G1</code></strong>在<strong><code>M1</code></strong>上进行上下文切换,而<strong><code>M1</code></strong>恰好运行在<strong><code>C1</code></strong>上,这允许<strong><code>G1</code></strong>执行它的工作。即向<strong><code>G2</code></strong>发送消息。</p>
<p><img src="/img/bVbhRuh?w=922&h=842" alt="图片描述" title="图片描述"></p>
<p><strong><code>G1</code></strong>发送完消息后,需要等待响应。<strong><code>M1</code></strong>就会把<strong><code>G1</code></strong>换出并使之进入等待状态。一旦<strong><code>G2</code></strong>得到消息,它就进入可运行状态。现在 Go 调度器可以执行上下文切换,让<strong><code>G2</code></strong>在<strong><code>M1</code></strong>上执行,<strong><code>M1</code></strong>仍然在<strong><code>C1</code></strong>上运行。接下来,<strong><code>G2</code></strong>处理消息并将新消息发送回<strong><code>G1</code></strong>。</p>
<p><img src="/img/bVbhRvw?w=930&h=838" alt="图片描述" title="图片描述"></p>
<p>当<strong><code>G2</code></strong>发送的消息被<strong><code>G1</code></strong>接收时,上下文切换再次发生。现在<strong><code>G2</code></strong>从运行中状态切换到等待状态,<strong><code>G1</code></strong>从等待状态切换到可运行状态,最后返回到执行状态,这允许它处理和发送一个新的消息。</p>
<p><img src="/img/bVbhRwd?w=958&h=840" alt="图片描述" title="图片描述"></p>
<p>表面上看起来没有什么不同。无论使用线程还是 Goroutine,都会发生相同的上下文切换和状态变更。然而,使用线程和 Goroutine 之间有一个主要区别:<br><strong>在使用 Goroutine 的情况下,会复用同一个系统线程和核心。这意味着,从操作系统的角度来看,操作系统线程永远不会进入等待状态。因此,在使用系统线程时的开销在使用 Goroutine 时就不存在了。</strong></p>
<p>基本上,Go 已经在操作系统级别将 <code>IO-Bound</code> 类型的工作转换为 <code>CPU-Bound</code> 类型。由于所有的上下文切换都是在应用程序级别进行的,所以在使用线程时,每个上下文切换(平均)不至于迟滞 600 条指令。该调度程序还有助于提高 <code>cache-line</code> 效率和 <code>NUMA</code>。在 Go 中,随着时间的推移,可以完成更多的工作,因为 Go 调度器尝试使用更少的线程,在每个线程上做更多的工作,这有助于减少操作系统和硬件的负载。</p>
<h2>结论</h2>
<p>Go 调度器在设计中考虑到复杂的操作系统和硬件的工作方式,真是令人惊叹。在操作系统级别将 <code>IO-Bound</code> 类型的工作转换为 <code>CPU-Bound</code> 类型的能力是我们在利用更多 CPU 的过程中获得巨大成功的地方。这就是为什么不需要比虚拟核心更多的操作系统线程的原因。你可以合理地期望每个虚拟内核只有一个系统线程来完成所有工作(CPU和IO)。对于网络应用程序和其他不会阻塞操作系统线程的系统调用的应用程序来说,这样做是可能的。</p>
<p>作为一个开发人员,你当然需要知道程序在运行中做了什么。你不可能创建无限数量的 Goroutine ,并期待惊人的性能。越少越好,但是通过了解这些 Go 调度器的语义,您可以做出更好的工程决策。</p>
<p>在下一篇文章中,我将探讨以保守的方式利用并发性以获得更好的性能,同时平衡可能需要增加到代码中的复杂性。</p>
Go 程序是如何编译成目标机器码的
https://segmentfault.com/a/1190000016523685
2018-09-26T15:25:38+08:00
2018-09-26T15:25:38+08:00
ronniesong
https://segmentfault.com/u/sxssxs
39
<blockquote>今天我们一起来研究 Go 1.11 的编译器,以及它将 Go 程序代码编译成可执行文件的过程。以便了解我们日常使用的工具是如何工作的。<br>本文还会带你了解 Go 程序为什么这么快,以及编译器在这中间起到了什么作用。</blockquote>
<h2>首先,编译器的三个阶段:</h2>
<ol>
<li>逐行扫描源代码,将之转换为一系列的 <code>token</code>,交给 <code>parser</code> 解析。</li>
<li>
<code>parser</code>,它将一系列 <code>token</code> 转换为 <code>AST(抽象语法树)</code>,用于下一步生成代码。</li>
<li>最后一步,代码生成,会利用上一步生成的 <code>AST</code> 并根据目标机器平台的不同,生成目标机器码。</li>
</ol>
<p>注意:下面使用的代码包(<strong>go/scanner</strong>,<strong>go/parser</strong>,<strong>go/token</strong>,<strong>go/ast</strong>)主要是让我们可以方便地对 Go 代码进行解析和生成,做出更有趣的事情。但是 Go 本身的编译器并不是用这些代码包实现的。</p>
<h2>扫描代码,进行词法分析</h2>
<p>任何编译器的第一步都是将源代码文本分解成 <code>token</code>,由扫描程序(也称为词法分析器)完成。<code>token</code> 可以是关键字,字符串,变量名,函数名等等。每一个有效的词都由 <code>token</code> 表示。<br>在 Go 中,我们写在代码上的 <code>"package"</code>,<code>"main"</code>,<code>"func"</code> 这些都是 <code>token</code>。</p>
<p><code>token</code> 由代码中的位置,类型和原始文本组成。我们可以使用 go/scanner 和 go/token 包在 Go 程序中自己执行扫描程序。这意味着我们可以像编译器那样扫描检视自己的代码。<br>下面,我们将通过一个打印 Hello World 的示例来展示 <code>token</code>。</p>
<pre><code class="go">package main
import (
"fmt"
"go/scanner"
"go/token"
)
func main() {
src := []byte(`
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
`)
var s scanner.Scanner
fset := token.NewFileSet()
file := fset.AddFile("", fset.Base(), len(src))
s.Init(file, src, nil, 0)
for {
pos, tok, lit := s.Scan()
fmt.Printf("%-6s%-8s%q\n", fset.Position(pos), tok, lit)
if tok == token.EOF {
break
}
}
}
</code></pre>
<p>首先通过源代码字符串创建 <code>token</code> 集合并初始化 <code>scan.Scanner</code>,它将逐行扫描我们的源代码。<br>接下来循环调用 <code>Scan()</code> 并打印每个 <code>token</code> 的位置,类型和文本字符串,直到遇到文件结束(EOF)标记。</p>
<p>输出:</p>
<pre><code class="tex">2:1 package "package"
2:9 IDENT "main"
2:13 ; "\n"
4:1 import "import"
4:8 STRING "\"fmt\""
4:13 ; "\n"
6:1 func "func"
6:6 IDENT "main"
6:10 ( ""
6:11 ) ""
6:13 { ""
7:2 IDENT "fmt"
7:5 . ""
7:6 IDENT "Println"
7:13 ( ""
7:14 STRING "\"Hello, world!\""
7:29 ) ""
7:30 ; "\n"
8:1 } ""
8:2 ; "\n"
8:3 EOF ""</code></pre>
<p>以第一行为例分析这个输出,第一列 <code>2:1</code> 表示扫描到了源代码第二行第一个字符,第二列 <code>package</code> 表示 <code>token</code> 是 <code>package</code>,第三列 <code>"package"</code> 表示源代码文本。<br>我们可以看到在 <code>Scanner</code> 执行过程中将 <code>\n</code> 换行符标记成了 <code>;</code> 分号,像在 C 语言中是用分号表示一行结束的。这就解释了为什么 Go 不需要分号:它们是在词法分析阶段由 <code>Scanner</code> 智能地解释的。</p>
<h2>语法分析</h2>
<p>源代码扫描完成后,扫描结果将被传递给语法分析器。语法分析是编译的一个阶段,它将 <code>token</code> 转换为 <code>抽象语法树(AST)</code>。 <br><code>AST</code> 是源代码的结构化表示。在 <code>AST</code> 中,我们将能够看到程序结构,比如函数和常量声明。</p>
<p>我们使用 <strong>go/parser</strong> 和 <strong>go/ast</strong> 来打印完整的 <code>AST</code>:</p>
<pre><code class="go">package main
import (
"go/ast"
"go/parser"
"go/token"
"log"
)
func main() {
src := []byte(`
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
`)
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
log.Fatal(err)
}
ast.Print(fset, file)
}</code></pre>
<p>输出:</p>
<pre><code> 0 *ast.File {
1 . Package: 2:1
2 . Name: *ast.Ident {
3 . . NamePos: 2:9
4 . . Name: "main"
5 . }
6 . Decls: []ast.Decl (len = 2) {
7 . . 0: *ast.GenDecl {
8 . . . TokPos: 4:1
9 . . . Tok: import
10 . . . Lparen: -
11 . . . Specs: []ast.Spec (len = 1) {
12 . . . . 0: *ast.ImportSpec {
13 . . . . . Path: *ast.BasicLit {
14 . . . . . . ValuePos: 4:8
15 . . . . . . Kind: STRING
16 . . . . . . Value: "\"fmt\""
17 . . . . . }
18 . . . . . EndPos: -
19 . . . . }
20 . . . }
21 . . . Rparen: -
22 . . }
23 . . 1: *ast.FuncDecl {
24 . . . Name: *ast.Ident {
25 . . . . NamePos: 6:6
26 . . . . Name: "main"
27 . . . . Obj: *ast.Object {
28 . . . . . Kind: func
29 . . . . . Name: "main"
30 . . . . . Decl: *(obj @ 23)
31 . . . . }
32 . . . }
33 . . . Type: *ast.FuncType {
34 . . . . Func: 6:1
35 . . . . Params: *ast.FieldList {
36 . . . . . Opening: 6:10
37 . . . . . Closing: 6:11
38 . . . . }
39 . . . }
40 . . . Body: *ast.BlockStmt {
41 . . . . Lbrace: 6:13
42 . . . . List: []ast.Stmt (len = 1) {
43 . . . . . 0: *ast.ExprStmt {
44 . . . . . . X: *ast.CallExpr {
45 . . . . . . . Fun: *ast.SelectorExpr {
46 . . . . . . . . X: *ast.Ident {
47 . . . . . . . . . NamePos: 7:2
48 . . . . . . . . . Name: "fmt"
49 . . . . . . . . }
50 . . . . . . . . Sel: *ast.Ident {
51 . . . . . . . . . NamePos: 7:6
52 . . . . . . . . . Name: "Println"
53 . . . . . . . . }
54 . . . . . . . }
55 . . . . . . . Lparen: 7:13
56 . . . . . . . Args: []ast.Expr (len = 1) {
57 . . . . . . . . 0: *ast.BasicLit {
58 . . . . . . . . . ValuePos: 7:14
59 . . . . . . . . . Kind: STRING
60 . . . . . . . . . Value: "\"Hello, world!\""
61 . . . . . . . . }
62 . . . . . . . }
63 . . . . . . . Ellipsis: -
64 . . . . . . . Rparen: 7:29
65 . . . . . . }
66 . . . . . }
67 . . . . }
68 . . . . Rbrace: 8:1
69 . . . }
70 . . }
71 . }
72 . Scope: *ast.Scope {
73 . . Objects: map[string]*ast.Object (len = 1) {
74 . . . "main": *(obj @ 27)
75 . . }
76 . }
77 . Imports: []*ast.ImportSpec (len = 1) {
78 . . 0: *(obj @ 12)
79 . }
80 . Unresolved: []*ast.Ident (len = 1) {
81 . . 0: *(obj @ 46)
82 . }
83 }</code></pre>
<p>分析这个输出,在 <code>Decls</code> 字段中,包含了代码中所有的声明,例如导入、常量、变量和函数。在本例中,我们只有两个:<strong>导入fmt包</strong> 和 <strong>主函数</strong>。<br>为了进一步理解它,我们可以看看下面这个图,它是上述数据的表示,但只包含类型,红色代表与节点对应的代码:</p>
<p><img src="/img/bVbhufq?w=1024&h=1312" alt="图片描述" title="图片描述"></p>
<p><code>main函数</code>由三个部分组成:<strong>Name</strong>、<strong>Type</strong> 和 <strong>Body</strong>。<strong>Name</strong> 是值为 <strong>main</strong> 的标识符。由 <code>Type</code> 字段指定的声明将包含参数列表和返回类型(如果我们指定了的话)。正文由一系列语句组成,里面包含了程序的所有行,在本例中只有一行<code>fmt.Println("Hello, world!")</code>。</p>
<p>我们的一条 <code>fmt.Println</code> 语句由 <code>AST</code> 中很多部分组成。<br>该语句是一个 <code>ExprStmt</code><strong>表达式语句(expression statement)</strong>,例如,它可以像这里一样是一个函数调用,它可以是字面量,可以是一个二元运算(例如加法和减法),当然也可以是一元运算(例如自增++,自减--,否定!等)等等。<br>同时,在函数调用的参数中可以使用任何表达式。</p>
<p>然后,<code>ExprStmt</code> 又包含一个 <code>CallExpr</code>,它是我们实际的函数调用。里面又包括几个部分,其中最重要的部分是 <code>Fun</code> 和 <code>Args</code>。 <br><code>Fun</code> 包含对函数调用的引用,在这种情况下,它是一个 <code>SelectorExpr</code>,因为我们从 <code>fmt</code> 包中选择 <code>Println</code> 标识符。<br>但是至此,在 <code>AST</code> 中,编译器还不知道 <code>fmt</code> 是一个包,它也可能是 <code>AST</code> 中的一个变量。</p>
<p><code>Args</code> 包含一个表达式列表,它是函数的参数。这里,我们将一个文本字符串传递给函数,因而它由一个类型为 <code>STRING</code> 的 <code>BasicLit</code> 表示。</p>
<p>显然,<code>AST</code> 包含了许多信息,我们不仅可以分析出以上结论,还可以进一步检查 <code>AST</code> 并查找文件中的所有函数调用。下面,我们将使用 <strong>go/ast</strong> 包中的 <code>Inspect</code> 函数来递归地遍历树,并分析所有节点的信息。</p>
<pre><code class="go">package main
import (
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"os"
)
func main() {
src := []byte(`
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
`)
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
fmt.Println(err)
}
ast.Inspect(file, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
printer.Fprint(os.Stdout, fset, call.Fun)
return false
})
}</code></pre>
<p>输出:</p>
<pre><code class="go">fmt.Println</code></pre>
<p>上面代码的作用是查找所有节点以及它们是否为 <code>*ast.CallExpr</code> 类型,上面也说过这种类型是函数调用。如果是,则使用 <strong>go/printer</strong> 包打印 <code>Fun</code> 中存在的函数的名称。</p>
<p>构建出 <code>AST</code> 后,将使用 <code>GOPATH</code> 或者在 Go 1.11 及更高版本中的 <strong><a href="https://link.segmentfault.com/?enc=c22gn4N2e2rqUk%2BDqsnN3A%3D%3D.hXA4VNvyPqPOpUPn7hdAxocp4794UIV57vVtTMKN4l0OWjhCdnMaT8rQdOPXl5bk" rel="nofollow">modules</a></strong> 解析所有导入。然后,执行类型检查,并做一些让程序运行更快的初级优化。</p>
<h2>代码生成</h2>
<p>在解析导入并做了类型检查之后,我们可以确认程序是合法的 Go 代码,然后就走到将 <code>AST</code> 转换为(伪)目标机器码的过程。</p>
<p>此过程的第一步是将 <code>AST</code> 转换为程序的低级表示,特别是转换为 <strong><a href="https://link.segmentfault.com/?enc=ZEeOrUkTuHLpbjZ33tiIZg%3D%3D.5%2B4IzjrRofWVN2sfJRN1ojudqWchNPOZe98GG5vYe26H1CFv4kOmOrPvpplvH2L33x3iEkzYG9uKE1qLBz9CyfTFHK9dytqzuRTjRUH2N6fGVA0GHipfvp4390sSQGP4" rel="nofollow">静态单赋值(SSA)表单</a></strong>。这个中间表示不是最终的机器代码,但它确实代表了最终的机器代码。 <code>SSA</code> 具有一组属性,会使应用优化变得更容易,其中最重要的是在使用变量之前总是定义变量,并且每个变量只分配一次。</p>
<p>在生成 <code>SSA</code> 的初始版本之后,将执行一些优化。这些优化适用于某些代码,可以使处理器执行起来更简单且更快速。例如,可以做 <strong><a href="https://segmentfault.com/a/1190000016354799#articleHeader10">死码消除</a></strong>。还有比如可以删除某些 nil 检查,因为编译器可以证明这些检查永远不会出错。</p>
<p>现在通过最简单的例子来说明 <code>SSA</code> 和一些优化过程:</p>
<pre><code class="go">package main
import "fmt"
func main() {
fmt.Println(2)
}</code></pre>
<p>如你所见,此程序只有一个函数和一个导入。它会在运行时打印 2。但是,此例足以让我们了解SSA。</p>
<p>为了显示生成的 <code>SSA</code>,我们需要将 <code>GOSSAFUNC</code> 环境变量设置为我们想要跟踪的函数,在本例中为<code>main</code> 函数。我们还需要将 -S 标识传递给编译器,这样它就会打印代码并创建一个HTML文件。我们还将编译Linux 64位的文件,以确保机器代码与您在这里看到的相同。<br>在终端执行下面的命令:</p>
<p><code>GOSSAFUNC=main GOOS=linux GOARCH=amd64 go build -gcflags -S main.go</code></p>
<p>会在终端打印出所有的 <code>SSA</code>,同时也会生成一个交互式的 <code>ssa.html</code> 文件,我们用浏览器打开它。</p>
<p><img src="/img/bVbhusX?w=1024&h=606" alt="图片描述" title="图片描述"></p>
<p>当你打开 <code>ssa.html</code> 时,将显示很多阶段,其中大部分都已折叠。<strong><code>start</code></strong> 阶段是从 <code>AST</code> 生成的<code>SSA</code>;<strong><code>lower</code></strong> 阶段将非机器特定的 <code>SSA</code> 转换为机器特定的 <code>SSA</code>,最后的 <strong><code>genssa</code></strong> 就是生成的机器代码。</p>
<p><strong><code>start</code></strong> 阶段的代码如下:</p>
<pre><code class="go">b1:
v1 = InitMem <mem>
v2 = SP <uintptr>
v3 = SB <uintptr>
v4 = ConstInterface <interface {}>
v5 = ArrayMake1 <[1]interface {}> v4
v6 = VarDef <mem> {.autotmp_0} v1
v7 = LocalAddr <*[1]interface {}> {.autotmp_0} v2 v6
v8 = Store <mem> {[1]interface {}} v7 v5 v6
v9 = LocalAddr <*[1]interface {}> {.autotmp_0} v2 v8
v10 = Addr <*uint8> {type.int} v3
v11 = Addr <*int> {"".statictmp_0} v3
v12 = IMake <interface {}> v10 v11
v13 = NilCheck <void> v9 v8
v14 = Const64 <int> [0]
v15 = Const64 <int> [1]
v16 = PtrIndex <*interface {}> v9 v14
v17 = Store <mem> {interface {}} v16 v12 v8
v18 = NilCheck <void> v9 v17
v19 = IsSliceInBounds <bool> v14 v15
v24 = OffPtr <*[]interface {}> [0] v2
v28 = OffPtr <*int> [24] v2
If v19 → b2 b3 (likely) (line 6)
b2: ← b1
v22 = Sub64 <int> v15 v14
v23 = SliceMake <[]interface {}> v9 v22 v22
v25 = Copy <mem> v17
v26 = Store <mem> {[]interface {}} v24 v23 v25
v27 = StaticCall <mem> {fmt.Println} [48] v26
v29 = VarKill <mem> {.autotmp_0} v27
Ret v29 (line 7)
b3: ← b1
v20 = Copy <mem> v17
v21 = StaticCall <mem> {runtime.panicslice} v20
Exit v21 (line 6)</code></pre>
<p>这个简单的程序就已经产生了相当多的 <code>SSA</code>(总共35行)。然而,很多都是引用,可以消除很多(最终的SSA版本有28行,最终的机器代码版本有18行)。</p>
<p>每个 <code>v</code> 都是一个新变量,可以点击来查看它被使用的位置。b 是块,这里有三块:b1,b2,b3。b1 始终会执行,<code>b2</code> 和 <code>b3</code> 是条件块,满足条件才执行。<br>我们来看 <code>b1</code> 结尾处的 <code>If v19 → b2 b3 (likely)</code>。单击该行中的 <code>v19</code> 可以查看它定义的位置。可以看到它定义为 <code>IsSliceInBounds <bool> v14 v15</code>,通过 <a href="https://link.segmentfault.com/?enc=%2FglUgsurjAWZv8k6LjiV5g%3D%3D.rZOjrTRJsAEWjPF76nmGwi7rEep%2BBJRSSxv3S3ySTqiSnEwZPlKUibey%2FQ2BkgAZnfIOxuzyitNC%2Bv6exNPHpQ9yhAC5zfm3lM5e%2FplAMofESdy97m%2BzADQZ7RAbZyel%2B%2FfId8kYDfXweVDMjIw16UlW5ioMcbk0OHuBJf%2BaETg%3D" rel="nofollow">Go 编译器源代码</a>,我们知道 <code>IsSliceInBounds</code> 的作用是检查 <code>0 <= arg0 <= arg1</code>。然后单击 <code>v14</code> 和 <code>v15</code> 看看在哪定义的,我们会看到 <code>v14 = Const64 <int> [0]</code>,Const64 是一个常量 64 位整数。 v15 定义一样,放在 args1 的位置。所以,实际执行的是 <code>0 <= 0 <= 1</code>,这显然是正确的。</p>
<p>编译器也能够证明这一点,当我们查看 <strong><code>opt</code></strong> 阶段(“机器无关优化”)时,我们可以看到它已经重写了 <code>v19</code> 为 <code>ConstBool <bool> [true]</code>。结果就是,在 <code>opt deadcode</code> 阶段,<code>b3</code> 条件块被删除了,因为永远也不会执行到 <code>b3</code>。</p>
<p>下面来看一下 Go 编译器在把 <code>SSA</code> 转换为 <code>机器特定的SSA</code> 之后所做的另一个更简单的优化,基于amd64体系结构的机器代码。下面,我们将比较 <strong><code>lower</code></strong> 和 <strong><code>lowered deadcode</code></strong>。<br><strong><code>lower</code></strong>:</p>
<pre><code class="go">b1:
BlockInvalid (6)
b2:
v2 (?) = SP <uintptr>
v3 (?) = SB <uintptr>
v10 (?) = LEAQ <*uint8> {type.int} v3
v11 (?) = LEAQ <*int> {"".statictmp_0} v3
v15 (?) = MOVQconst <int> [1]
v20 (?) = MOVQconst <uintptr> [0]
v25 (?) = MOVQconst <*uint8> [0]
v1 (?) = InitMem <mem>
v6 (6) = VarDef <mem> {.autotmp_0} v1
v7 (6) = LEAQ <*[1]interface {}> {.autotmp_0} v2
v9 (6) = LEAQ <*[1]interface {}> {.autotmp_0} v2
v16 (+6) = LEAQ <*interface {}> {.autotmp_0} v2
v18 (6) = LEAQ <**uint8> {.autotmp_0} [8] v2
v21 (6) = LEAQ <**uint8> {.autotmp_0} [8] v2
v30 (6) = LEAQ <*int> [16] v2
v19 (6) = LEAQ <*int> [8] v2
v23 (6) = MOVOconst <int128> [0]
v8 (6) = MOVOstore <mem> {.autotmp_0} v2 v23 v6
v22 (6) = MOVQstore <mem> {.autotmp_0} v2 v10 v8
v17 (6) = MOVQstore <mem> {.autotmp_0} [8] v2 v11 v22
v14 (6) = MOVQstore <mem> v2 v9 v17
v28 (6) = MOVQstoreconst <mem> [val=1,off=8] v2 v14
v26 (6) = MOVQstoreconst <mem> [val=1,off=16] v2 v28
v27 (6) = CALLstatic <mem> {fmt.Println} [48] v26
v29 (5) = VarKill <mem> {.autotmp_0} v27
Ret v29 (+7)</code></pre>
<p>在HTML中,某些行是灰色的,这意味着它们将在下一个阶段中被删除或修改。<br>例如,<code>v15 (?) = MOVQconst <int> [1]</code> 显示为灰色。点击 <code>v15</code>,我们看到它在其他地方都没有使用,而 <code>MOVQconst</code> 基本上与我们之前看到的 <code>Const64</code> 相同,只针对amd64的特定机器。我们把 <code>v15</code> 设置为1。但是,<code>v15</code> 在其他地方都没有使用,所以它是无用的(死的)代码并且可以消除。</p>
<p>Go 编译器应用了很多这类优化。因此,虽然 <code>AST</code> 生成的初始 <code>SSA</code> 可能不是最快的实现,但编译器将SSA优化为更快的版本。 HTML 文件中的每个阶段都有可能发生优化。</p>
<p>如果你有兴趣了解 Go 编译器中有关 <code>SSA</code> 的更多信息,请查看 <a href="https://link.segmentfault.com/?enc=Yg%2B0FMlvfy2H1%2FlT%2B0CfbA%3D%3D.7kuJFQF6omGWWSahRf7pWKoqthIt%2FXjS4EqMoBt4O2byQ0aLFtqCGxBw6zaAQ%2BC3YNps%2BMr5a0tg7OML6o1JlKqAkABNH9dgi%2FqQXG6EPA8%3D" rel="nofollow">Go 编译器的 SSA 源代码</a>。<br>这里定义了所有的操作以及优化。</p>
<h2>结论</h2>
<p>Go 是一种非常高效且高性能的语言,由其编译器及其优化支撑。要了解有关 Go 编译器的更多信息,<a href="https://link.segmentfault.com/?enc=mydHkNfPozTzSz%2Fuw%2FlfKg%3D%3D.yl3%2FQYYHqdklzlvjS9K2whSG50JxR1k24Rb0fNLG3rLD4P4mFy7F5%2BUDkWy0vz5mTCwpKAVAhxO9bXEesZ%2FQLg%3D%3D" rel="nofollow">源代码的 README</a> 是不错的选择。</p>
6. Go 性能调优之 —— 总结
https://segmentfault.com/a/1190000016354922
2018-09-11T15:58:07+08:00
2018-09-11T15:58:07+08:00
ronniesong
https://segmentfault.com/u/sxssxs
9
<blockquote>原文链接:<a href="https://link.segmentfault.com/?enc=4qXxUFXoEnDrHp57AkA2aA%3D%3D.8wjxxhrO%2FHXPovjMczVJKObCm%2BufTK0GUXAnqm0tfCrCvWx0NSoBvUePmOGM%2Fax6" rel="nofollow">https://github.com/sxs2473/go...</a><br>本文使用 <a href="https://link.segmentfault.com/?enc=ElLOXpWhgPGpw0aA8VKpUA%3D%3D.gVkqjwf6vYkVMYR1rIvm%2FCR3W9RkLiC0GrwcOY362zYL55Gm3BgSfGnDDdgBl7%2BuzTrMKdlwIk4toxUlIPerQVtTS%2FrHS6pMQWh0qVibZlWlCMn4DtEWcmcOx9KGgNiO" rel="nofollow">Creative Commons Attribution-ShareAlike 4.0 International</a> 协议进行授权许可。</blockquote>
<h2>总结</h2>
<h3>保持简单</h3>
<p>从最简单的代码开始。</p>
<p><em>测量!</em> 分析你的代码来找到瓶颈, <em>不要猜测</em> !</p>
<p>如果性能还不错, <em>收手吧</em> !你不需要优化所有的代码,只需要针对影响最大的部分就可以了。</p>
<h3>不是程序的每部分都需要高性能</h3>
<p>对于大多数关注性能的应用程序,适用80/20规则。80%的时间将花在20%的代码上。</p>
<p>随着应用程序的增长或业务发展,这些性能问题的重点将会变化。</p>
<p>不要留着对性能不重要的复杂代码,如果瓶颈转移到其他地方,就用更简单的实现重写它。</p>
<h3>Go 编译器针对简单代码进行了优化</h3>
<p>总是写你能写出的最简单的代码,编译器针对简单代码进行了优化。我不说 <em>惯用的</em> ,因为我不喜欢我们在讨论 Go 时使用这个词。我只说简单,而不是 <em>聪明</em> 的代码。</p>
<p>更短的代码就是更快; Go 不是 C++,不要指望编译器解开复杂的抽象。</p>
<p>更短的代码体积更小;这对 CPU 的缓存很重要。</p>
<h3>注意二次方复杂度的操作</h3>
<blockquote>If a program is too slow, it must have a loop -- Ken Thompson</blockquote>
<p>大多数程序在少量数据的情况下表现良好。 这是 <a href="https://link.segmentfault.com/?enc=742xeqq2H4Sjj85KNCBnWg%3D%3D.nBJZL%2B0TE8KSqmQ4TjaS0H%2F5Des8AR8DEHS%2Fw8i3yF1dFzOwfRpBsUfxtP0LhA5g" rel="nofollow">Pike's 3rd rule</a> 思想背后的精髓。</p>
<p>然而,当数据集很大时,任何接触输入集不止一次的东西,例如,对于集合中的每个元素,对集合中的每个其他元素进行测试,都有可能成为性能方面的大问题。</p>
<p>限制程序各部分之间的通信和协调点,以遵守 <a href="https://link.segmentfault.com/?enc=TUe9Q2PxeVFnlmGyVUiTuw%3D%3D.IudfgsPT4laHWTnlicXE4jToGQGjtPqu5iOpD1UpS5NMDjnbjItCgSVOYi%2FTjtOrVDAyroXJjFBpv2Qeft8rADqaPYmMH4S7an1HupMRMM5jbgz2K%2BvpP1ENh%2Fmeh9zR" rel="nofollow">Amdahl定律</a>。</p>
<h3>性能经验法则</h3>
<p>网络/硬盘 io >> 内存分配 >> 函数调用 ( >> 表示远远大于,意味着数量级之间的差距)</p>
<p>如果您的程序主要工作是网络或硬盘访问,那么不要费心去优化内存分配方面的事情。重点关注如何利用缓冲和批处理,以减少等待IO的时间。</p>
<p>如果您的程序是主要工作是分配和管理内存,不要费心去优化函数内联、循环展开等事情。</p>
<p>注意内存分配的使用,尽量避免不必要的分配。</p>
<h3>不要为了可靠性而牺牲性能</h3>
<blockquote>I can make things very fast if they don't have to be correct. -- Russ Cox</blockquote>
<p>最后,不要为了可靠性而牺牲性能</p>
<blockquote>Readable means reliable -- Rob Pike</blockquote>
<p>性能和可靠性同样重要。</p>
<h3>谢谢</h3>
5. Go 性能调优之 —— 技巧
https://segmentfault.com/a/1190000016354883
2018-09-11T15:56:15+08:00
2018-09-11T15:56:15+08:00
ronniesong
https://segmentfault.com/u/sxssxs
21
<blockquote>原文链接:<a href="https://link.segmentfault.com/?enc=H2p8TrqH8fRmAFVbKCOJ%2BA%3D%3D.vjLHYlbpbThnzmaZk9C61g9Q5Vk9ZZ7a%2B8Ls0rT7hV4C5cNUiufaDrmMCqdcZWL9" rel="nofollow">https://github.com/sxs2473/go...</a><br>本文使用 <a href="https://link.segmentfault.com/?enc=vxGN6piqdxW63y9%2FSoOutA%3D%3D.nf6rHOCoQ53miTzAfcpv0rkMKGMNU%2BTyWwn31vakVJHi976itWnR0S1vUVyX%2Fl0gpk%2BXYIMN%2F7oBaw7fWESqVA%3D%3D" rel="nofollow">Creative Commons Attribution-ShareAlike 4.0 International</a> 协议进行授权许可。</blockquote>
<h2>技巧</h2>
<p>本节包含一些优化 Go 代码的技巧。</p>
<h3>减少分配</h3>
<p>确保你的 APIs 不会给调用方增加垃圾。</p>
<p>考虑这两个 Read 方法</p>
<pre><code class="go">func (r *Reader) Read() ([]byte, error)
func (r *Reader) Read(buf []byte) (int, error)</code></pre>
<p>第一个 Read 方法不带参数,并将一些数据作为<code>[]byte</code>返回。 第二个采用<code>[]byte</code>缓冲区并返回读取的字节数。</p>
<p>第一个 Read 方法总是会分配一个缓冲区,这会给 GC 带来压力。 第二个填充传入的缓冲区。</p>
<h3>strings vs []bytes</h3>
<p>Go 语言中 <code>string</code> 是不可改变的,而 <code>[]byte</code> 是可变的。</p>
<p>大多数程序喜欢使用 <code>string</code>,而大多数 IO 操作更喜欢使用 <code>[]byte</code>。</p>
<p>尽可能避免 <code>[]byte</code> 到 <code>string</code> 的转换,对于一个值来说,最好选定一种表示方式,要么是<code>[]byte</code>,要么是<code>string</code>。 通常情况下,如果你从网络或磁盘读取数据,将使用<code>[]byte</code> 表示。</p>
<p><a href="https://link.segmentfault.com/?enc=vX9JiVLycPJqaLtu%2BwaCog%3D%3D.pYtxG7OFAbOBc0l8M6%2B1vwaDjuQYK4iVIrAwU1cXrgo%3D" rel="nofollow"><code>bytes</code></a> 包也有一些和 <a href="https://link.segmentfault.com/?enc=bRcqtdlqK4X9RRn1brFJTQ%3D%3D.A2G3q1%2FOz1wxWn5HSb1CW5mVTIrYXiED%2FPj5hSWCiaE%3D" rel="nofollow"><code>strings</code></a> 包相同的操作函数—— <code>Split</code>, <code>Compare</code>, <code>HasPrefix</code>, <code>Trim</code>等。</p>
<p>实际上, <code>strings</code> 使用和 <code>bytes</code> 包相同的汇编原语。</p>
<h3>使用 []byte 当做 map 的 key</h3>
<p>使用 <code>string</code> 作为 map 的 key 是很常见的,但有时你拿到的是一个 <code>[]byte</code>。</p>
<p>编译器为这种情况实现特定的优化:</p>
<pre><code class="go">var m map[string]string
v, ok := m[string(bytes)]</code></pre>
<p>如上面这样写,编译器会避免将字节切片转换为字符串到 map 中查找,这是非常特定的细节,如果你像下面这样写,这个优化就会失效:</p>
<pre><code class="go">key := string(bytes)
val, ok := m[key]</code></pre>
<h3>优化字符串连接操作</h3>
<p>Go 的字符串是不可变的。连接两个字符串就会生成第三个字符串。下面哪种写法是最快的呢?</p>
<pre><code class="go">s := request.ID
s += " " + client.Addr().String()
s += " " + time.Now().String()
r = s</code></pre>
<pre><code class="go">var b bytes.Buffer
fmt.Fprintf(&b, "%s %v %v", request.ID, client.Addr(), time.Now())
r = b.String()</code></pre>
<pre><code class="go">r = fmt.Sprintf("%s %v %v", request.ID, client.Addr(), time.Now())</code></pre>
<pre><code class="go">b := make([]byte, 0, 40)
b = append(b, request.ID...)
b = append(b, ' ')
b = append(b, client.Addr().String()...)
b = append(b, ' ')
b = time.Now().AppendFormat(b, "2006-01-02 15:04:05.999999999 -0700 MST")
r = string(b)</code></pre>
<pre><code>% go test -bench=. ./examples/concat/</code></pre>
<p>我的测试结果:</p>
<ul><li>go 1.10.3</li></ul>
<pre><code>goos: darwin
goarch: amd64
pkg: test/benchmark
BenchmarkConcatenate-8 2000000 873 ns/op 272 B/op 10 allocs/op
BenchmarkFprintf-8 1000000 1509 ns/op 496 B/op 13 allocs/op
BenchmarkSprintf-8 1000000 1316 ns/op 304 B/op 11 allocs/op
BenchmarkStrconv-8 2000000 620 ns/op 165 B/op 5 allocs/op
PASS
</code></pre>
<ul><li>go 1.11</li></ul>
<pre><code>goos: darwin
goarch: amd64
pkg: test/benchmark
BenchmarkConcatenate-8 1000000 1027 ns/op 271 B/op 10 allocs/op
BenchmarkFprintf-8 1000000 1707 ns/op 496 B/op 12 allocs/op
BenchmarkSprintf-8 1000000 1412 ns/op 304 B/op 11 allocs/op
BenchmarkStrconv-8 2000000 707 ns/op 165 B/op 5 allocs/op
PASS</code></pre>
<p>所有的基准测试在1.11版本下都变慢了?</p>
<h3>已知长度时,切片一次分配好</h3>
<p>Append 操作虽然方便,但是有代价。</p>
<p>切片的增长在元素到达 1024 个之前一直是两倍左右地变化,在到达 1024 个之后之后大约是 25% 地增长。在我们 append 之后的容量是多少呢?</p>
<pre><code class="go">func main() {
b := make([]int, 1024)
fmt.Println("len:", len(b), "cap:", cap(b))
b = append(b, 99)
fmt.Println("len:", len(b), "cap:", cap(b))
}
output:
len: 1024 cap: 1024
len: 1025 cap: 1280</code></pre>
<p>如果你使用 append,你可能会复制大量数据并产生大量垃圾。</p>
<p>如果事先知道片的长度,最好预先分配大小以避免复制,并确保目标的大小完全正确。</p>
<p><em>Before:</em></p>
<pre><code class="go">var s []string
for _, v := range fn() {
s = append(s, v)
}
return s</code></pre>
<p><em>After:</em></p>
<pre><code class="go">vals := fn()
s := make([]string, len(vals))
for i, v := range vals {
s[i] = v
}
return s</code></pre>
<h3>Goroutines</h3>
<p>使 Go 非常适合现代硬件的关键特性是 goroutines。goroutine 很容易使用,成本也很低,你可以认为它们几乎是没有成本的。</p>
<p>Go 运行时是为运行数以万计的 goroutines 所设计的,即使有上十万也在意料之中。</p>
<p>但是,每个 goroutine 确实消耗了 goroutine 栈的最小内存量,目前至少为 2k。</p>
<p>2048 * 1,000,000 goroutines == 2GB 内存,什么都不干的情况下。</p>
<p>这也许算多,也许不算多,同时取决于机器上其他耗费内存的应用。</p>
<h3>要了解 goroutine 什么时候退出</h3>
<p>虽然 goroutine 的启动和运行成本都很低,但它们的内存占用是有限的;你不可能创建无限数量的 goroutine。</p>
<p>每次在程序中使用<code>go</code>关键字启动 goroutine 时,你都必须知道这个 goroutine 将如何退出,以及何时退出。</p>
<p>如果你不知道,那这就是潜在的内存泄漏。</p>
<p>在你的设计中,一些 goroutine 可能会一直运行到程序退出。这样的 goroutine 不应该太多</p>
<p><strong>永远不要在不知道该什么时候停止它的情况下启动一个 goroutine</strong></p>
<p>实现此目的的一个好方法是利用如 <a href="https://link.segmentfault.com/?enc=BEgIZyUXyglLcpPNUAvZDA%3D%3D.VrXs%2BbXseElAWyUj9oZdE0rqOyCVcarQvkb7cJPjfEs%3D" rel="nofollow">run.Group</a>, <a href="https://link.segmentfault.com/?enc=2J0DPG7oJT3A1jANXO3NGA%3D%3D.I38Cz9m2WG9My8AlyOKy1x%2BmnDnkR0Wz3pRnNsLs1ExMalAZz5cU72P4zkK4UmzJ" rel="nofollow">workgroup.Group</a> 这类的东西。</p>
<p>Peter Bourgon has a great presentation on the design behing run.Group from GopherCon EU</p>
<h4>进一步阅读</h4>
<ul>
<li>
<a href="https://link.segmentfault.com/?enc=PqkU4FnpzM3VdcWgQEQoMQ%3D%3D.Z%2B4nNhTGiO2czRDiCZZHatYjHCHw7ZMupjkt8imT1z67gpuJEMWkB0qcCncRQN7C2lq0Oi3uP3z7Dih64ue%2FaA%3D%3D" rel="nofollow">Concurrency Made Easy</a> (视频)</li>
<li>
<a href="https://link.segmentfault.com/?enc=u0TPOgN41zE1KKfi%2F7kZoQ%3D%3D.x8%2FTke68xb%2FATMVoE7J0on4BJAvsw9WnQnnZYtsiuj8sf5a%2BmbQxKZe%2B7oTyhro5PhBRLZaaLVZgIXHvbSQvLg%3D%3D" rel="nofollow">Concurrency Made Easy</a> (幻灯片)</li>
</ul>
<h3>Go 对一些请求使用高效的网络轮询</h3>
<p>Go 运行时使用高效的操作系统轮询机制(kqueue,epoll,windows IOCP等)处理网络IO。 许多等待的 goroutine 将由一个操作系统线程提供服务。</p>
<p>但是,对于本地文件IO(channel 除外),Go 不实现任何 IO 轮询。每一个<code>*os.File</code>在运行时都消耗一个操作系统线程。</p>
<p>大量使用本地文件IO会导致程序产生数百或数千个线程;这可能会超过操作系统的最大值限制。</p>
<p>您的磁盘子系统可能处理不数百或数千个并发IO请求。</p>
<h3>注意程序中的 IO 复杂度</h3>
<p>如果你写的是服务端程序,那么其主要工作是复用网络连接客户端和存储在应用程序中的数据。</p>
<p>大多数服务端程序都是接受请求,进行一些处理,然后返回结果。这听起来很简单,但有的时候,这样做会让客户端在服务器上消耗大量(可能无限制)的资源。下面有一些注意事项:</p>
<ul>
<li>每个请求的IO操作数量;单个客户端请求生成多少个IO事件? 如果使用缓存,则它可能平均为1,或者可能小于1。</li>
<li>服务查询所需的读取量;它是固定的?N + 1的?还是线性的(读取整个表格以生成结果的最后一页)?</li>
</ul>
<p>如果内存都不算快,那么相对来说,IO操作就太慢了,你应该不惜一切代价避免这样做。 最重要的是避免在请求的上下文中执行IO——不要让用户等待磁盘子系统写入磁盘,甚至连读取都不要做。</p>
<h3>使用流式 IO 接口</h3>
<p>尽可能避免将数据读入<code>[]byte</code> 并传递使用它。</p>
<p>根据请求的不同,你最终可能会将兆字节(或更多)的数据读入内存。这会给GC带来巨大的压力,并且会增加应用程序的平均延迟。</p>
<p>作为替代,最好使用<code>io.Reader</code>和<code>io.Writer</code>构建数据处理流,以限制每个请求使用的内存量。</p>
<p>如果你使用了大量的<code>io.Copy</code>,那么为了提高效率,请考虑实现<code>io.ReaderFrom</code> / <code>io.WriterTo</code>。 这些接口效率更高,并避免将内存复制到临时缓冲区。</p>
<h3>超时,超时,还是超时</h3>
<p>永远不要在不知道需要多长时间才能完成的情况下执行 IO 操作。</p>
<p>你要在使用<code>SetDeadline</code>,<code>SetReadDeadline</code>,<code>SetWriteDeadline</code>进行的每个网络请求上设置超时。</p>
<p>您要限制所使用的阻塞IO的数量。 使用 goroutine 池或带缓冲的 channel 作为信号量。</p>
<pre><code class="go">var semaphore = make(chan struct{}, 10)
func processRequest(work *Work) {
semaphore <- struct{}{} // 持有信号量
// 执行请求
<-semaphore // 释放信号量
}</code></pre>
<h3>Defer 操作成本如何?</h3>
<p><code>defer</code> 是有成本的,因为它必须为其执行参数构造一个闭包去执行。</p>
<pre><code>defer mu.Unlock()</code></pre>
<p>相当于</p>
<pre><code>defer func() {
mu.Unlock()
}()</code></pre>
<p>如果你用它干的事情很少,<code>defer</code> 的成本就会显得比较高。一个经典的例子是使用<code>defer</code>对 struct 或 map 进行<code>mutex unlock</code> 操作。 你可以在这些情况下避免使用<code>defer</code></p>
<p>当然,这是为了提高性能而牺牲可读性和维护性的情况。</p>
<h5>总是重新考虑这些决定。</h5>
<h3>避免使用 Finalizers</h3>
<p>终结器是一种将行为附加到即将被垃圾收集的对象的技术。 </p>
<p>因此,终结器是非确定性的。</p>
<p>要运行 Finalizers,要保证任何东西都不会访问该对象。 如果你不小心在 map 中保留了对象的引用,则 Finalizers 无法执行。</p>
<p>Finalizers 作为 gc 的一部分运行,这意味着它们在运行时是不可预测的,并且它会与 <em>减少 gc 时间</em> 的目标相悖。</p>
<p>当你有一个非常大的堆块,并且已经优化过你的程序使之减少生成垃圾,Finalizers 可能才会很快结束。</p>
<p><em>提示</em> :参考 <a href="https://link.segmentfault.com/?enc=S5EgHb9I9Qg4rtK3QlCQyQ%3D%3D.KcEv81JSIJ7frrefCxkEDHPvYYk5piorjm1TRwYS%2B7MJpeCJMATJF%2BdQHzzide6J" rel="nofollow">SetFinalizer</a></p>
<h3>最小化 cgo</h3>
<p>cgo 允许 Go 程序调用 C 语言库。</p>
<p>C 代码和 Go 代码存在于两个不同的世界中,cgo 用来转换它们。</p>
<p>这种转换不是没有代价的,主要取决于它在代码中的位置,有时成本可能很高。</p>
<p>cgo 调用类似于阻塞IO,它们在操作期间消耗一个系统线程。</p>
<p>不要在一个 <a href="https://link.segmentfault.com/?enc=wSi480dcuMmJn98KkPASbQ%3D%3D.9q3yar3TIiMMhmweXUMmC9WWMEVLiy0VixsWPpOWqUudVacf7R%2BsVCrHEmc2PKaI" rel="nofollow">tight loop</a> 中调用 C 代码。</p>
<h3>实际上,避免使用 cgo</h3>
<p>cgo 的开销很高。</p>
<p>为了获得最佳性能,我建议你在应用中避免使用cgo。</p>
<ul>
<li>如果C代码需要很长时间,那么 cgo 本身的开销就不那么重要了。</li>
<li>如果你使用 cgo 来调用非常短的C函数,那么cgo本身的开销就会显得非常突出,那么最好的办法是在 Go 中重写该代码。(因为很短,重写也没什么成本。</li>
<li>如果你就是要使用大量高开销成本的C代码在 tight loop 中调用,为什么使用 Go?(直接用 C 写就好了被。</li>
</ul>
<h3>始终使用最新版发布的 Go 版本</h3>
<p>Go 的旧版本永远不会变得更好。他们永远不会得到错误修复或优化。</p>
<ul>
<li>Go 1.4 不应该再使用。</li>
<li>Go 1.5 和 1.6 编译器的速度更慢,但它产生更快的代码,并具有更快的 GC。</li>
<li>Go 1.7 的编译速度比 1.6 提高了大约 30%,链接速度提高了2倍(优于之前的Go版本)。</li>
<li>Go 1.8 在编译速度方面带来较小的改进,且在非Intel体系结构的代码质量方面有显著的改进。</li>
<li>Go 1.9,1.10,1.11 继续降低 GC 暂停时间并提高生成代码的质量。</li>
</ul>
<p>Go 的旧版本不会有任何更新。 不要使用它们。 使用最新版本,你将获得最佳性能。</p>
4. Go 性能调优之 —— 跟踪
https://segmentfault.com/a/1190000016354853
2018-09-11T15:55:01+08:00
2018-09-11T15:55:01+08:00
ronniesong
https://segmentfault.com/u/sxssxs
12
<blockquote>原文链接:<a href="https://link.segmentfault.com/?enc=ifDp9Kb7qnssaNf1mr8aMw%3D%3D.cIqcXMvq0lJCukPapszuolUd1TOKbNl2uiV7%2BGanEbPSZ6vIlmPt8tPL14FKZhbe" rel="nofollow">https://github.com/sxs2473/go...</a><br>本文使用 <a href="/img/bVbgMAJ">Creative Commons Attribution-ShareAlike 4.0 International</a> 协议进行授权许可。</blockquote>
<h2>Tracing Go programs</h2>
<p>在 Go 1.5 中,添加了一个新的工具:执行跟踪器。在本章中,我们将了解跟踪器的作用以及它如何帮助我们在程序中指出性能问题。</p>
<p>与<code>pprof</code>不同的是,正如我们在检查 Go 程序当前执行的内容之前看到的,执行跟踪器使 Go 运行时在每次事件发生时主动报告。这些事件可以是 goroutine 的创建、系统调用、堆大小的更改等等。每次发生这些事件中的一个时,都会报告其时间戳和大多数事件的堆栈跟踪。</p>
<p>在启用Go执行跟踪器的情况下执行程序的结果是一个相当大的二进制文件,可以用 <code>go tool trace</code>进行分析。</p>
<h3>第一个例子</h3>
<p>关于Go执行跟踪器的一个很棒的事情是它不需要运行很长时间,因此我们可以通过简单地加上对<code>trace.Start</code>和<code>trace.Stop</code>的调用来了解程序的功能。</p>
<pre><code class="go">package main
import (
"log"
"os"
"runtime/trace"
)
func main() {
_ = trace.Start(os.Stdout)
defer trace.Stop()
const n = 3
leftmost := make(chan int)
right := leftmost
left := leftmost
for i := 0; i < n; i++ {
right = make(chan int)
go pass(left, right)
left = right
}
go sendFirst(right)
log.Println(<-leftmost)
}
func pass(left, right chan int) {
v := 1 + <-right
left <- v
}
func sendFirst(ch chan int) { ch <- 0 }</code></pre>
<p>因此,在不读取代码中任何其他内容的情况下,让我们只运行代码并存储跟踪输出。</p>
<pre><code class="bash">$ go run daisy/main.go > trace.out
3
$ go tool trace trace.out
2017/07/10 17:47:47 Parsing trace...
2017/07/10 17:47:47 Serializing trace...
2017/07/10 17:47:47 Splitting trace...
2017/07/10 17:47:47 Opening browser</code></pre>
<p>这将打开一个带有一系列链接的浏览器,让我们点击 <code>Goroutine analysis</code>,你会看到这样的东西:</p>
<pre><code>Goroutines:
runtime.main N=1
main.pass N=3
runtime/trace.Start.func1 N=1
main.sendFirst N=1
N=3 </code></pre>
<p>好的,所以我们总共有5个 goroutines,一个正在运行<code>main</code>,一个正在运行<code>pass</code>,一个正在运行<code>sendFirst</code>。 还有一个运行跟踪器。</p>
<p>当我们点击 <code>Synchronization blocking profile</code> 你会看到一个有趣的图表。</p>
<p><img src="/img/bVbgMAJ?w=805&h=231" alt="图片描述" title="图片描述"></p>
<p>看起来<code>main</code>和<code>pass</code>都花了相当多的时间尝试从一个 channel 接收。</p>
<p>现在让我们点击 <code>View trace</code>, 你会看到这样的东西:</p>
<p><img src="/img/bVbgMAK?w=837&h=364" alt="图片描述" title="图片描述"></p>
<p>好的,我们已经可以在这里看到很多信息了!让我们从线程的数量开始。在图形的<code>Threads</code>上单击任意位置,你将看到当时运行了多少个线程。</p>
<p><img src="/img/bVbgMAO?w=657&h=318" alt="图片描述" title="图片描述"></p>
<p>在本例中,我们看到有四个线程,其中一个用于系统调用。</p>
<p>类似地,你可以单击<code>Goroutines</code>行,并了解在程序的每个点上有多少个Goroutines。</p>
<p><img src="/img/bVbgMAT?w=670&h=438" alt="图片描述" title="图片描述"></p>
<p>看起来我们有4个,其中2个正在运行,并且没有一个被垃圾收集器阻塞。</p>
<p>Wow! 甚至在我们 一行代码都没读之前,我们就可以从程序中理解很多东西! 但现在是时候阅读代码了,以便更好地解释发生了什么。</p>
<pre><code class="go">package main
import (
"log"
"os"
"runtime/trace"
)
func main() {
_ = trace.Start(os.Stdout)
defer trace.Stop()
const n = 3
leftmost := make(chan int)
right := leftmost
left := leftmost
for i := 0; i < n; i++ {
right = make(chan int)
go pass(left, right)
left = right
}
go sendFirst(right)
log.Println(<-leftmost)
}
func pass(left, right chan int) {
v := 1 + <-right
left <- v
}
func sendFirst(ch chan int) { ch <- 0 }</code></pre>
<p>上述代码创建了一个通过 channel 连接的 goroutines 链,然后在一端发送值并等待在另一端接收该值。</p>
<p><img src="/img/bVbgMA3?w=1051&h=472" alt="图片描述" title="图片描述"></p>
<p>现在我们知道了这一点,让我们回到跟踪查看器并分析依赖关系。</p>
<p>点击 <code>View Options</code>展开,选中<code>Flow Events</code>开启可视化。</p>
<p><img src="/img/bVbgMA9?w=203&h=90" alt="图片描述" title="图片描述"></p>
<p>花些时间浏览依赖关系图并尝试查看每个 goroutine 如何通过 channel 与其他 goroutine 同步。</p>
<p><img src="/img/bVbgMBa?w=1096&h=132" alt="图片描述" title="图片描述"></p>
3. Go 性能调优之 —— 性能测量和分析
https://segmentfault.com/a/1190000016354834
2018-09-11T15:53:38+08:00
2018-09-11T15:53:38+08:00
ronniesong
https://segmentfault.com/u/sxssxs
16
<blockquote>原文链接:<a href="https://link.segmentfault.com/?enc=aKJ%2FH32Ys%2Br4exfCXC%2Bsug%3D%3D.xuCGC9eMCtXYqvcxUKAyqFdI7dOmicoYLe6ZvPRk4YBbFy6rDn3x3MShs1itCtpt" rel="nofollow">https://github.com/sxs2473/go...</a><br>本文使用 <a href="https://link.segmentfault.com/?enc=gbCqVNVqCC%2FTsXyRh3vqnw%3D%3D.jmLzIlkUhOtApfRRbnuxebAxFeLtTsFtZB9vtMao6e8%3D" rel="nofollow">Creative Commons Attribution-ShareAlike 4.0 International</a> 协议进行授权许可。</blockquote>
<h2>性能测量和分析</h2>
<p>在先前的部分,我们研究了对单个函数的基准测试,当您提前知道瓶颈在哪里时,这是非常有用的。然而,你经常会发现自己处于提问的位置</p>
<blockquote>为什么这个程序要运行这么长时间?</blockquote>
<p>剖析整个程序,这对于回答诸如此类的高级问题非常有用。在本节中,我们将使用Go内置的分析工具从内部研究程序的操作。</p>
<h3>pprof</h3>
<p>我们今天要讲的第一个工具是 <em>pprof</em> 。 <a href="https://link.segmentfault.com/?enc=FkwJAiQ6Yj%2BxJsyhvS6%2FMg%3D%3D.%2ByqEdgmMF2c%2BoN%2BEMFRj0Az9vIrmjzP5XtGBHMNY8z8%3D" rel="nofollow">pprof</a> 来自于 <a href="https://link.segmentfault.com/?enc=a%2FWKzcdr8IRDu8peKJ5q5w%3D%3D.NyuRrOQ7CssFzort8zE3lzJQZsJz%2F%2FrmmXOQ20Znwk8%2Fd7Y%2F8ov8frOFBFiudnKm" rel="nofollow">Google Perf Tools</a> ,自最早的公开发布以来,已经集成到 Go 运行时中。</p>
<p><code>pprof</code> 由两部分组成:</p>
<ul>
<li>
<code>runtime/pprof</code> 每个 Go 程序都内置的包</li>
<li>
<code>go</code>tool<code>pprof</code> 用于解析 profile 文件</li>
</ul>
<p>pprof 支持好几种类型的分析,我们今天将讨论其中的三种:</p>
<ul>
<li>CPU 分析</li>
<li>内存分析</li>
<li>阻塞分析</li>
<li>锁竞争分析</li>
</ul>
<h3>CPU 分析</h3>
<p>CPU 分析是最常见的类型,也是最明显的。</p>
<p>当启用 CPU 分析时,运行时将每 10ms 中断一次,并记录当前运行的 goroutines 的栈跟踪。</p>
<p>一旦分析文件完成,我们就可以解析它以确定运行时间最长的代码路径。</p>
<p>函数在分析文件中出现的次数越多,代码路径占总运行时间的百分比就越多。</p>
<h3>内存分析</h3>
<p>在进行堆分配时,内存分析会记录调用栈跟踪</p>
<p>栈分配被认为是无成本的,并且在内存 profile 中不被追踪</p>
<p>内存分析,就像 CPU 分析是基于样本的一样,默认情况下,每 1000 个分配中有 1 个内存分析样本。这个速率是可以改变的。</p>
<p>由于内存分析是基于样本的,并且因为它也跟踪尚没被使用的分配,因此使用内存分析来确定应用程序的总内存使用量是很困难的。</p>
<p><em>个人想法:</em> 我不认为内存分析对查找内存泄漏有用。有更好的方法来确定应用程序使用了多少内存。我们将在以后的文章中讨论这些。</p>
<h3>阻塞分析</h3>
<p>阻塞分析非常独特。</p>
<p>阻塞 profile 和 CPU profile 非常类似,但它记录了 goroutine 等待共享资源所花费的时间。</p>
<p>这对于确定应用程序中的并发瓶颈非常有用。</p>
<p>阻塞分析可以向你展示大量 goroutine 何时可以取得进展但是被阻塞了。包括:</p>
<ul>
<li>在没有缓冲的 channel 上发送或接收</li>
<li>向已满的 channel 发送,或从一个空 channel 接收</li>
<li>试图 <code>Lock</code> 一个已经被另一个 goroutine 锁定的 <code>sync.Mutex</code>
</li>
</ul>
<p>阻塞分析是一个非常专业的工具,在你认为已经消除了所有 CPU 和内存使用瓶颈之前,不应该使用它。</p>
<h3>互斥锁分析</h3>
<p>互斥锁分析与阻塞分析类似,但只关注互斥锁竞争导致延迟的操作。</p>
<h3>一次一个 profile</h3>
<p>profile 记录是有成本的</p>
<p>profile 分析对程序性能有一种适度但可衡量的影响, 尤其是在增加内存分析采样率的情况下。</p>
<p>大多数工具不会阻止你同时启用多个 profile 。</p>
<p><strong>但还是不要一次启用多个 profile 。</strong></p>
<p>如果你同时启用多个 profile,他们将观察自己的互动并抛弃你的结果。</p>
<h3>收集一个 profile</h3>
<p>Go 运行时的分析接口存在于 <code>runtime/pprof</code> 包中。 <code>runtime/pprof</code> 是一个非常低级的工具,由于历史原因,不同类型 profile 的接口并不统一。</p>
<p>正如我们在前一节中所看到的,pprof 分析工具已经构建到 <code>testing</code> 包中,但有时,在<code>testing.B</code>基准测试的上下文中放置您想要分析的代码是不方便或困难的,并且必须直接使用<code>runtime/pprof</code> API。</p>
<p>这里有一个 <a href="https://link.segmentfault.com/?enc=IOlYCiKuOVSYjGl1ODUnZQ%3D%3D.YggkojJ83x%2BKgMJm0ZcLM24Xflw4HGyP4fQ9AcUObLs%3D" rel="nofollow">small package</a>,便于更容易地分析现有的程序。</p>
<pre><code>import "github.com/pkg/profile"
func main() {
defer profile.Start().Stop()
...
}</code></pre>
<p>我们将在本节中使用这个 profile 包。晚些时候,我们将直接使用<code>runtime/pprof</code>接口。</p>
<h4>使用 pprof</h4>
<p>解析使用 <code>go pprof</code> 子命令:</p>
<pre><code>go tool pprof /path/to/your/profile</code></pre>
<p><em>注意</em> : 如果你已经使用 Go 一段时间了,你可能会被告知<code>pprof</code>有两个参数。从 Go 1.9 开始,profile 文件包含展示 profile 所需的所有信息。你不再需要生成 profile 的二进制文件了。 🎉</p>
<h5>进一步阅读</h5>
<ul>
<li>
<a href="https://link.segmentfault.com/?enc=7Xf%2BQffjKdx1Xgg6GLVxxg%3D%3D.PEs5Qjhk6Uor3vS6TkMDUzv8e2NaCoDG6Zhnldqe3KcnAxkxQbVeIN4J4fyq1uKM" rel="nofollow">Profiling Go programs</a> (Go Blog)</li>
<li><a href="https://link.segmentfault.com/?enc=bmjVnVh9feGkmcphUVKKeg%3D%3D.eJEuK%2FqTEnORPMqE7idx7ms%2BlsicRtkxggsZXsoWlYv2lZcW6xjH1oWUotUFaYLxAE0I5T3nJlc2uDDrLp6elsCQJ%2FUdB55NH9aSQZqaxFY0MFD3f9pcVdSlgGCi5TnH" rel="nofollow">Debugging performance issues in Go programs</a></li>
</ul>
<h3>CPU 分析 - 例1</h3>
<p>我们写一个程序来计算单词数量:</p>
<pre><code>package main
import (
"fmt"
"io"
"log"
"os"
"unicode"
)
func readbyte(r io.Reader) (rune, error) {
var buf [1]byte
_, err := r.Read(buf[:])
return rune(buf[0]), err
}
func main() {
f, err := os.Open(os.Args[1])
if err != nil {
log.Fatalf("could not open file %q: %v", os.Args[1], err)
}
words := 0
inword := false
for {
r, err := readbyte(f)
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("could not read file %q: %v", os.Args[1], err)
}
if unicode.IsSpace(r) && inword {
words++
inword = false
}
inword = unicode.IsLetter(r)
}
fmt.Printf("%q: %d words\n", os.Args[1], words)
}</code></pre>
<p>让我们看看赫尔曼·梅尔维尔的经典<a href="https://link.segmentfault.com/?enc=%2FIxc0Wvvu%2FJKMFsUBfuQ6g%3D%3D.mwMnzNUXhQaBhjzfTgnQTlyQ02oeYA3rw%2BKt3LdOoSFty9qDcqDXQU0cgz78mPZe" rel="nofollow">《白鲸记》</a> (源自古腾堡计划)中有多少单词。</p>
<pre><code>% time go run main.go moby.txt
"moby.txt": 181275 words
real 0m2.110s
user 0m1.264s
sys 0m0.944s</code></pre>
<p>来和 unix 系统的 <code>wc -w</code> 做一个比较</p>
<pre><code>% time wc -w moby.txt
215829 moby.txt
real 0m0.012s
user 0m0.009s
sys 0m0.002s</code></pre>
<p>结果不一样。wc 给出的字数高出 19% 左右,因为它计算一个词的规则与我的例子不同。这并不重要——两个程序都将整个文件作为输入,并在一次传递中计算从单词到非单词的转换次数。</p>
<p>让我们使用 pprof 调查这些程序为何具有不同的运行时间。</p>
<h4>加入 CPU 分析</h4>
<p>首先,编辑 <code>main.go</code> 并开启 profile</p>
<pre><code class="go"> ...
"github.com/pkg/profile"
)
func main() {
defer profile.Start().Stop()
...</code></pre>
<p>现在,当我们的程序运行起来时,会创建一个<code>cpu.pprof</code> 文件</p>
<pre><code>% go run main.go moby.txt
2018/08/25 14:09:01 profile: cpu profiling enabled, /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile239941020/cpu.pprof
"moby.txt": 181275 words
2018/08/25 14:09:03 profile: cpu profiling disabled, /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile239941020/cpu.pprof</code></pre>
<p>现在我们可用用 <code>go tool pprof</code>来分析它</p>
<pre><code>% go tool pprof /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile239941020/cpu.pprof
Type: cpu
Time: Aug 25, 2018 at 2:09pm (AEST)
Duration: 2.05s, Total samples = 1.36s (66.29%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 1.42s, 100% of 1.42s total
flat flat% sum% cum cum%
1.41s 99.30% 99.30% 1.41s 99.30% syscall.Syscall
0.01s 0.7% 100% 1.42s 100% main.readbyte
0 0% 100% 1.41s 99.30% internal/poll.(*FD).Read
0 0% 100% 1.42s 100% main.main
0 0% 100% 1.41s 99.30% os.(*File).Read
0 0% 100% 1.41s 99.30% os.(*File).read
0 0% 100% 1.42s 100% runtime.main
0 0% 100% 1.41s 99.30% syscall.Read
0 0% 100% 1.41s 99.30% syscall.read</code></pre>
<p><code>top</code> 命令从降序展示了函数的调用时间。 我们可以看到在 <code>syscall.Syscall</code>上花费了 99% 的时间, 和 <code>main.readbyte</code>花费了很少的一部分。 </p>
<p>我们还可以使用web命令可视化这个调用。这将从 profile 数据生成有向图。它实际使用来自 Graphviz 的<code>dot</code>命令。</p>
<p><img src="/img/bVbgMzV?w=2700&h=2048" alt="图片描述" title="图片描述"></p>
<p>在图中,消耗 CPU 时间最多的盒子是最大的 -- 我们看到的<code>sys call.Syscall</code> 占用了总程序运行时间的 99.3%。通往<code>syscall.Syscall</code>的一串盒子代表它的直接调用者 -- 如果多个路径收敛于同一个函数,则表示有多个调用者。箭头旁边的数字表示运行所花费的时间。我们从 <code>main.readbyte</code> 开始看,一直到最后,占用都接近0。</p>
<h4>改进我们的版本</h4>
<p>我们程序跑慢不是因为 Go 的 <code>syscall.Syscall</code> 。因为系统调用本来就慢。</p>
<p>每次调用<code>readbyte</code>都会产生一个缓冲区大小为1的<code>syscall.Read</code>。因此,我们程序执行的系统调用数等于输入的大小。在 pprof 图中我们可以看到,读取输入决定了其他一切。</p>
<pre><code class="go">func main() {
f, err := os.Open(os.Args[1])
if err != nil {
log.Fatalf("could not open file %q: %v", os.Args[1], err)
}
b := bufio.NewReader(f)
words := 0
inword := false
for {
r, err := readbyte(b)
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("could not read file %q: %v", os.Args[1], err)
}
if unicode.IsSpace(r) && inword {
words++
inword = false
}
inword = unicode.IsLetter(r)
}
fmt.Printf("%q: %d words\n", os.Args[1], words)
}</code></pre>
<p>这样我们可以通过在输入文件和<code>readbyte</code> 之间插入<code>bufio.Reader</code>来提升性能。</p>
<h3>内存分析</h3>
<p><code>words</code> profile 还告诉了我们,<code>readbyte</code> 函数内部分配了一些东西。我们可以使用 pprof 进行研究。</p>
<pre><code class="go">defer profile.Start(profile.MemProfile).Stop()</code></pre>
<p>然后正常运行程序</p>
<pre><code>% go run main2.go moby.txt
2018/08/25 14:41:15 profile: memory profiling enabled (rate 4096), /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile312088211/mem.pprof
"moby.txt": 181275 words
2018/08/25 14:41:15 profile: memory profiling disabled, /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile312088211/mem.pprof</code></pre>
<p><img src="/img/bVbgMy8?w=2752&h=1920" alt="图片描述" title="图片描述"></p>
<p>正如我们所怀疑的那样,分配来自 <code>readbyte</code> -- 这并不复杂,只有三行代码:</p>
<pre><code class="go">func readbyte(r io.Reader) (rune, error) {
var buf [1]byte // allocation is here
_, err := r.Read(buf[:])
return rune(buf[0]), err
}</code></pre>
<p>我们将在下一节详细讨论为什么会发生这种情况,但目前我们看到的是,每个对<code>readbyte</code>的调用都在分配一个新的1字节长的数组,而这个数组正在堆上分配。</p>
<h4>分配对象 vs. 使用中的对象</h4>
<p>内存分析有两种选择,以 <code>go tool pprof</code> 工具的标识命名:</p>
<ul>
<li>
<code>-alloc_objects</code> 报告每次分配时的所在的地方</li>
<li>
<code>-inuse_objects</code> 报告被使用的地方,可以在 profile 文件的末尾找到</li>
</ul>
<p>为了说明这一点,这里有一个设计好的程序,它将以一种受控的方式分配一些内存。</p>
<pre><code class="go">// ensure y is live beyond the end of main.
var y []byte
func main() {
defer profile.Start(profile.MemProfile, profile.MemProfileRate(1)).Stop()
y = allocate(100000)
runtime.GC()
}
// allocate allocates count byte slices and returns the first slice allocated.
func allocate(count int) []byte {
var x [][]byte
for i := 0; i < count; i++ {
x = append(x, makeByteSlice())
}
return x[0]
}
// makeByteSlice returns a byte slice of a random length in the range [0, 16384).
func makeByteSlice() []byte {
return make([]byte, rand.Intn(1<<14))
}</code></pre>
<p>该程序使用 profile 包进行标注,我们将内存采集速率设置为1——也就是说,每个分配都记录堆栈跟踪。这大大降低了程序的速度,但你很快就会明白为什么。</p>
<pre><code>% go run main.go
2018/08/25 15:22:05 profile: memory profiling enabled (rate 1), /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile730812803/mem.pprof
2018/08/25 15:22:05 profile: memory profiling disabled, /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile730812803/mem.pprof</code></pre>
<p>让我们看一下分配对象的图,这是默认值,并显示了导致 profile 文件中每个对象分配的调用图。</p>
<pre><code>% go tool pprof -web -alloc_objects /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile891268605/mem.pprof</code></pre>
<p><img src="/img/bVbgMzu?w=2612&h=1856" alt="图片描述" title="图片描述"><br>不足为奇的是超过 99% 的分配都是在 <code>makeByteSlice</code>内部进行的。现在让我们 换用<code>-inuse_objects</code>查看 profile</p>
<pre><code>% go tool pprof -web -inuse_objects /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile891268605/mem.pprof</code></pre>
<p><img src="/img/bVbgMzz?w=2304&h=1928" alt="图片描述" title="图片描述"><br>我们看到的不是在 profile 期间分配的对象,而是在 profile 获取时仍在使用的对象——这忽略了垃圾收集器回收的对象的堆栈跟踪。</p>
<h3>阻塞分析 - 例1</h3>
<p>我们将展示最后一种分析 - 阻塞分析。我们使用 <code>net/http</code>包中<code>ClientServer</code>的基准测试。</p>
<pre><code>% go test -run=XXX -bench=ClientServer$ -blockprofile=/tmp/block.p net/http
% go tool pprof -web /tmp/block.p</code></pre>
<p><img src="/img/bVbgMzI?w=1234&h=2200" alt="图片描述" title="图片描述"></p>
<h3>帧指针</h3>
<p>Go 1.7 已经发布,并且与 amd64 的新编译器一起,编译器现在默认启用帧指针。</p>
<p>帧指针是一个始终指向当前栈帧顶部的寄存器。</p>
<p>它支持使用 <code>gdb(1)</code> 和 <code>perf(1)</code> 等工具解析 Go 调用栈</p>
<p>详情可以参考下面的扩展阅读</p>
<h3>进一步阅读:</h3>
<ul>
<li>七种方式分析 Go 程序 (幻灯片)[<a href="https://link.segmentfault.com/?enc=e1dD7H0XYJVqIsIXn1Io%2BA%3D%3D.8NCuY2j%2BopFbrPyzEfOCe0H5Nr%2BPx4A74Z6tIpA9iDLM6tBfdBD4cuNSuPScB0l4YnCSpb02zOfap5URD1O9xdX4LBTKTthgX%2FUZ9U3Tae8%3D" rel="nofollow">https://talks.godoc.org/githu...</a>]</li>
<li>七种方式分析 Go 程序 (视频,30分钟)[<a href="https://link.segmentfault.com/?enc=dj%2BdsFuNCfFKg91Eb4XsSA%3D%3D.S7CAsOGSxT%2BRDBm7TiEMawrrXHq7BM14JnlPZYhkddsTsujP8Sy%2F3BlT0nm1NlPq" rel="nofollow">https://www.youtube.com/watch...</a>]</li>
<li>七种方式分析 Go 程序 (网络直播,60分钟)[ <a href="https://link.segmentfault.com/?enc=mFT1kNw0GHknsHJp425nFg%3D%3D.VrarRr5HxvoPyQ2OcP1BKV5QtvTbjgpDt35We5ePzP8QvkoEwnuspbYgJNyxtDvBKKkkyO2tVlKOIdSDpXOnmzufkt%2FhQZMKa1y1J68z2P0%3D" rel="nofollow">https://www.bigmarker.com/rem...</a>]</li>
</ul>
2. Go 性能调优之 —— 编译优化
https://segmentfault.com/a/1190000016354799
2018-09-11T15:52:24+08:00
2018-09-11T15:52:24+08:00
ronniesong
https://segmentfault.com/u/sxssxs
32
<blockquote>原文链接:<a href="https://link.segmentfault.com/?enc=H%2FVnfupxGZvBLeLdbWcPhQ%3D%3D.TZ1HLP%2B9tTEJdM3Ls7Mb%2BL96px%2ByltMge%2Fwegem%2Bt9IS2%2Bj5BRwlFjwoYjeMubqp" rel="nofollow">https://github.com/sxs2473/go...</a><br>本文使用 <a href="https://link.segmentfault.com/?enc=tFKte3kiTEBnwy9xffMgjQ%3D%3D.bLwAQE3NR84KnZ5mPwtjmlx9pKuxqQA6%2FMSNZydOghxKYLi219go3GTAx8C2OXhFOiL3SMeWvJgcbf6rJdNvMw%3D%3D" rel="nofollow">Creative Commons Attribution-ShareAlike 4.0 International</a> 协议进行授权许可。</blockquote>
<h2>编译优化</h2>
<p>本节介绍Go编译器执行的三个重要优化。</p>
<ul>
<li>逃逸分析</li>
<li>内联</li>
<li>死码消除</li>
</ul>
<h3>Go 编译器的历史</h3>
<p>Go 编译器在2007年左右开始作为 Plan9 编译器工具链的一个分支。当时的编译器与 Aho 和 Ullman 的 <a href="https://link.segmentfault.com/?enc=VU4M%2BZbdV9Eco8gQVd6cXg%3D%3D.u79oQyD%2Fw2gE8GgXotbVViU%2FWFVRjn3fQbspMapecva%2BKfZgDI57%2FmUncHzLZ7yAg53q9IZgsQQdotn9J%2BgatEI7tHWW4ZsMrE77QRcF8DM%3D" rel="nofollow"><em>Dragon Book</em></a> 非常相似。</p>
<p>2015年,当时的 Go 1.5 编译器 <a href="https://link.segmentfault.com/?enc=%2F6dJrbSV1hO4e48CN2ZW1Q%3D%3D.5bFieR9SLg%2FY%2Bka2stYYJoXmRtStDBu2s8MYquL%2BAD4%3D" rel="nofollow">从 C 机械地翻译成 Go</a>。</p>
<p>一年后,Go 1.7 引入了一个基于 <a href="https://link.segmentfault.com/?enc=%2Fu9EwAoNkpR504lyltj2yQ%3D%3D.eFEfPTF8lF2FWuaWFaeHafNZRfhoSVRJgEOolO0OWVtBx6cwtK1fMHX2OZWwoOOMqW4eRdtf5I8nFpdxIDS2Qg%3D%3D" rel="nofollow">SSA</a> 技术的 <a href="https://link.segmentfault.com/?enc=AvDMqZpW4dG0%2Bs4uuhL8gg%3D%3D.GhXNCJu75cbYMFI6f0VIojJhxcJUx9a7fJl5DIQQG84%3D" rel="nofollow">新编译器后端</a> ,取代了之前的 Plan 9风格的代码。这个新的后端为泛型和体系结构特定的优化提供了许多可能。</p>
<h3>逃逸分析</h3>
<p>我们要讨论的第一个优化是逃逸分析。</p>
<p>为了说明逃逸分析,首先让我们来回忆一下在 <a href="https://link.segmentfault.com/?enc=n47WHuMlko6erMJIo0PmPw%3D%3D.E9DETVGcLQhHY0btVBo91GWj1gU3rfgbePR1kjlBDm0%3D" rel="nofollow">Go spec</a> 中没有提到堆和栈,它只提到 Go 语言是有垃圾回收的,但也没有说明如何是如何实现的。</p>
<p>一个遵循 Go spec 的 Go 实现可以将每个分配操作都在堆上执行。这会给垃圾回收器带来很大压力,但这样做是绝对错误的 -- 多年来,gccgo对逃逸分析的支持非常有限,所以才导致这样做被认为是有效的。</p>
<p>然而,goroutine 的栈是作为存储局部变量的廉价场所而存在;没有必要在栈上执行垃圾回收。因此,在栈上分配内存也是更加安全和有效的。</p>
<p>在一些语言中,如<code>C</code>和<code>C++</code>,在栈还是堆上分配内存由程序员手动决定——堆分配使用<code>malloc</code> 和<code>free</code>,而栈分配通过<code>alloca</code>。错误地使用这种机制会是导致内存错误的常见原因。</p>
<p>在 Go 中,如果一个值超过了函数调用的生命周期,编译器会自动将之移动到堆中。我们管这种现象叫:该值逃逸到了堆。</p>
<pre><code class="go">type Foo struct {
a, b, c, d int
}
func NewFoo() *Foo {
return &Foo{a: 3, b: 1, c: 4, d: 7}
}</code></pre>
<p>在这个例子中,<code>NewFoo</code> 函数中分配的 <code>Foo</code> 将被移动到堆中,因此在 <code>NewFoo</code> 返回后 <code>Foo</code> 仍然有效。</p>
<p>这是从早期的 Go 就开始有的。与其说它是一种优化,不如说它是一种自动正确性特性。无法在 Go 中返回栈上分配的变量的地址。</p>
<p>同时编译器也可以做相反的事情;它可以找到堆上要分配的东西,并将它们移动到栈上。</p>
<h3>逃逸分析 - 例1</h3>
<p>让我们来看下面的例子:</p>
<pre><code class="go">// Sum 函数返回 0-100 的整数之和
func Sum() int {
const count = 100
numbers := make([]int, count)
for i := range numbers {
numbers[i] = i + 1
}
var sum int
for _, i := range numbers {
sum += i
}
return sum
}</code></pre>
<p><code>Sum</code> 将 0-100 的 <code>ints</code>型数字相加并返回结果。</p>
<p>因为 <code>numbers</code> 切片仅在 <code>Sum</code>函数内部使用,编译器将在栈上存储这100个整数而不是堆。也没有必要对 <code>numbers</code>进行垃圾回收,因为它会在 <code>Sum</code> 返回时自动释放。</p>
<h3>调查逃逸分析</h3>
<p>证明它!</p>
<p>要打印编译器关于逃逸分析的决策,请使用<code>-m</code>标志。</p>
<pre><code>% go build -gcflags=-m examples/esc/sum.go
# command-line-arguments
examples/esc/sum.go:8:17: Sum make([]int, count) does not escape
examples/esc/sum.go:22:13: answer escapes to heap
examples/esc/sum.go:22:13: main ... argument does not escape</code></pre>
<p>第8行显示编译器已正确推断 <code>make([]int, 100)</code>的结果不会逃逸到堆。</p>
<p>第22行显示<code>answer</code>逃逸到堆的原因是<code>fmt.Println</code>是一个可变函数。 可变参数函数的参数被装入一个切片,在本例中为<code>[]interface{}</code>,所以会将<code>answer</code>赋值为接口值,因为它是通过调用<code>fmt.Println</code>引用的。 从 Go 1.6(可能是)开始,垃圾收集器需要通过接口传递的所有值都是指针,编译器看到的是这样的:</p>
<pre><code>var answer = Sum()
fmt.Println([]interface{&answer}...)</code></pre>
<p>我们可以使用标识 <code>-gcflags="-m -m"</code> 来确定这一点。会返回:</p>
<pre><code>examples/esc/sum.go:22:13: answer escapes to heap
examples/esc/sum.go:22:13: from ... argument (arg to ...) at examples/esc/sum.go:22:13
examples/esc/sum.go:22:13: from *(... argument) (indirection) at examples/esc/sum.go:22:13
examples/esc/sum.go:22:13: from ... argument (passed to call[argument content escapes]) at examples/esc/sum.go:22:13
examples/esc/sum.go:22:13: main ... argument does not escape</code></pre>
<p>总之,不要担心第22行,这对我们的讨论并不重要。</p>
<h3>逃逸分析 - 例2</h3>
<p>这个例子是我们模拟的。 它不是真正的代码,只是一个例子。</p>
<pre><code class="go">package main
import "fmt"
type Point struct{ X, Y int }
const Width = 640
const Height = 480
func Center(p *Point) {
p.X = Width / 2
p.Y = Height / 2
}
func NewPoint() {
p := new(Point)
Center(p)
fmt.Println(p.X, p.Y)
}</code></pre>
<p><code>NewPoint</code> 创建了一个 <code>*Point</code> 指针值 <code>p</code>。 我们将<code>p</code>传递给<code>Center</code>函数,该函数将点移动到屏幕中心的位置。最后我们打印出 <code>p.X</code> 和 <code>p.Y </code> 的值。</p>
<pre><code>% go build -gcflags=-m examples/esc/center.go
# command-line-arguments
examples/esc/center.go:10:6: can inline Center
examples/esc/center.go:17:8: inlining call to Center
examples/esc/center.go:10:13: Center p does not escape
examples/esc/center.go:18:15: p.X escapes to heap
examples/esc/center.go:18:20: p.Y escapes to heap
examples/esc/center.go:16:10: NewPoint new(Point) does not escape
examples/esc/center.go:18:13: NewPoint ... argument does not escape
# command-line-arguments</code></pre>
<p>尽管<code>p</code>是使用<code>new</code>分配的,但它不会存储在堆上,因为<code>Center</code>被内联了,所以没有<code>p</code>的引用会逃逸到<code>Center</code>函数。</p>
<h3>内联</h3>
<p>在 Go 中,函数调用有固定的开销;栈和抢占检查。</p>
<p>硬件分支预测器改善了其中的一些功能,但就功能大小和时钟周期而言,这仍然是一个成本。</p>
<p>内联是避免这些成本的经典优化方法。</p>
<p>内联只对叶子函数有效,叶子函数是不调用其他函数的。这样做的理由是:</p>
<ul>
<li>如果你的函数做了很多工作,那么前序开销可以忽略不计。</li>
<li>另一方面,小函数为相对较少的有用工作付出固定的开销。这些是内联目标的功能,因为它们最受益。</li>
</ul>
<p>还有一个原因就是严重的内联会使得堆栈信息更加难以跟踪。</p>
<h3>内联 - 例1</h3>
<pre><code class="go">func Max(a, b int) int {
if a > b {
return a
}
return b
}
func F() {
const a, b = 100, 20
if Max(a, b) == b {
panic(b)
}
}</code></pre>
<p>我们再次使用 <code>-gcflags = -m</code> 标识来查看编译器优化决策。</p>
<pre><code>% go build -gcflags=-m examples/max/max.go
# command-line-arguments
examples/max/max.go:3:6: can inline Max
examples/max/max.go:12:8: inlining call to Max</code></pre>
<p>编译器打印了两行信息:</p>
<ul>
<li>首先第3行,<code>Max</code>的声明告诉我们它可以内联</li>
<li>其次告诉我们,<code>Max</code>的主体已经内联到第12行调用者中。</li>
</ul>
<h4>内联是什么样的?</h4>
<p>编译 <code>max.go</code> 然后我们看看优化版本的 <code>F()</code> 变成什么样了。</p>
<pre><code>% go build -gcflags=-S examples/max/max.go 2>&1 | grep -A5 '"".F STEXT'
"".F STEXT nosplit size=1 args=0x0 locals=0x0
0x0000 00000 (/Users/dfc/devel/gophercon2018-performance-tuning-workshop/4-compiler-optimisations/examples/max/max.go:10) TEXT "".F(SB), NOSPLIT, $0-0
0x0000 00000 (/Users/dfc/devel/gophercon2018-performance-tuning-workshop/4-compiler-optimisations/examples/max/max.go:10) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (/Users/dfc/devel/gophercon2018-performance-tuning-workshop/4-compiler-optimisations/examples/max/max.go:10) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (<unknown line number>) RET
0x0000 c3</code></pre>
<p>一旦<code>Max</code>被内联到这里,这就是F的主体 - 这个函数什么都没干。我知道屏幕上有很多没用的文字,但是相信我的话,唯一发生的就是<code>RET</code>。实际上<code>F</code>变成了:</p>
<pre><code class="go">func F() {
return
}</code></pre>
<p><em>注意</em> : 利用 <code>-S</code> 的输出并不是进入二进制文件的最终机器码。链接器在最后的链接阶段进行一些处理。像<code>FUNCDATA</code>和<code>PCDATA</code>这样的行是垃圾收集器的元数据,它们在链接时移动到其他位置。 如果你正在读取<code>-S</code>的输出,请忽略<code>FUNCDATA</code>和<code>PCDATA</code>行;它们不是最终二进制的一部分。</p>
<h4>调整内联级别</h4>
<p>使用<code>-gcflags=-l</code>标识调整内联级别。有些令人困惑的是,传递一个<code>-l</code>将禁用内联,两个或两个以上将在更激进的设置中启用内联。</p>
<ul>
<li>
<code>-gcflags=-l</code>,禁用内联。</li>
<li>什么都不做,常规的内联</li>
<li>
<code>-gcflags='-l -l'</code> 内联级别2,更积极,可能更快,可能会制作更大的二进制文件。</li>
<li>
<code>-gcflags='-l -l -l'</code> 内联级别3,再次更加激进,二进制文件肯定更大,也许更快,但也许会有 bug。</li>
<li>
<code>-gcflags=-l=4</code> (4个 <code>-l</code>) 在 Go 1.11 中将支持实验性的 <a href="https://link.segmentfault.com/?enc=Dln2yOHiwlRdV0nBv2f6jw%3D%3D.khvO82BoAtVxda8SnGkHp4ZpyFA6zAWjmawn4qu3hcMH8zKEhXSeGEHndW3KKvzA095TeHFkHDA9gM%2Be0Qh9yH%2FE71z1MG3e4J9uSPcdds0%3D" rel="nofollow">中间栈内联优化</a>。</li>
</ul>
<h3>死码消除</h3>
<p>为什么<code>a</code>和<code>b</code>是常数很重要?</p>
<p>为了理解发生了什么,让我们看一下编译器在把<code>Max</code>内联到<code>F</code>中的时候看到了什么。我们不能轻易地从编译器中获得这个,但是直接手动完成它。</p>
<p>Before:</p>
<pre><code class="go">func Max(a, b int) int {
if a > b {
return a
}
return b
}
func F() {
const a, b = 100, 20
if Max(a, b) == b {
panic(b)
}
}</code></pre>
<p>After:</p>
<pre><code class="go">func F() {
const a, b = 100, 20
var result int
if a > b {
result = a
} else {
result = b
}
if result == b {
panic(b)
}
}</code></pre>
<p>因为<code>a</code>和<code>b</code>是常量,所以编译器可以在编译时证明分支永远不会是假的;<code>100</code>总是大于<code>20</code>。因此它可以进一步优化 <code>F</code> 为</p>
<pre><code class="go">func F() {
const a, b = 100, 20
var result int
if true {
result = a
} else {
result = b
}
if result == b {
panic(b)
}
}</code></pre>
<p>既然分支的结果已经知道了,那么结果的内容也就知道了。这叫做分支消除。</p>
<pre><code class="go">func F() {
const a, b = 100, 20
const result = a
if result == b {
panic(b)
}
}</code></pre>
<p>现在分支被消除了,我们知道结果总是等于<code>a</code>,并且因为<code>a</code>是常数,我们知道结果是常数。 编译器将此证明应用于第二个分支</p>
<pre><code class="go">func F() {
const a, b = 100, 20
const result = a
if false {
panic(b)
}
}</code></pre>
<p>并且再次使用分支消除,<code>F</code>的最终形式减少成这样。</p>
<pre><code class="go">func F() {
const a, b = 100, 20
const result = a
}</code></pre>
<p>最后就变成</p>
<pre><code class="go">func F() {
}</code></pre>
<h4>死码消除(续)</h4>
<p>分支消除是一种被称为死码消除的优化。实际上,使用静态证明来表明一段代码永远不可达,通常称为死代码,因此它不需要在最终的二进制文件中编译、优化或发出。</p>
<p>我们发现死码消除与内联一起工作,以减少循环和分支产生的代码数量,这些循环和分支被证明是不可到达的。</p>
<p>你可以利用这一点来实现昂贵的调试,并将其隐藏起来</p>
<pre><code>const debug = false </code></pre>
<p>结合构建标记,这可能非常有用。</p>
<h4>进一步阅读</h4>
<ul>
<li><a href="https://link.segmentfault.com/?enc=ovqKRX0YY1cMyv8amIhEuw%3D%3D.PckfbyDR5%2FXmKCqT%2F7TDyFqkhgyDmeX9esg8neeX6v41U8xppAQdseEFaml1508TW90rQz8m9TZ3VPjAykaMKArYJ4Rua0HBNWvCqd95x4mk2Fo%2BQwEDAO169%2Bl4hPI%2F" rel="nofollow">Using // +build to switch between debug and release builds</a></li>
<li><a href="https://link.segmentfault.com/?enc=IfOPCGVCi0YBhMHyonHhFA%3D%3D.WqC0%2BWJ6wyyn%2BaWBLwnt3a7gb8nn%2BoxJ7rDt8aMOVlvUYAPHZ%2BOYzwtdgbfb3oWjVRMM8OvzZZD3dCCOQ1xVouKNh%2F3WRvT4sJpA8yEWCBqSEWZC2wuYwh1jSS%2FMPK2H" rel="nofollow">How to use conditional compilation with the go build tool</a></li>
</ul>
<h4>编译器标识练习</h4>
<p>编译器标识提供如下:</p>
<pre><code>go build -gcflags=$FLAGS</code></pre>
<p>研究以下编译器功能的操作:</p>
<ul>
<li>
<code>-S</code> 打印正在编译的包的汇编代码</li>
<li>
<code>-l</code> 控制内联行为; <code>-l</code> 禁止内联, <code>-l -l</code> 增加<code>-l</code>(更多<code>-l</code>会增加编译器对代码内联的强度)。试验编译时间,程序大小和运行时间的差异。</li>
<li>
<code>-m</code> 控制优化决策的打印,如内联,逃逸分析。<code>-m</code>打印关于编译器的想法的更多细节。</li>
<li>
<code>-l -N</code> 禁用所有优化。</li>
</ul>
<p><em>注意</em> : If you find that subsequent runs of <code>go build ...</code> produce no output, delete the <code>./max</code> binary in your working directory.</p>
1. Go 性能调优之 —— 基准测试
https://segmentfault.com/a/1190000016354758
2018-09-11T15:51:14+08:00
2018-09-11T15:51:14+08:00
ronniesong
https://segmentfault.com/u/sxssxs
27
<blockquote>原文链接:<a href="https://link.segmentfault.com/?enc=awR31tcWjpV9x4H%2B4lsPrA%3D%3D.MQ52J%2FPPEGYE2tADCoDgdKK4%2BQIulMXGpOjIdN%2FJvkBru2VTsV4du5rsn3EfVkpI" rel="nofollow">https://github.com/sxs2473/go...</a><br>本文使用 <a href="https://link.segmentfault.com/?enc=oBr2ktd0rq5KZluI%2B1J4Xw%3D%3D.0gCKUM5BWDmTPGuZ5OBqDOGOpXkd%2B7anPa8zwISiWV4FJPNWnSEcVvbC5RO7T597" rel="nofollow">Creative Commons Attribution-ShareAlike 4.0 International</a> 协议进行授权许可。</blockquote>
<h2>基准测试</h2>
<p>本节重点讨论如何使用 Go 测试框架构建一个有效的基准测试,并提供一些实用的技巧来避免性能缺陷。</p>
<h3>基准测试的基本规则</h3>
<p>在进行基准测试之前,我们必须要有一个稳定的环境来获得可重现的结果。</p>
<ul>
<li>机器必须是空闲的——不要运行在共享硬件上,在长时间运行基准测试时不要进行其他操作</li>
<li>注意节电和热缩放(主要指 CPU 受温度影响导致频率不稳定)</li>
<li>避免虚拟机和共享云托管; 它们太乱,无法进行一致的测量。</li>
</ul>
<p>如果你负担得起,最好购买专用的性能测试硬件。并禁用所有电源管理和热缩放,保持机器上的软件版本不变。</p>
<p>对于其他人,请使用前后样本并多次运行它们以获得一致的结果。</p>
<h3>使用测试包进行基准测试</h3>
<p><code>testing</code> 包已经内置了支持基准测试的能力. 比如你有一个简单的函数:</p>
<pre><code class="go">// 此函数计算斐波那契数列中第 N 个数字
func Fib(n int) int {
switch n {
case 0:
return 0
case 1:
return 1
default:
return Fib(n-1) + Fib(n-2)
}
}</code></pre>
<p>我们可以使用 <code>testing</code> 包以如下形式为此函数写一个基准测试。基准测试函数也写在以 <code>_test.go</code> 结尾的文件里,它和<code>test</code>函数共存.</p>
<pre><code class="go">func BenchmarkFib20(b *testing.B) {
for n := 0; n < b.N; n++ {
Fib(20) // 运行 Fib 函数 N 次
}
}</code></pre>
<p>基准测试和普通单元测试类似。 唯一的区别是基准测试接收的参数是<code>*testing.B</code> 而不是 <code>*testing.T</code>。 这两种类型都实现了 <code>testing.TB</code> 接口,这个接口提供了一些比较常用的方法 <code>Errorf()</code>, <code>Fatalf()</code>, and <code>FailNow()</code>。</p>
<h4>运行包的基准测试</h4>
<p>因为基准测试使用<code>testing</code> 包,它们同样通过 go test 命令执行。但是,默认情况下,当你调用<code>go test</code>时,基准测试是不执行的。</p>
<p>要显式地执行基准测试请使用 <code>-bench</code> 标识。 <code>-bench</code> 接收一个与待运行的基准测试名称相匹配的正则表达式,因此,如果要运行包中所有的基准测试,最常见的方法是这样写 <code>-bench=.</code>。例如:</p>
<pre><code>% go test -bench=. ./examples/fib/
goos: darwin
goarch: amd64
BenchmarkFib20-8 30000 44514 ns/op
PASS
ok _/Users/dfc/devel/gophercon2018-performance-tuning-workshop/2-benchmarking/examples/fib 1.795s</code></pre>
<p><em>注意</em> : <code>go test</code> 会在运行基准测试之前之前执行包里所有的单元测试,所有如果你的包里有很多单元测试,或者它们会运行很长时间,你也可以通过 <code>go test</code> 的<code>-run</code> 标识排除这些单元测试,不让它们执行; 比如: <code>go test -run=^$</code>。</p>
<h4>基准测试的工作原理</h4>
<p>基准测试函数会被一直调用直到<code>b.N</code>无效,它是基准测试循环的次数</p>
<p><code>b.N</code> 从 1 开始,如果基准测试函数在1秒内就完成 (默认值),则 <code>b.N</code> 增加,并再次运行基准测试函数。</p>
<p><code>b.N</code> 在近似这样的序列中不断增加;1, 2, 3, 5, 10, 20, 30, 50, 100 等等。 基准框架试图变得聪明,如果它看到当<code>b.N</code>较小而且测试很快就完成的时候,它将让序列增加地更快。</p>
<p>看上面的例子, <code>BenchmarkFib20-8</code> 发现约 30000 次迭代只需要1秒钟。 From there the benchmark framework computed that </p>
<p><em>注意</em> : The <code>-8</code> 后缀和用于运行次测试的 <code>GOMAXPROCS</code> 值有关。 与<code>GOMAXPROCS</code>一样,此数字默认为启动时Go进程可见的CPU数。 你可以使用<code>-cpu</code>标识更改此值,可以传入多个值以列表形式来运行基准测试。</p>
<pre><code>% go test -bench=. -cpu=1,2,4 ./examples/fib/
goos: darwin
goarch: amd64
BenchmarkFib20 30000 44644 ns/op
BenchmarkFib20-2 30000 44504 ns/op
BenchmarkFib20-4 30000 44848 ns/op
PASS</code></pre>
<h4>提高基准测试的精度</h4>
<p><code>fib</code> 函数是一个模拟的例子 — 除非你编写 TechPower 服务器基准测试来验证,否则你的业务不太可能是你计算斐波那契数列中第20个数字的速度。 但是,基准确实展现了我认为有效的基准。</p>
<p>具体来说,当你的基准测试运行几千次迭代的时候,我们可以认为获得了一个每次运行的平均值,而如果基准测试只运行几十次,那么这个平均值很可能不稳定,也就不能说明问题。</p>
<p>要增加迭代次数,可以使用<code>-benchtime</code>标识增加运行时间,例如</p>
<pre><code>% go test -bench=. -benchtime=10s ./examples/fib/
goos: darwin
goarch: amd64
BenchmarkFib20-8 300000 44616 ns/op</code></pre>
<p>运行一个相同的基准测试,直到它到达<code>b.N</code>的值,运行时间超过10秒。当我们运行时间是10倍的时候,迭代次数也会增加到10倍。然而每一次执行的结果却没有什么变化,这正是我们所预期的。</p>
<p>如果你有一个基准测试,它运行数百万次或数十亿次迭代,每次操作的时间都在微秒或纳秒级,那么你可能会发现基准测试结果不稳定,因为热缩放、内存局部性、后台处理、gc活动等等。</p>
<p>对于每次操作是以10或个位数纳秒为单位计算的函数来说,指令重新排序和代码对齐的相对效应都将对结果产生影响。</p>
<p>可以使用<code>-count</code> 标识多次运行基准测试来解决这个问题:</p>
<pre><code>% go test -bench=Fib1 -count=10 ./examples/fib/
goos: darwin
goarch: amd64
BenchmarkFib1-8 2000000000 1.99 ns/op
BenchmarkFib1-8 1000000000 1.95 ns/op
BenchmarkFib1-8 2000000000 1.99 ns/op
BenchmarkFib1-8 2000000000 1.97 ns/op
BenchmarkFib1-8 2000000000 1.99 ns/op
BenchmarkFib1-8 2000000000 1.96 ns/op
BenchmarkFib1-8 2000000000 1.99 ns/op
BenchmarkFib1-8 2000000000 2.01 ns/op
BenchmarkFib1-8 2000000000 1.99 ns/op
BenchmarkFib1-8 1000000000 2.00 ns/op</code></pre>
<p>得出<code>Fib(1)</code>的基准测试在2纳秒左右,方差为正负2%. </p>
<p><em>提示</em> : 如果你发现需要针对特定的包调整不同的默认值,我建议使用<code>Makefile</code>中完成这些设定,这样每个想要运行基准测试的人都可以使用相同的配置进行编码。</p>
<h3>Benchstat</h3>
<p>在上一节中,我建议多次运行基准测试以获得更多的平均数据。对于任何基准测试来说,这都是一个很好的建议,因为测试过程会受到电源管理、后台进程和热管理的影响,这个问题我在本章的开头已经提到过。</p>
<p>下面我将介绍一个由 Russ Cox 编写的测试工具 <a href="https://link.segmentfault.com/?enc=MP9OoUGXn7%2B8yesiv4WVIw%3D%3D.GmNXwAEcm0sQIKpO%2BpF%2FuifJY07xty0OiafJWjCOXxB6%2BPyHMGpwGsxsaMBtKQFoBjLU28lDT%2FCdV7VwJpXI0g%3D%3D" rel="nofollow">benchstat</a></p>
<pre><code>% go get golang.org/x/perf/cmd/benchstat</code></pre>
<p>Benchstat 可以获取一组基准测试数据,并告诉你它的稳定性如何。以下是使用电池时的数据:</p>
<pre><code>% go test -bench=Fib20 -count=10 ./examples/fib/ | tee old.txt
goos: darwin
goarch: amd64
BenchmarkFib20-8 30000 46295 ns/op
BenchmarkFib20-8 30000 41589 ns/op
BenchmarkFib20-8 30000 42204 ns/op
BenchmarkFib20-8 30000 43923 ns/op
BenchmarkFib20-8 30000 44339 ns/op
BenchmarkFib20-8 30000 45340 ns/op
BenchmarkFib20-8 30000 45754 ns/op
BenchmarkFib20-8 30000 45373 ns/op
BenchmarkFib20-8 30000 44283 ns/op
BenchmarkFib20-8 30000 43812 ns/op
PASS
ok _/Users/dfc/devel/gophercon2018-performance-tuning-workshop/2-benchmarking/examples/fib 17.865s
% benchstat old.txt
name time/op
Fib20-8 44.3µs ± 6%</code></pre>
<p><code>benchstat</code> 告诉我们,平均值为44.3微秒,样本间的波动区间为正负 6%。 这对电池电量来说在意料之中。</p>
<ul>
<li>第一次运行是最慢的,因为操作系统的 CPU 时钟频率已经降低以节省功耗。</li>
<li>接下来的两次运行是最快的,因为操作系统识别到有一个较大的工作负载加入,就会提高 CPU 时钟速度,以尽快通过工作。</li>
<li>剩下的是当 CPU 高速运转发热,因为功耗导致又被限制,所以又慢了下来。</li>
</ul>
<h3>对比标准 benchmarks 和 benchstat</h3>
<p>确定两组基准测试结果之间的差异可能是单调乏味且容易出错的。 Benchstat 可以帮助我们解决这个问题。</p>
<p><em>提示</em> : 保存基准运行的输出很有用,但你也可以保存生成它的二进制文件。 为此,请使用<code>-c</code>标志来保存测试二进制文件;我经常将这个二进制文件从<code>.test</code>重命名为<code>.golden</code>。</p>
<pre><code>% go test -c
% mv fib.test fib.golden </code></pre>
<h3>提升 <code>Fib</code> 性能</h3>
<p>先前的<code>Fib</code>函数对斐波纳契数列中的第0和第1个数字进行了硬编码。 之后,代码以递归方式调用自身。 我们将在后边讨论递归的代价,但目前,假设它有代价,特别当我们的算法是指数级复杂度的时候。</p>
<p>要解决这个问题,最简单的方法就是硬编码斐波那契数列中的另一个数字,将每次调用的深度减少一个。</p>
<pre><code class="go">func Fib(n int) int {
switch n {
case 0:
return 0
case 1:
return 1
case 2:
return 1
default:
return Fib(n-1) + Fib(n-2)
}
}</code></pre>
<p>为了比较我们的新版本,我们编译了一个新的测试二进制文件并对它们都进行了基准测试,并使用<code>benchstat</code>对输出进行比较。</p>
<pre><code>% go test -c
% ./fib.golden -test.bench=. -test.count=10 > old.txt
% ./fib.test -test.bench=. -test.count=10 > new.txt
% benchstat old.txt new.txt
name old time/op new time/op delta
Fib20-8 44.3µs ± 6% 25.6µs ± 2% -42.31% (p=0.000 n=10+10)</code></pre>
<p>比较基准测试时需要检查三件事</p>
<ul>
<li>新老两次的方差。1-2% 是不错的, 3-5% 也还行,但是大于5%的话,可能不太可靠。 在比较一方具有高差异的基准时要小心,您可能看不到改进。</li>
<li>p值。p值低于0.05是比较好的情况,大于0.05则意味着基准测试结果可能没有统计学意义。</li>
<li>样本不足。benchstat将报告它认为有效的新旧样本的数量,有时你可能只发现9个报告,即使你设置了<code>-count=10</code>。拒绝率小于10%一般是没问题的,而高于10%可能表明你的设置是不稳定的,也可能是比较的样本太少了。</li>
</ul>
<h3>避免基准测试的启动成本</h3>
<p>有时候每次基准测试运行前都有一些初始化操作。 <code>b.ResetTimer()</code>将让你跳过这些运行时间。</p>
<pre><code class="go">func BenchmarkExpensive(b *testing.B) {
boringAndExpensiveSetup()
b.ResetTimer() // HL
for n := 0; n < b.N; n++ {
// 被测试的功能
}
}</code></pre>
<p>如果每次循环迭代内部都有一些高成本的其他逻辑,请使用<code>b.StopTimer()</code>和<code>b.StartTimer()</code>来暂停基准计时器。</p>
<pre><code class="go">func BenchmarkComplicated(b *testing.B) {
for n := 0; n < b.N; n++ {
b.StopTimer() // HL
complicatedSetup()
b.StartTimer() // HL
// 被测试的功能
}
}</code></pre>
<h3>内存分配的基准测试</h3>
<p>分配计数和大小与基准测试的执行时间密切相关。 你可以告诉测试框架记录被测代码所做的分配数量。</p>
<pre><code class="go">func BenchmarkRead(b *testing.B) {
b.ReportAllocs()
for n := 0; n < b.N; n++ {
// 被测试的功能
}
}</code></pre>
<p>以下是使用bufio软件包基准测试的示例:</p>
<pre><code>% go test -run=^$ -bench=. bufio
goos: darwin
goarch: amd64
pkg: bufio
BenchmarkReaderCopyOptimal-8 20000000 103 ns/op
BenchmarkReaderCopyUnoptimal-8 10000000 159 ns/op
BenchmarkReaderCopyNoWriteTo-8 500000 3644 ns/op
BenchmarkReaderWriteToOptimal-8 5000000 344 ns/op
BenchmarkWriterCopyOptimal-8 20000000 98.6 ns/op
BenchmarkWriterCopyUnoptimal-8 10000000 131 ns/op
BenchmarkWriterCopyNoReadFrom-8 300000 3955 ns/op
BenchmarkReaderEmpty-8 2000000 789 ns/op 4224 B/op 3 allocs/op
BenchmarkWriterEmpty-8 2000000 683 ns/op 4096 B/op 1 allocs/op
BenchmarkWriterFlush-8 100000000 17.0 ns/op 0 B/op 0 allocs/op</code></pre>
<p><em>注意</em> : 想对所有基准测试都生效,你也可以使用<code>go test -benchmem</code>标识。</p>
<pre><code>% go test -run=^$ -bench=. -benchmem bufio
goos: darwin
goarch: amd64
pkg: bufio
BenchmarkReaderCopyOptimal-8 20000000 93.5 ns/op 16 B/op 1 allocs/op
BenchmarkReaderCopyUnoptimal-8 10000000 155 ns/op 32 B/op 2 allocs/op
BenchmarkReaderCopyNoWriteTo-8 500000 3238 ns/op 32800 B/op 3 allocs/op
BenchmarkReaderWriteToOptimal-8 5000000 335 ns/op 16 B/op 1 allocs/op
BenchmarkWriterCopyOptimal-8 20000000 96.7 ns/op 16 B/op 1 allocs/op
BenchmarkWriterCopyUnoptimal-8 10000000 124 ns/op 32 B/op 2 allocs/op
BenchmarkWriterCopyNoReadFrom-8 500000 3219 ns/op 32800 B/op 3 allocs/op
BenchmarkReaderEmpty-8 2000000 748 ns/op 4224 B/op 3 allocs/op
BenchmarkWriterEmpty-8 2000000 662 ns/op 4096 B/op 1 allocs/op
BenchmarkWriterFlush-8 100000000 16.9 ns/op 0 B/op 0 allocs/op
PASS
ok bufio 20.366s</code></pre>
<h4>注意编译优化</h4>
<p>这个例子来自 <a href="https://link.segmentfault.com/?enc=nTKbl5iosc4C7RFLqQo9SA%3D%3D.qVMUsCiyqYguAHT1m8IgIDs7ksdLpbky2jdtgD8zZ5Vrij01ecVfolYUbAtuy7w9C1dG1sVTTUXr6EtJba0uFg%3D%3D" rel="nofollow">issue 14813</a>。</p>
<pre><code class="go">const m1 = 0x5555555555555555
const m2 = 0x3333333333333333
const m4 = 0x0f0f0f0f0f0f0f0f
const h01 = 0x0101010101010101
func popcnt(x uint64) uint64 {
x -= (x >> 1) & m1
x = (x & m2) + ((x >> 2) & m2)
x = (x + (x >> 4)) & m4
return (x * h01) >> 56
}
func BenchmarkPopcnt(b *testing.B) {
for i := 0; i < b.N; i++ {
popcnt(uint64(i))
}
}</code></pre>
<p>你觉得这个基准测试会有多快?让我们来看看。</p>
<pre><code>% go test -bench=. ./examples/popcnt/
goos: darwin
goarch: amd64
BenchmarkPopcnt-8 2000000000 0.30 ns/op
PASS</code></pre>
<p>0.3 纳秒,这基本上是一个时钟周期。即使假设CPU每个时钟周期内会执行多条指令,这个数字似乎也不合理地低。 发生了什么?</p>
<p>要了解发生了什么,我们必须看看benchmark下的函数popcnt。 popcnt是一个叶子函数 - 它不调用任何其他函数 - 因此编译器可以内联它。</p>
<p>因为函数是内联的,所以编译器现在可以看到它没有副作用。 popcnt不会影响任何全局变量的状态。 这样,调用就被消除了。 这是编译器看到的:</p>
<pre><code class="go">func BenchmarkPopcnt(b *testing.B) {
for i := 0; i < b.N; i++ {
// 优化了
}
}</code></pre>
<p>在所有版本的Go编译器上,仍然会生成循环。 但是英特尔CPU非常擅长优化循环,尤其是空循环。</p>
<h4>优化是一件好事</h4>
<p>需要去掉的是,通过删除不必要的计算使真正的代码快速运行的优化,与删除没有明显副作用的基准测试的优化是相同的。</p>
<p>随着Go编译器的改进,这只会变得更加普遍。</p>
<h4>修复基准测试</h4>
<p>要修复此基准测试,我们必须确保编译器无法检验<code>BenchmarkPopcnt</code>的主体不会导致全局状态发生变化。</p>
<pre><code class="go">var Result uint64
func BenchmarkPopcnt(b *testing.B) {
var r uint64
for i := 0; i < b.N; i++ {
r = popcnt(uint64(i))
}
Result = r
}</code></pre>
<p>这是确保编译器无法优化循环体的推荐方法。</p>
<p>首先,我们通过将调用<code>popcnt</code>的结果存储在<code>r</code>中。 然后,当测试基准结束时,<code>r</code>在<code>BenchmarkPopcnt</code>的范围内被声明,<code>r</code>的结果对于程序的另一部分是不可见的,所以最终,我们将<code>r</code>值赋给包级别的公共变量<code>Result</code>。</p>
<p>因为<code>Result</code>是公共的,所以编译器无法证明导入此类的另一个包将无法看到<code>Result</code>随时间变化的值,因此它无法优化导致其赋值的任何操作。</p>
<h3>错误的基准测试</h3>
<p><code>for</code> 循环对基准测试的执行非常重要</p>
<p>下面是两个错误的的基准测试例子:</p>
<pre><code class="go">func BenchmarkFibWrong(b *testing.B) {
Fib(b.N)
}</code></pre>
<pre><code class="go">func BenchmarkFibWrong2(b *testing.B) {
for n := 0; n < b.N; n++ {
Fib(n)
}
}</code></pre>
<p>结果是,它们会一直执行下去</p>
<h3>分析基准测试的结果</h3>
<p><code>testing</code>包内置了支持生成CPU,内存和块的profile文件。</p>
<ul>
<li>
<code>-cpuprofile=$FILE</code> 将 CPU 分析结果写入 <code>$FILE</code>.</li>
<li>
<code>-memprofile=$FILE</code> 将内存分析结果写入 <code>$FILE</code>, <code>-memprofilerate=N</code> 调整记录速率为 <code>1/N</code>.</li>
<li>
<code>-blockprofile=$FILE</code>, 将块分析结果写入 <code>$FILE</code>.</li>
</ul>
<p>使用这些标识中的任何一个同时都会保留二进制文件。</p>
<pre><code>% go test -run=XXX -bench=. -cpuprofile=c.p bytes
% go tool pprof c.p</code></pre>
Go Reflect 高级实践
https://segmentfault.com/a/1190000016230264
2018-08-31T20:25:03+08:00
2018-08-31T20:25:03+08:00
ronniesong
https://segmentfault.com/u/sxssxs
41
<blockquote><ol>
<li>
<a href="https://link.segmentfault.com/?enc=h1D1eQruRg1kPLZatxp9NA%3D%3D.6Ints8djzqdlyQB3fiPRV6bBpM3Jja%2BJ4wFneKAlb9Y%3D" rel="nofollow">https://golang.org/pkg/reflect/</a> 最重要的官方文档,建议先粗读一遍再来看本文。</li>
<li>go 的 reflect 还是比较简单的,可以很快上手。</li>
<li>
<a href="https://link.segmentfault.com/?enc=BjU%2Bqcy31Qw8MtrZ7sZg%2Bg%3D%3D.5QnS%2BGd7FmN72MoTNxMXAsLVQgBgU4YB2aVj7HSLOEDW8mEQW5WSs08ic9HpACsdZZ7lLf4gAaBA4s%2BtGXBITg%3D%3D" rel="nofollow">https://github.com/golang/go/blob/master/src/reflect/type.go</a> <a href="https://link.segmentfault.com/?enc=L8uAzXFrPdc61jv7sBmoqg%3D%3D.yXMAW8ZuS6cxx%2BFTqhU89OW3LAaqwMLdh0PRKIYGv%2FKBM12J07liX%2FGFUHw%2FNR1D1fiUDLF2jEXHB6eEv7ZIpg%3D%3D" rel="nofollow">https://github.com/golang/go/blob/master/src/reflect/value.go</a><br>源码中有上百个 panic,各种检查做的很全面,有想法就大胆地去试,只要能 run 起来,一般问题不大。</li>
<li>实际使用中可以先不考虑使用 reflect 对性能的影响,先实现功能,再利用 benchmark test 去优化</li>
</ol></blockquote>
<h3>什么时候应该用 reflect</h3>
<ol>
<li><strong><code>为了降低多写代码造成的bug率,做更好的归约和抽象。</code></strong></li>
<li><strong><code>为了灵活、好用、方便,做动态解析、调用和处理。</code></strong></li>
<li><strong><code>为了代码好看、易读、提高开发效率,补足与动态语言之间的一些差别</code></strong></li>
</ol>
<p><strong>记住!reflect 不是用来实现你的奇技淫巧的!使用 reflect 要适可而止!</strong></p>
<h3>reflect 核心</h3>
<h4>TypeOf(i interface{}) Type</h4>
<p><strong>重点看这个返回值,它是一个接口,主要实现它的是 <code>struct rtype</code>,这个也是 go 类型系统的核心,和 runtime/type.go <code>struct _type</code> 一致,这里就不深入展开了,回头再说。</strong></p>
<pre><code>type Type interface {
// 变量的内存对齐,返回 rtype.align
Align() int
// struct 字段的内存对齐,返回 rtype.fieldAlign
FieldAlign() int
// 根据传入的 i,返回方法实例,表示类型的第 i 个方法
Method(int) Method
// 根据名字返回方法实例,这个比较常用
MethodByName(string) (Method, bool)
// 返回类型方法集中可导出的方法的数量
NumMethod() int
// 只返回类型名,不含包名
Name() string
// 返回导入路径,即 import 路径
PkgPath() string
// 返回 rtype.size 即类型大小,单位是字节数
Size() uintptr
// 返回类型名字,实际就是 PkgPath() + Name()
String() string
// 返回 rtype.kind,描述一种基础类型
Kind() Kind
// 检查当前类型有没有实现接口 u
Implements(u Type) bool
// 检查当前类型能不能赋值给接口 u
AssignableTo(u Type) bool
// 检查当前类型能不能转换成接口 u 类型
ConvertibleTo(u Type) bool
// 检查当前类型能不能做比较运算,其实就是看这个类型底层有没有绑定 typeAlg 的 equal 方法。
// 打住!不要去搜 typeAlg 是什么,不然你会陷进去的!先把本文看完。
Comparable() bool
// 返回类型的位大小,但不是所有类型都能调这个方法,不能调的会 panic
Bits() int
// 返回 channel 类型的方向,如果不是 channel,会 panic
ChanDir() ChanDir
// 返回函数类型的最后一个参数是不是可变数量的,"..." 就这样的,同样,如果不是函数类型,会 panic
IsVariadic() bool
// 返回所包含元素的类型,只有 Array, Chan, Map, Ptr, Slice 这些才能调,其他类型会 panic。
// 这不是废话吗。。其他类型也没有包含元素一说。
Elem() Type
// 返回 struct 类型的第 i 个字段,不是 struct 会 panic,i 越界也会 panic
Field(i int) StructField
// 跟上边一样,不过是嵌套调用的,比如 [1, 2] 就是说返回当前 struct 的第1个struct 的第2个字段,适用于 struct 本身嵌套的类型
FieldByIndex(index []int) StructField
// 按名字找 struct 字段,第二个返回值 ok 表示有没有
FieldByName(name string) (StructField, bool)
// 按函数名找 struct 字段,因为 struct 里也可能有类型是 func 的嘛
FieldByNameFunc(match func(string) bool) (StructField, bool)
// 返回函数第 i 个参数的类型,不是 func 会 panic
In(i int) Type
// 返回 map 的 key 的类型,不是 map 会 panic
Key() Type
// 返回 array 的长度,不是 array 会 panic
Len() int
// 返回 struct 字段数量,不是 struct 会 panic
NumField() int
// 返回函数的参数数量,不是 func 会 panic
NumIn() int
// 返回函数的返回值数量,不是 func 会 panic
NumOut() int
// 返回函数第 i 个返回值的类型,不是 func 会 panic
Out(i int) Type
}</code></pre>
<h4>ValueOf(i interface{}) Value</h4>
<p>先看看定义吧,就这么点东西。</p>
<pre><code>type Value struct {
// 反射出来此值的类型,rtype 是啥往上看,但可别弄错了,这 typ 是未导出的,从外部调不到 Type 接口的方法
typ *rtype
// 数据形式的指针值
ptr unsafe.Pointer
// 保存元数据
flag
}</code></pre>
<hr>
<p>Value 的方法太多了,参考开头的官方文档吧,我下面挑几个重点的说一下,像 len,cap 这种简单的就不介绍了:</p>
<pre><code>// 前提 v 是一个 func,然后调用 v,并传入 in 参数,第一个参数是 in[0],第二个是 in[1],以此类推
func (v Value) Call(in []Value) []Value
// 返回 v 的接口值或者指针
func (v Value) Elem() Value
// 前提 v 是一个 struct,返回第 i 个字段,这个主要用于遍历
func (v Value) Field(i int) Value
// 前提 v 是一个 struct,根据字段名直接定位返回
func (v Value) FieldByName(name string) Value
// 前提 v 是 Array, Slice, String 之一,返回第 i 个元素,主要也是用于遍历,注意不能越界
func (v Value) Index(i int) Value
// 判断 v 是不是 nil,只有 chan, func, interface, map, pointer, slice 可以用,其他类型会 panic
func (v Value) IsNil() bool
// 判断 v 是否合法,如果返回 false,那么除了 String() 以外的其他方法调用都会 panic,事前检查是必要的
func (v Value) IsValid() bool
// 前提 v 是个 map,返回对应 value
func (v Value) MapIndex(key Value)
// 前提 v 是个 map,返回所有 key 组成的一个 slice
func (v Value) MapKeys() []Value
// 前提 v 是个 struct,返回字段个数
func (v Value) NumField() int
// 赋值
func (v Value) Set(x Value)
// 类型
func (v Value) Type() Type</code></pre>
<h3>reflect 场景实践</h3>
<h4>动态调用函数(无参数)</h4>
<pre><code class="go">type T struct {}
func main() {
name := "Do"
t := &T{}
reflect.ValueOf(t).MethodByName(name).Call(nil)
}
func (t *T) Do() {
fmt.Println("hello")
}</code></pre>
<h4>动态调用函数(有参数)</h4>
<pre><code class="go">type T struct{}
func main() {
name := "Do"
t := &T{}
a := reflect.ValueOf(1111)
b := reflect.ValueOf("world")
in := []reflect.Value{a, b}
reflect.ValueOf(t).MethodByName(name).Call(in)
}
func (t *T) Do(a int, b string) {
fmt.Println("hello" + b, a)
}</code></pre>
<h4>处理返回值中的错误</h4>
<p>返回值也是 Value 类型,对于错误,可以转为 interface 之后断言</p>
<pre><code class="go">type T struct{}
func main() {
name := "Do"
t := &T{}
ret := reflect.ValueOf(t).MethodByName(name).Call(nil)
fmt.Printf("strValue: %[1]v\nerrValue: %[2]v\nstrType: %[1]T\nerrType: %[2]T", ret[0], ret[1].Interface().(error))
}
func (t *T) Do() (string, error) {
return "hello", errors.New("new error")
}
</code></pre>
<h4>struct tag 解析</h4>
<pre><code class="go">type T struct {
A int `json:"aaa" test:"testaaa"`
B string `json:"bbb" test:"testbbb"`
}
func main() {
t := T{
A: 123,
B: "hello",
}
tt := reflect.TypeOf(t)
for i := 0; i < tt.NumField(); i++ {
field := tt.Field(i)
if json, ok := field.Tag.Lookup("json"); ok {
fmt.Println(json)
}
test := field.Tag.Get("test")
fmt.Println(test)
}
}</code></pre>
<h4>类型转换和赋值</h4>
<pre><code class="go">type T struct {
A int `newT:"AA"`
B string `newT:"BB"`
}
type newT struct {
AA int
BB string
}
func main() {
t := T{
A: 123,
B: "hello",
}
tt := reflect.TypeOf(t)
tv := reflect.ValueOf(t)
newT := &newT{}
newTValue := reflect.ValueOf(newT)
for i := 0; i < tt.NumField(); i++ {
field := tt.Field(i)
newTTag := field.Tag.Get("newT")
tValue := tv.Field(i)
newTValue.Elem().FieldByName(newTTag).Set(tValue)
}
fmt.Println(newT)
}</code></pre>
<h4>通过 kind()处理不同分支</h4>
<pre><code class="go">func main() {
a := 1
t := reflect.TypeOf(a)
switch t.Kind() {
case reflect.Int:
fmt.Println("int")
case reflect.String:
fmt.Println("string")
}
}</code></pre>
<h4>判断实例是否实现了某接口</h4>
<pre><code class="go">type IT interface {
test1()
}
type T struct {
A string
}
func (t *T) test1() {}
func main() {
t := &T{}
ITF := reflect.TypeOf((*IT)(nil)).Elem()
tv := reflect.TypeOf(t)
fmt.Println(tv.Implements(ITF))
}</code></pre>
<h4>未完待续</h4>
<p>...</p>
Go Channel 高级实践
https://segmentfault.com/a/1190000016197615
2018-08-29T17:29:37+08:00
2018-08-29T17:29:37+08:00
ronniesong
https://segmentfault.com/u/sxssxs
125
<blockquote><ol>
<li>本文主要讲实践,原理部分会一笔带过,关于 go 语言并发实现和内存模型后续会有文章。</li>
<li>channel 实现的源码不复杂,推荐阅读,<a href="https://link.segmentfault.com/?enc=Ohb1rzOXct52k4UVG5q6uQ%3D%3D.I6lSnXNfVAI%2B6nPgEsh7CtpxIaBZFoa44IL1W%2FgeImgmkv1Y17bPYEVCcus6Vs90Y6BsJLfpn%2BqQi%2B%2FAlxeKTA%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=kZlr%2BOjWfrjN3GMAvChuHA%3D%3D.loe4qnUtm7VWAUYL%2FVtr8zbiZWEBN9mk0RlMyKSUd607cYlkeJGd%2F8xlxVsvzpZP5LwRDp1PpIXZMOgNplw8Jg%3D%3D" rel="nofollow">https://github.com/golang/go/...</a>
</li>
</ol></blockquote>
<h3>channel 是干什么的</h3>
<h4><strong>意义:<code>channel 是用来通信的</code></strong></h4>
<p><em>实际上:(数据拷贝了一份,并通过 channel 传递,本质就是个队列)</em></p>
<h3>channel 应该用在什么地方</h3>
<h4><strong>核心:<code>需要通信的地方</code></strong></h4>
<p>例如以下场景:</p>
<ul>
<li>通知广播</li>
<li>交换数据</li>
<li>显式同步</li>
<li>并发控制</li>
<li>...</li>
</ul>
<p><strong>记住!channel 不是用来实现锁机制的,虽然有些地方可以用它来实现类似读写锁,保护临界区的功能,但不要这么用!</strong></p>
<h3>channel 用例实现</h3>
<h4>超时控制</h4>
<pre><code class="go">// 利用 time.After 实现
func main() {
done := do()
select {
case <-done:
// logic
case <-time.After(3 * time.Second):
// timeout
}
}
func do() <-chan struct{} {
done := make(chan struct{}, 1)
go func() {
// do something
// ...
done <- struct{}{}
}()
return done
}</code></pre>
<h4>取最快的结果</h4>
<p>比较常见的一个场景是重试,第一个请求在指定超时时间内没有返回结果,这时重试第二次,取两次中最快返回的结果使用。<br>超时控制在上面有,下面代码部分就简单实现调用多次了。</p>
<pre><code class="go">func main() {
ret := make(chan string, 3)
for i := 0; i < cap(ret); i++ {
go call(ret)
}
fmt.Println(<-ret)
}
func call(ret chan<- string) {
// do something
// ...
ret <- "result"
}</code></pre>
<h4>限制最大并发数</h4>
<pre><code class="go">// 最大并发数为 2
limits := make(chan struct{}, 2)
for i := 0; i < 10; i++ {
go func() {
// 缓冲区满了就会阻塞在这
limits <- struct{}{}
do()
<-limits
}()
}</code></pre>
<h4>for...range 优先</h4>
<p><code>for ... range c { do }</code> 这种写法相当于 <code>if _, ok := <-c; ok { do }</code></p>
<pre><code class="go">func main() {
c := make(chan int, 20)
go func() {
for i := 0; i < 10; i++ {
c <- i
}
close(c)
}()
// 当 c 被关闭后,取完里面的元素就会跳出循环
for x := range c {
fmt.Println(x)
}
}</code></pre>
<h4>多个 goroutine 同步响应</h4>
<p>利用 close 广播</p>
<pre><code class="go">func main() {
c := make(chan struct{})
for i := 0; i < 5; i++ {
go do(c)
}
close(c)
}
func do(c <-chan struct{}) {
// 会阻塞直到收到 close
<-c
fmt.Println("hello")
}</code></pre>
<h4>非阻塞的 select</h4>
<p>select 本身是阻塞的,当所有分支都不满足就会一直阻塞,如果想不阻塞,那么一个什么都不干的 default 分支是最好的选择</p>
<pre><code class="go">select {
case <-done:
return
default:
}</code></pre>
<h4>for{select{}} 终止</h4>
<p>尽量不要用 break label 形式,而是把终止循环的条件放到 for 条件里来实现</p>
<pre><code class="go">for ok {
select {
case ch <- 0:
case <-done:
ok = false
}
}</code></pre>
<h4>未完待续</h4>
<p>...</p>
<h3>channel 特性</h3>
<h4>基础特性</h4>
<table>
<thead><tr>
<th>操作</th>
<th>值为 nil 的 channel</th>
<th>被关闭的 channel</th>
<th>正常的 channel</th>
</tr></thead>
<tbody>
<tr>
<td>close</td>
<td>panic</td>
<td>panic</td>
<td>成功关闭</td>
</tr>
<tr>
<td>c<-</td>
<td>永远阻塞</td>
<td>panic</td>
<td>阻塞或成功发送</td>
</tr>
<tr>
<td><-c</td>
<td>永远阻塞</td>
<td>永远不阻塞</td>
<td>阻塞或成功接收</td>
</tr>
</tbody>
</table>
<h4>happens-before 特性</h4>
<ol>
<li>无缓冲时,接收 happens-before 发送</li>
<li>任何情况下,发送 happens-before 接收</li>
<li>close happens-before 接收</li>
</ol>
<h3>参考</h3>
<ul>
<li><a href="https://link.segmentfault.com/?enc=a0cpFTptAuZET%2B3P5G420A%3D%3D.Iov2mgrNMnUJD7xFLNsOECwOTYAyn9jkPLDFQeG1mXNyVGAJNF0qYbbCS37dX07X" rel="nofollow">https://go101.org/article/channel.html</a></li>
<li><a href="https://link.segmentfault.com/?enc=WJ4k3tfC7sFm7r7pukPngQ%3D%3D.xpviU%2BMGY%2FdOle434wfy4pNU78gOP8ERaQ62CsB7k9m6iD7DSX4A8OlUITloGhLf94dDGTccP3EWYzLFV%2BBhGg%3D%3D" rel="nofollow">https://golang.org/doc/effective_go.html#channels</a></li>
</ul>
Go Slice 高级实践
https://segmentfault.com/a/1190000016126261
2018-08-23T17:50:58+08:00
2018-08-23T17:50:58+08:00
ronniesong
https://segmentfault.com/u/sxssxs
16
<blockquote><ol>
<li>以下用法中,类型均使用 <code>int64</code> 做为示例,不处理 interface 。</li>
<li>代码只是展示实现思路,不一定完善。</li>
</ol></blockquote>
<h3>合并两个有序切片,新切片仍然有序</h3>
<pre><code class="go">func MergeSortedSlice(s1, s2 []int64) []int64 {
// 从末尾元素开始遍历
i := len(s1) - 1
j := len(s2) - 1
// 合并后的长度
newLen := len(s1) + len(s2)
// 合并后的索引,也从末尾元素开始
newIdx := newLen - 1
// 创建一个新切片,代表合并后的
newS := make([]int64, newLen)
// 将 s1 的内容拷贝到新切片
for k, v := range s1 {
newS[k] = v
}
// 开始遍历
for i >= 0 && j >= 0 {
// 新元素
var newNum int64
// 将较大的值赋给新元素,同时向前移动指针
if newS[i] > s2[j] {
newNum = newS[i]
i--
} else {
newNum = s2[j]
j--
}
newS[newIdx] = newNum
newIdx--
}
// 如果 s2 还有剩余元素,则剩余元素一定都是最小的,直接放到头部即可
for j >= 0 {
newS[newIdx] = s2[j]
j--
newIdx--
}
return newS
}</code></pre>
<h3>根据特定规则过滤元素</h3>
<pre><code class="go">func FilterSlice(s []int64, filter func(x int64) bool) []int64 {
// 返回的新切片
// s[:0] 这种写法是创建了一个 len 为 0,cap 为 len(s) 即和原始切片最大容量一致的切片
// 因为是过滤,所以新切片的元素总个数一定不大于比原始切片,这样做减少了切片扩容带来的影响
// 同时,也有一个问题,因为 newS 和 s 共享底层数组,那么过滤后 s 也会被修改!
newS := s[:0]
// 遍历,对每个元素执行 filter,符合条件的加入新切片中
for _, x := range s {
if !filter(x) {
newS = append(newS, x)
}
}
return newS
}</code></pre>
<h3>去重</h3>
<h4>两种思路,循环顺序查找和使用 map 加快查找(引入一个 map 在各方面也是有开销的)。选用哪种,可以通过具体场景的 Benchmark 决定</h4>
<pre><code class="go">func RemoveDuplicates(s []int64) []int64 {
var ret []int64
for _, v := range s {
found := false
for _, v2 := range ret {
if v == v2 {
found = true
break
}
}
if !found {
ret = append(ret, v)
}
}
return ret
}
func RemoveDuplicates2(s []int64) []int64 {
ret := s[:0]
// 利用 struct{}{} 减少内存占用
assist := map[int64]struct{}{}
for _, v := range s {
if _, ok := assist[v]; !ok {
assist[v] = struct{}{}
ret = append(ret, v)
}
}
return ret
}</code></pre>
<h3>反转</h3>
<pre><code class="go">func Reversing(s []int64) []int64 {
for left, right := 0, len(s)-1; left < right; left, right = left+1, right-1 {
s[left], s[right] = s[right], s[left]
}
return s
}</code></pre>
<h3>分块</h3>
<h4>主要用于当单个切片过大,需要分多次使用的时候,比如网络调用等。</h4>
<pre><code class="go">func SliceChunk(s []int64, size int) [][]int64 {
var ret [][]int64
for size < len(s) {
// s[:size:size] 表示 len 为 size,cap 也为 size,第二个冒号后的 size 表示 cap
s, ret = s[size:], append(ret, s[:size:size])
}
ret = append(ret, s)
return ret
}</code></pre>
<h3>类型转换</h3>
<h4>RPC 中,不同下游接收的类型可能不一样,还有自定义类型,这里提供一个快速转换的方法</h4>
<pre><code class="go">s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var newS []int64
// 做法是利用 reflect 直接替换数据指针
// 但是这个不保证在以后的版本中一直可用 ╮(╯▽╰)╭
*(*reflect.SliceHeader)(unsafe.Pointer(&newS)) = *(*reflect.SliceHeader)(unsafe.Pointer(&s))
fmt.Printf("type:%T value:%v", newS, newS)</code></pre>
<h3>未完待续</h3>
<blockquote>主要参考:<br><a href="https://link.segmentfault.com/?enc=sMG6kqzbpEoTV1hcSEW%2BFg%3D%3D.BDXGMIpZdNJeTGKQ%2B5jbfOvqlijhASmHTB2VuS03MCvLeZGzkg0ZhbaG8JDQDAcO" rel="nofollow">https://github.com/golang/go/wiki/SliceTricks</a><br>官方使用技巧,建议多看看。</blockquote>
Golang - 调度剖析【第一部分】
https://segmentfault.com/a/1190000016038785
2018-08-16T17:07:40+08:00
2018-08-16T17:07:40+08:00
ronniesong
https://segmentfault.com/u/sxssxs
61
<h2>简介</h2>
<p>首先,Golang 调度器的设计和实现让我们的 Go 程序在多线程执行时效率更高,性能更好。这要归功于 Go 调度器与操作系统(OS)调度器的协同合作。不过在本篇文章中,多线程 Go 程序在设计和实现上是否与调度器的工作原理完全契合不是重点。重要的是对系统调度器和 Go 调度器,它们是如何正确地设计多线程程序,有一个全面且深入的理解。</p>
<p>本章多数内容将侧重于讨论调度器的高级机制和语义。我将展示一些细节,让你可以通过图像来理解它们是如何工作的,可以让你在写代码时做出更好的决策。因为原理和语义是必备的基础知识中的关键。</p>
<h2>系统调度</h2>
<p>操作系统调度器是一个复杂的程序。它们要考虑到运行时的硬件设计和设置,其中包括但不限于多处理器核心、CPU 缓存和 NUMA,只有考虑全面,调度器才能做到尽可能地高效。值得高兴的是,你不需要深入研究这些问题,就可以大致上了解操作系统调度器是如何工作的。</p>
<p>你的代码会被翻译成一系列机器指令,然后依次执行。为了实现这一点,操作系统使用线程(<em>Thread</em>)的概念。线程负责顺序执行分配给它的指令。一直执行到没有指令为止。这就是我将线程称为“执行流”的原因。</p>
<p>你运行的每个程序都会创建一个进程,每个进程都有一个初始线程。而后线程可以创建更多的线程。每个线程互相独立地运行着,调度是在线程级别而不是在进程级别做出的。<strong>线程可以并发运行(每个线程在单个内核上轮流运行),也可以并行运行(每个线程在不同的内核上同时运行)。</strong>线程还维护自己的状态,以便安全、本地和独立地执行它们的指令。</p>
<p>如果有线程可以执行,操作系统调度器就会调度它到空闲的 CPU 核心上去执行,保证 CPU 不闲着。它还必须模拟一个假象,即所有可以执行的线程都在同时地执行着。在这个过程中,调度器还会根据优先级不同选择线程执行的先后顺序,高优先级的先执行,低优先级的后执行。当然,低优先级的线程也不会被饿着。调度器还需要通过快速而明智的决策尽可能减少调度延迟。</p>
<p>为了实现这一目标,算法在其中做了很多工作,且幸运的是,这个领域已经积累了几十年经验。为了我们能更好地理解这一切,接下来我们来看几个重要的概念。</p>
<h2>执行指令</h2>
<p>程序计数器(PC),有时称为指令指针(IP),线程利用它来跟踪下一个要执行的指令。在大多数处理器中,PC指向的是下一条指令,而不是当前指令。<br><img src="/img/bVbfrRN?w=1001&h=751" alt="图片描述" title="图片描述"><br>如果你之前看过 Go 程序的堆栈跟踪,那么你可能已经注意到了每行末尾的这些十六进制数字。如下:</p>
<pre><code>goroutine 1 [running]:
main.example(0xc000042748, 0x2, 0x4, 0x106abae, 0x5, 0xa)
stack_trace/example1/example1.go:13 +0x39 <- LOOK HERE
main.main()
stack_trace/example1/example1.go:8 +0x72 <- LOOK HERE</code></pre>
<p>这些数字表示 PC 值与相应函数顶部的偏移量。<code>+0x39</code>PC 偏移量表示在程序没中断的情况下,线程即将执行的下一条指令。如果控制权回到主函数中,则主函数中的下一条指令是<code>0+x72</code>PC 偏移量。更重要的是,指针前面的指令是当前正在执行的指令。</p>
<pre><code>下面是对应的代码
https://github.com/ardanlabs/gotraining/blob/master/topics/go/profiling/stack_trace/example1/example1.go
07 func main() {
08 example(make([]string, 2, 4), "hello", 10)
09 }
12 func example(slice []string, str string, i int) {
13 panic("Want stack trace")
14 }</code></pre>
<p>十六进制数<code>+0x39</code>表示示例函数内的一条指令的 PC 偏移量,该指令位于函数的起始指令后面第57条(10进制)。接下来,我们用 objdump 来看一下汇编指令。找到第57条指令,注意,<code>runtime.gopanic</code>那一行。</p>
<pre><code>$ go tool objdump -S -s "main.example" ./example1
TEXT main.example(SB) stack_trace/example1/example1.go
func example(slice []string, str string, i int) {
0x104dfa0 65488b0c2530000000 MOVQ GS:0x30, CX
0x104dfa9 483b6110 CMPQ 0x10(CX), SP
0x104dfad 762c JBE 0x104dfdb
0x104dfaf 4883ec18 SUBQ $0x18, SP
0x104dfb3 48896c2410 MOVQ BP, 0x10(SP)
0x104dfb8 488d6c2410 LEAQ 0x10(SP), BP
panic("Want stack trace")
0x104dfbd 488d059ca20000 LEAQ runtime.types+41504(SB), AX
0x104dfc4 48890424 MOVQ AX, 0(SP)
0x104dfc8 488d05a1870200 LEAQ main.statictmp_0(SB), AX
0x104dfcf 4889442408 MOVQ AX, 0x8(SP)
0x104dfd4 e8c735fdff CALL runtime.gopanic(SB)
0x104dfd9 0f0b UD2 <--- 这里是 PC(+0x39)</code></pre>
<p><strong>记住: PC 是下一个指令,而不是当前指令</strong>。上面是基于 amd64 的汇编指令的一个很好的例子,该 Go 程序的线程负责顺序执行。</p>
<h2>线程状态</h2>
<p>另一个重要的概念是线程状态,它描述了调度器在线程中的角色。<br>线程可以处于三种状态之一: <code>等待中(Waiting)</code>、<code>待执行(Runnable)</code>或<code>执行中(Executing)</code>。</p>
<p><code>等待中(Waiting)</code>:这意味着线程停止并等待某件事情以继续。这可能是因为等待硬件(磁盘、网络)、操作系统(系统调用)或同步调用(原子、互斥)等原因。这些类型的延迟是性能下降的根本原因。</p>
<p><code>待执行(Runnable)</code>:这意味着线程需要内核上的时间,以便执行它指定的机器指令。如果有很多线程都需要时间,那么线程需要等待更长的时间才能获得执行。此外,由于更多的线程在竞争,每个线程获得的单个执行时间都会缩短。这种类型的调度延迟也可能导致性能下降。</p>
<p><code>执行中(Executing)</code>:这意味着线程已经被放置在一个核心上,并且正在执行它的机器指令。与应用程序相关的工作正在完成。这是每个人都想要的。</p>
<h2>工作类型</h2>
<p>线程可以做两种类型的工作。第一个称为 <strong>CPU-Bound</strong>,第二个称为 <strong>IO-Bound</strong>。</p>
<p><strong>CPU-Bound</strong>:这种工作类型永远也不会让线程处在等待状态,因为这是一项不断进行计算的工作。比如计算 π 的第 n 位,就是一个 CPU-Bound 线程。</p>
<p><strong>IO-Bound</strong>:这是导致线程进入等待状态的工作类型。比如通过网络请求对资源的访问或对操作系统进行系统调用。</p>
<h2>上下文切换</h2>
<p>诸如 Linux、Mac、 Windows 是一个具有抢占式调度器的操作系统。这意味着一些重要的事情。首先,这意味着调度程序在什么时候选择运行哪些线程是不可预测的。线程优先级和事件混在一起(比如在网络上接收数据)使得无法确定调度程序将选择做什么以及什么时候做。</p>
<p>其次,这意味着你永远不能基于一些你曾经历过但不能保证每次都发生的行为来编写代码。如果应用程序中需要确定性,则必须控制线程的同步和协调管理。</p>
<p>在核心上交换线程的物理行为称为上下文切换。当调度器将一个正在执行的线程从内核中取出并将其更改状态为一个可运行的线程时,就会发生上下文切换。</p>
<p>上下文切换的代价是高昂的,因为在核心上交换线程会花费很多时间。上下文切换的延迟取决于不同的因素,大概在在 50 到 100 纳秒之间。考虑到硬件应该能够合理地(平均)在每个核心上每纳秒执行 12 条指令,那么一次上下文切换可能会花费 600 到 1200 条指令的延迟时间。实际上,上下文切换占用了大量程序执行指令的时间。</p>
<p>如果你在执行一个 IO-Bound 程序,那么上下文切换将是一个优势。一旦一个线程更改到等待状态,另一个处于可运行状态的线程就会取而代之。这使得 CPU 总是在工作。这是调度器最重要的之一,最好不要让 CPU 闲下来。</p>
<p>而如果你在执行一个 CPU-Bound 程序,那么上下文切换将成为性能瓶颈的噩梦。由于线程总是有工作要做,所以上下文切换阻碍了工作的进展。这种情况与 IO-Bound 类型的工作形成了鲜明对比。</p>
<h2>少即是多</h2>
<p>在早期处理器只有一个核心的时代,调度相对简单。因为只有一个核心,所以物理上在任何时候都只有一个线程可以执行。其思想是定义一个调度程序周期,并尝试在这段时间内执行所有可运行线程。<strong>算法很简单:用调度周期除以需要执行的线程数。</strong></p>
<p>例如,如果你将调度器周期定义为 10ms(毫秒),并且你有 2 个线程,那么每个线程将分别获得 5ms。如果你有 5 个线程,每个线程得到 2ms。但是,如果有 1000 个线程,会发生什么情况呢?给每个线程一个时间片 10μs (微秒)?错了,这么干是愚蠢的,因为你会花费大量的时间在上下文切换上,而真正的工作却做不成。</p>
<p>你需要限制时间片的长度。在最后一个场景中,如果最小时间片是 2ms,并且有 1000 个线程,那么调度器周期需要增加到 2s(秒)。如果有 10000 个线程,那么调度器周期就是 20s。在这个简单的例子中,如果每个线程使用它的全时间片,那么所有线程运行一次需要花费 20s。</p>
<p>要知道,这是一个非常简单的场景。在真正进行调度决策时,调度程序需要考虑和处理比这更多的事情。你可以控制应用程序中使用的线程数量。当有更多的线程要考虑,并且发生 IO-Bound 工作时,就会出现一些混乱和不确定的行为。任务需要更长的时间来调度和执行。</p>
<p>这就是为什么游戏规则是“少即是多”。处于可运行状态的线程越少,意味着调度开销越少,每个线程执行的时间越长。完成的工作会越多。如此,效率就越高。</p>
<h2>寻找一个平衡</h2>
<p>你需要在 <strong>CPU 核心数</strong>和为应用程序获得最佳吞吐量所需的<strong>线程数</strong>之间找到<strong>平衡</strong>。当涉及到管理这种平衡时,线程池是一个很好的解决方案。将在第二部分中为你解析,Go 并不是这样做的。</p>
<h2>CPU 缓存</h2>
<p>从主存访问数据有很高的延迟成本(大约 100 到 300 个时钟周期),因此处理器核心使用本地高速缓存来将数据保存在需要的硬件线程附近。从缓存访问数据的成本要低得多(大约 3 到 40 个时钟周期),这取决于所访问的缓存。如今,提高性能的一个方面是关于如何有效地将数据放入处理器以减少这些数据访问延迟。编写多线程应用程序也需要考虑 CPU 缓存的机制。</p>
<p><img src="/img/bVbfsoO?w=1085&h=835" alt="图片描述" title="图片描述"></p>
<p>数据通过<code>cache lines</code>在处理器和主存储器之间交换。<code>cache line</code>是在主存和高速缓存系统之间交换的 64 字节内存块。每个内核都有自己所需的<code>cache line</code>的副本,这意味着硬件使用值语义。这就是为什么多线程应用程序中内存的变化会造成性能噩梦。</p>
<p>当并行运行的多个线程正在访问相同的数据值,甚至是相邻的数据值时,它们将访问同一<code>cache line</code>上的数据。在任何核心上运行的任何线程都将获得同一<code>cache line</code>的副本。</p>
<p><img src="/img/bVbfsrm?w=1082&h=832" alt="图片描述" title="图片描述"></p>
<p>如果某个核心上的一个线程对其<code>cache line</code>的副本进行了更改,那么同一<code>cache line</code>的所有其他副本都必须标记为<code>dirty</code>的。当线程尝试对<code>dirty cache line</code>进行读写访问时,需要向主存访问(大约 100 到 300 个时钟周期)来获得<code>cache line</code>的新副本。</p>
<p>也许在一个 2 核处理器上这不是什么大问题,但是如果一个 32 核处理器在同一<code>cache line</code>上同时运行 32 个线程来访问和改变数据,那会发生什么?如果一个系统有两个物理处理器,每个处理器有16个核心,那又该怎么办呢?这将变得更糟,因为处理器到处理器的通信延迟更大。应用程序将会在主存中周转,性能将会大幅下降。</p>
<p>这被称为缓存一致性问题,还引入了错误共享等问题。在编写可能会改变共享状态的多线程应用程序时,必须考虑缓存系统。</p>
<h2>调度决策场景</h2>
<p>假设我要求你基于我给你的信息编写操作系统调度器。考虑一下这个你必须考虑的情况。记住,这是调度程序在做出调度决策时必须考虑的许多有趣的事情之一。</p>
<p>启动应用程序,创建主线程并在<code>核心1</code>上执行。当线程开始执行其指令时,由于需要数据,正在检索<code>cache line</code>。现在,线程决定为一些并发处理创建一个新线程。下面是问题:</p>
<ol>
<li>进行上下文切换,切出<code>核心1</code>的主线程,切入新线程?这样做有助于提高性能,因为这个新线程需要的相同部分的数据很可能已经被缓存。但主线程没有得到它的全部时间片。</li>
<li>新线程等待<code>核心1</code>在主线程完成之前变为可用?线程没有运行,但一旦启动,获取数据的延迟将被消除。</li>
<li>线程等待下一个可用的核心?这意味着所选核心的<code>cache line</code>将被刷新、检索和复制,从而导致延迟。然而,线程将启动得更快,主线程可以完成它的时间片。</li>
</ol>
<p>有意思吗?这些是系统调度器在做出调度决策时需要考虑的有趣问题。幸运的是,不是我做的。我能告诉你的就是,如果有一个空闲核心,它将被使用。你希望线程在可以运行时运行。</p>
<h2>结论</h2>
<p>本文的第一部分深入介绍了在编写多线程应用程序时需要考虑的关于线程和系统调度器的问题。这些是 Go 调度器也要考虑的事情。在下一篇文章中,我将解析 Go 调度器的语义以及它们如何与这些信息相关联,并通过一些示例程序来展示。</p>
Go 中 io 包的使用方法
https://segmentfault.com/a/1190000015591319
2018-07-10T19:39:26+08:00
2018-07-10T19:39:26+08:00
ronniesong
https://segmentfault.com/u/sxssxs
69
<h2>前言</h2>
<p>在 Go 中,输入和输出操作是使用原语实现的,这些原语将数据模拟成可读的或可写的字节流。<br>为此,Go 的 <code>io</code> 包提供了 <code>io.Reader</code> 和 <code>io.Writer</code> 接口,分别用于数据的输入和输出,如图:</p>
<p><img src="/img/bVbdzja?w=1600&h=214" alt="图片描述" title="图片描述"></p>
<p>Go 官方提供了一些 API,支持对<strong>内存结构</strong>,<strong>文件</strong>,<strong>网络连接</strong>等资源进行操作<br>本文重点介绍如何实现标准库中 <code>io.Reader</code> 和 <code>io.Writer</code> 两个接口,来完成流式传输数据。</p>
<h2><code>io.Reader</code></h2>
<p><code>io.Reader</code> 表示一个读取器,它将数据从某个资源读取到传输缓冲区。在缓冲区中,数据可以被流式传输和使用。<br>如图:<br><img src="/img/bVbdzru?w=1600&h=354" alt="图片描述" title="图片描述"></p>
<p>对于要用作读取器的类型,它必须实现 <code>io.Reader</code> 接口的唯一一个方法 <code>Read(p []byte)</code>。<br>换句话说,只要实现了 <code>Read(p []byte)</code> ,那它就是一个读取器。</p>
<pre><code class="go">type Reader interface {
Read(p []byte) (n int, err error)
}</code></pre>
<p><code>Read()</code> 方法有两个返回值,一个是读取到的字节数,一个是发生错误时的错误。<br>同时,如果资源内容已全部读取完毕,应该返回 <code>io.EOF</code> 错误。</p>
<h3>使用 Reader</h3>
<p>利用 <code>Reader</code> 可以很容易地进行流式数据传输。<code>Reader</code> 方法内部是被循环调用的,每次迭代,它会从数据源读取一块数据放入缓冲区 <code>p</code> (即 Read 的参数 p)中,直到返回 <code>io.EOF</code> 错误时停止。</p>
<p>下面是一个简单的例子,通过 <code>string.NewReader(string)</code> 创建一个字符串读取器,然后流式地按字节读取:</p>
<pre><code class="go">func main() {
reader := strings.NewReader("Clear is better than clever")
p := make([]byte, 4)
for {
n, err := reader.Read(p)
if err != nil{
if err == io.EOF {
fmt.Println("EOF:", n)
break
}
fmt.Println(err)
os.Exit(1)
}
fmt.Println(n, string(p[:n]))
}
}</code></pre>
<pre><code class="tex">输出打印的内容:
4 Clea
4 r is
4 bet
4 ter
4 than
4 cle
3 ver
EOF: 0 </code></pre>
<p>可以看到,最后一次返回的 n 值有可能小于缓冲区大小。</p>
<h3>自己实现一个 Reader</h3>
<p>上一节是使用标准库中的 <code>io.Reader</code> 读取器实现的。<br>现在,让我们看看如何自己实现一个。它的功能是从流中过滤掉非字母字符。</p>
<pre><code class="go">type alphaReader struct {
// 资源
src string
// 当前读取到的位置
cur int
}
// 创建一个实例
func newAlphaReader(src string) *alphaReader {
return &alphaReader{src: src}
}
// 过滤函数
func alpha(r byte) byte {
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
return r
}
return 0
}
// Read 方法
func (a *alphaReader) Read(p []byte) (int, error) {
// 当前位置 >= 字符串长度 说明已经读取到结尾 返回 EOF
if a.cur >= len(a.src) {
return 0, io.EOF
}
// x 是剩余未读取的长度
x := len(a.src) - a.cur
n, bound := 0, 0
if x >= len(p) {
// 剩余长度超过缓冲区大小,说明本次可完全填满缓冲区
bound = len(p)
} else if x < len(p) {
// 剩余长度小于缓冲区大小,使用剩余长度输出,缓冲区不补满
bound = x
}
buf := make([]byte, bound)
for n < bound {
// 每次读取一个字节,执行过滤函数
if char := alpha(a.src[a.cur]); char != 0 {
buf[n] = char
}
n++
a.cur++
}
// 将处理后得到的 buf 内容复制到 p 中
copy(p, buf)
return n, nil
}
func main() {
reader := newAlphaReader("Hello! It's 9am, where is the sun?")
p := make([]byte, 4)
for {
n, err := reader.Read(p)
if err == io.EOF {
break
}
fmt.Print(string(p[:n]))
}
fmt.Println()
}</code></pre>
<pre><code class="tex">输出打印的内容:
HelloItsamwhereisthesun</code></pre>
<h3>组合多个 <strong>Reader</strong>,目的是重用和屏蔽下层实现的复杂度</h3>
<p>标准库已经实现了许多 <strong>Reader</strong>。<br>使用一个 <code>Reader</code> 作为另一个 <code>Reader</code> 的实现是一种常见的用法。<br>这样做可以让一个 <code>Reader</code> 重用另一个 <code>Reader</code> 的逻辑,下面展示通过更新 <code>alphaReader</code> 以接受 <code>io.Reader</code> 作为其来源。</p>
<pre><code class="go">type alphaReader struct {
// alphaReader 里组合了标准库的 io.Reader
reader io.Reader
}
func newAlphaReader(reader io.Reader) *alphaReader {
return &alphaReader{reader: reader}
}
func alpha(r byte) byte {
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
return r
}
return 0
}
func (a *alphaReader) Read(p []byte) (int, error) {
// 这行代码调用的就是 io.Reader
n, err := a.reader.Read(p)
if err != nil {
return n, err
}
buf := make([]byte, n)
for i := 0; i < n; i++ {
if char := alpha(p[i]); char != 0 {
buf[i] = char
}
}
copy(p, buf)
return n, nil
}
func main() {
// 使用实现了标准库 io.Reader 接口的 strings.Reader 作为实现
reader := newAlphaReader(strings.NewReader("Hello! It's 9am, where is the sun?"))
p := make([]byte, 4)
for {
n, err := reader.Read(p)
if err == io.EOF {
break
}
fmt.Print(string(p[:n]))
}
fmt.Println()
}</code></pre>
<p><strong>这样做的另一个优点是 <code>alphaReader</code> 能够从任何 Reader 实现中读取。</strong><br><strong>例如,以下代码展示了 <code>alphaReader</code> 如何与 <code>os.File</code> 结合以过滤掉文件中的非字母字符:</strong></p>
<pre><code class="go">func main() {
// file 也实现了 io.Reader
file, err := os.Open("./alpha_reader3.go")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer file.Close()
// 任何实现了 io.Reader 的类型都可以传入 newAlphaReader
// 至于具体如何读取文件,那是标准库已经实现了的,我们不用再做一遍,达到了重用的目的
reader := newAlphaReader(file)
p := make([]byte, 4)
for {
n, err := reader.Read(p)
if err == io.EOF {
break
}
fmt.Print(string(p[:n]))
}
fmt.Println()
}</code></pre>
<h2><code>io.Writer</code></h2>
<p><code>io.Writer</code> 表示一个编写器,它从缓冲区读取数据,并将数据写入目标资源。</p>
<p><img src="/img/bVbdzWd?w=1600&h=358" alt="图片描述" title="图片描述"></p>
<p>对于要用作编写器的类型,必须实现 <code>io.Writer</code> 接口的唯一一个方法 <code>Write(p []byte)</code><br>同样,只要实现了 <code>Write(p []byte)</code> ,那它就是一个编写器。</p>
<pre><code class="go">type Writer interface {
Write(p []byte) (n int, err error)
}</code></pre>
<p><code>Write()</code> 方法有两个返回值,一个是写入到目标资源的字节数,一个是发生错误时的错误。</p>
<h3>使用 Writer</h3>
<p>标准库提供了许多已经实现了 <code>io.Writer</code> 的类型。<br>下面是一个简单的例子,它使用 <code>bytes.Buffer</code> 类型作为 <code>io.Writer</code> 将数据写入内存缓冲区。</p>
<pre><code class="go">func main() {
proverbs := []string{
"Channels orchestrate mutexes serialize",
"Cgo is not Go",
"Errors are values",
"Don't panic",
}
var writer bytes.Buffer
for _, p := range proverbs {
n, err := writer.Write([]byte(p))
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if n != len(p) {
fmt.Println("failed to write data")
os.Exit(1)
}
}
fmt.Println(writer.String())
}</code></pre>
<pre><code class="tex">输出打印的内容:
Channels orchestrate mutexes serializeCgo is not GoErrors are valuesDon't panic</code></pre>
<h3>自己实现一个 Writer</h3>
<p>下面我们来实现一个名为 <code>chanWriter</code> 的自定义 <code>io.Writer</code> ,它将其内容作为字节序列写入 <code>channel</code> 。</p>
<pre><code class="go">type chanWriter struct {
// ch 实际上就是目标资源
ch chan byte
}
func newChanWriter() *chanWriter {
return &chanWriter{make(chan byte, 1024)}
}
func (w *chanWriter) Chan() <-chan byte {
return w.ch
}
func (w *chanWriter) Write(p []byte) (int, error) {
n := 0
// 遍历输入数据,按字节写入目标资源
for _, b := range p {
w.ch <- b
n++
}
return n, nil
}
func (w *chanWriter) Close() error {
close(w.ch)
return nil
}
func main() {
writer := newChanWriter()
go func() {
defer writer.Close()
writer.Write([]byte("Stream "))
writer.Write([]byte("me!"))
}()
for c := range writer.Chan() {
fmt.Printf("%c", c)
}
fmt.Println()
}</code></pre>
<p>要使用这个 <strong>Writer</strong>,只需在函数 <code>main()</code> 中调用 <code>writer.Write()</code>(在单独的goroutine中)。<br>因为 <code>chanWriter</code> 还实现了接口 <code>io.Closer</code> ,所以调用方法 <code>writer.Close()</code> 来正确地关闭channel,以避免发生泄漏和死锁。</p>
<h2>
<code>io</code> 包里其他有用的类型和方法</h2>
<p>如前所述,Go标准库附带了许多有用的功能和类型,让我们可以轻松使用流式io。</p>
<h3><code>os.File</code></h3>
<p>类型 <code>os.File</code> 表示本地系统上的文件。它实现了 <code>io.Reader</code> 和 <code>io.Writer</code> ,因此可以在任何 io 上下文中使用。<br>例如,下面的例子展示如何将连续的字符串切片直接写入文件:</p>
<pre><code class="go">func main() {
proverbs := []string{
"Channels orchestrate mutexes serialize\n",
"Cgo is not Go\n",
"Errors are values\n",
"Don't panic\n",
}
file, err := os.Create("./proverbs.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer file.Close()
for _, p := range proverbs {
// file 类型实现了 io.Writer
n, err := file.Write([]byte(p))
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if n != len(p) {
fmt.Println("failed to write data")
os.Exit(1)
}
}
fmt.Println("file write done")
}</code></pre>
<p>同时,<code>io.File</code> 也可以用作读取器来从本地文件系统读取文件的内容。<br>例如,下面的例子展示了如何读取文件并打印其内容:</p>
<pre><code class="go">func main() {
file, err := os.Open("./proverbs.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer file.Close()
p := make([]byte, 4)
for {
n, err := file.Read(p)
if err == io.EOF {
break
}
fmt.Print(string(p[:n]))
}
}</code></pre>
<h3><code>标准输入、输出和错误</code></h3>
<p><code>os</code> 包有三个可用变量 <code>os.Stdout</code> ,<code>os.Stdin</code> 和 <code>os.Stderr</code> ,它们的类型为 <code>*os.File</code>,分别代表 <code>系统标准输入</code>,<code>系统标准输出</code> 和 <code>系统标准错误</code> 的文件句柄。<br>例如,下面的代码直接打印到标准输出:</p>
<pre><code class="go">func main() {
proverbs := []string{
"Channels orchestrate mutexes serialize\n",
"Cgo is not Go\n",
"Errors are values\n",
"Don't panic\n",
}
for _, p := range proverbs {
// 因为 os.Stdout 也实现了 io.Writer
n, err := os.Stdout.Write([]byte(p))
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if n != len(p) {
fmt.Println("failed to write data")
os.Exit(1)
}
}
}</code></pre>
<h3><code>io.Copy()</code></h3>
<p><code>io.Copy()</code> 可以轻松地将数据从一个 Reader 拷贝到另一个 Writer。<br>它抽象出 <code>for</code> 循环模式(我们上面已经实现了)并正确处理 <code>io.EOF</code> 和 字节计数。 <br>下面是我们之前实现的简化版本:</p>
<pre><code class="go">func main() {
proverbs := new(bytes.Buffer)
proverbs.WriteString("Channels orchestrate mutexes serialize\n")
proverbs.WriteString("Cgo is not Go\n")
proverbs.WriteString("Errors are values\n")
proverbs.WriteString("Don't panic\n")
file, err := os.Create("./proverbs.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer file.Close()
// io.Copy 完成了从 proverbs 读取数据并写入 file 的流程
if _, err := io.Copy(file, proverbs); err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("file created")
}</code></pre>
<p>那么,我们也可以使用 <code>io.Copy()</code> 函数重写从文件读取并打印到标准输出的先前程序,如下所示:</p>
<pre><code class="go">func main() {
file, err := os.Open("./proverbs.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer file.Close()
if _, err := io.Copy(os.Stdout, file); err != nil {
fmt.Println(err)
os.Exit(1)
}
}</code></pre>
<h3><code>io.WriteString()</code></h3>
<p>此函数让我们方便地将字符串类型写入一个 Writer:</p>
<pre><code class="go">func main() {
file, err := os.Create("./magic_msg.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer file.Close()
if _, err := io.WriteString(file, "Go is fun!"); err != nil {
fmt.Println(err)
os.Exit(1)
}
}</code></pre>
<h3><code>使用管道的 Writer 和 Reader</code></h3>
<p>类型 <code>io.PipeWriter</code> 和 <code>io.PipeReader</code> 在内存管道中模拟 io 操作。<br>数据被写入管道的一端,并使用单独的 goroutine 在管道的另一端读取。<br>下面使用 <code>io.Pipe()</code> 创建管道的 reader 和 writer,然后将数据从 <code>proverbs</code> 缓冲区复制到<code>io.Stdout</code> :</p>
<pre><code class="go">func main() {
proverbs := new(bytes.Buffer)
proverbs.WriteString("Channels orchestrate mutexes serialize\n")
proverbs.WriteString("Cgo is not Go\n")
proverbs.WriteString("Errors are values\n")
proverbs.WriteString("Don't panic\n")
piper, pipew := io.Pipe()
// 将 proverbs 写入 pipew 这一端
go func() {
defer pipew.Close()
io.Copy(pipew, proverbs)
}()
// 从另一端 piper 中读取数据并拷贝到标准输出
io.Copy(os.Stdout, piper)
piper.Close()
}</code></pre>
<h3><code>缓冲区 io</code></h3>
<p>标准库中 <code>bufio</code> 包支持 缓冲区 io 操作,可以轻松处理文本内容。<br>例如,以下程序逐行读取文件的内容,并以值 <code>'\n'</code> 分隔:</p>
<pre><code class="go">func main() {
file, err := os.Open("./planets.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer file.Close()
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
break
} else {
fmt.Println(err)
os.Exit(1)
}
}
fmt.Print(line)
}
}</code></pre>
<h3><code>ioutil</code></h3>
<p><code>io</code> 包下面的一个子包 <code>utilio</code> 封装了一些非常方便的功能<br>例如,下面使用函数 <code>ReadFile</code> 将文件内容加载到 <code>[]byte</code> 中。</p>
<pre><code class="go">package main
import (
"io/ioutil"
...
)
func main() {
bytes, err := ioutil.ReadFile("./planets.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Printf("%s", bytes)
}</code></pre>
<h2>总结</h2>
<p>本文介绍了如何使用 <code>io.Reader</code> 和 <code>io.Writer</code> 接口在程序中实现流式IO。<br>阅读本文后,您应该能够了解如何使用 <code>io</code> 包来实现 流式传输IO数据的程序。<br>其中有一些例子,展示了如何创建自己的类型,并实现<code>io.Reader</code> 和 <code>io.Writer</code> 。</p>
<p>这是一个简单介绍性质的文章,没有扩展开来讲。<br>例如,我们没有深入文件IO,缓冲IO,网络IO或格式化IO(保存用于将来的写入)。<br>我希望这篇文章可以让你了解 Go语言中 流式IO 的常见用法是什么。</p>
<p>谢谢!</p>