标签 性能 下的文章

Go 1.4中值得关注的几个变化

在Go 1.3发布半年过去后,Go核心项目组于本月初发布了Go 1.4 Beta1版本。这个版本的几个变化点虽然不是革命性的,但对后续Go语言的发展来说,打下了基础,定下了基调。这里就几个值得关注的变化点结合Go 1.4代码进行一些简单描述,希望大家能对Go 1.4有个感性的认知和了解。

Go 1.4依旧保持了Go 1兼容性的承诺,你的已有代码几乎无需任何改动就可以通过Go 1.4的编译并运行。(以下是我的测试环境:go version go1.3 darwin/amd64 vs. go version go1.4beta1 linux/amd64

一、语言变化

1、For-range循环

在Go 1.3及以前,for-range循环具有两种形式:

for k, v := range x {
    …
}

for k := range x {
    …
}

问题:如果我们不关心循环中的值,我们只关心循环本身,我们仍然要提供一个变量,或用_占位。

for _ = range x {
    …
}

下面这样的语法在Go 1.3及以前是无法编译通过的:

for range x {
    …
}

不过Go 1.4支持这种形式的语法,它使得代码更加clean,虽然它可能很少会被使用到。

例子:

//testforrange.go

package main

import "fmt"

func main() {
        var a [5]int = [5]int{2, 3, 4, 5, 6}
        for k, v := range a {
                fmt.Println(k, v)
        }

        for k := range a {
                fmt.Println(k)
        }

        for _ = range a {
                fmt.Println("print without care about the key and value")
        }

        for range a {
                fmt.Println("new syntax – print without care about the key and value")
        }
}

Go 1.3编译出错:

$go run testforrange.go
# command-line-arguments
./testforrange.go:19: syntax error: unexpected range, expecting {
./testforrange.go:22: syntax error: unexpected }

Go 1.4编译成功并输出正确结果:

0 2
1 3
2 4
3 5
4 6
0
1
2
3
4
print without care about the key and value
print without care about the key and value
print without care about the key and value
print without care about the key and value
print without care about the key and value
new syntax – print without care about the key and value
new syntax – print without care about the key and value
new syntax – print without care about the key and value
new syntax – print without care about the key and value
new syntax – print without care about the key and value

2、通过**T调用方法

下面这个例子:

package main

import "fmt"

type T int

func (T) M() {
        fmt.Println("Call M")
}

var x **T

func main() {
        x.M()
}

按照Go 1.4官方release note的说法,1.3版本及以前的gc和gccgo都会正常接受这种调用方式。但Go 1规范只允许自动在x前面加一个解引用,而不是两个,因此这个是有悖于定义的。Go 1.4强制禁止这种调用。

不过根据我实际的测试,Go 1.3和Go 1.4针对上面代码都会出现同样地编译错误。

$go run testdoubledeferpointer.go
# command-line-arguments
./testdoubledeferpointer.go:14: calling method M with receiver x (type **T) requires explicit dereference

二、支持的操作系统以及处理器体系架构的变化

这个无法演示。不过一个主要的变化就是Go 1.4可以构建出运行于ARM处理器Android操作系统上的二进制程序了。使用go.mobile库中的支持包,Go 1.4也可以构建出可以被Android应用加载的.so库。

三、兼容性变化

人们通过unsafe包并利用Go的内部实现细节和数据的机器表示形式来绕过Go语言类型系统的约束。Go的设计者们认为这是对Go兼容性规范的 不尊重,在Go 1.4中,Go核心组正式宣布unsafe code不再保证其兼容性。这次Go 1.4并没有针对此做任何代码变动,只是一个clarification而已。

四、实现和工具的变化

1、运行时(runtime)的变化

Go 1.3及以前版本,Go语言的runtime(垃圾收集、并发支持、interface管理、maps、slices、strings等)主要由C语言和 少量汇编语言实现的。在1.4版本中,很多代码被替换成了用Go自身实现,这样垃圾回收器可以扫描程序运行时栈,获取活跃变量的精确信息。这个变 化很大,但对程序应该没有语义上的影响。

这次重写使得垃圾回收器变得更加精确,这意味着它知道所有程序中活跃指针的位置。这些相关改变将减小heap的大小,总体上大约减少 10%~30%。

这样做的结果是栈也不再需要是分段的(segmented)了,消除了“hot split”的问题。如果一个stack到达了使用上限,Go将分配一个新的更大的stack,相应goroutine中的所有活跃的栈帧将被复制到新 stack上,所有指向栈的指针将被更新。在某些场景下,其性能将会变得显著提升,并且这样修改后,其性能更具可预测性。

连续栈(contiguous stacks)的使用使得栈的初始Size可以更小,在Go 1.4中goroutine的初始栈大小从8192字节缩小为2048字节。(正式发布时也许会改为4096)。

interface值类型的实现也做了调整。在之前的发布版中,interface值内部用一个字(word)来承载,要么是一个指针,要么是一 个单字(one-word)大小的纯量值,这取决于interface值变量中具体存储的是什么对象。这个实现会给垃圾收集器带来诸多困难,因此 在Go 1.4版本中interface值内部就用指针表示。在运行的程序中,绝大多数interface值都是指针,因此这个影响很小。不过那些在 interface值类型变量中存储整型值的程序将会有更多的内存分配。

2、gccgo的状态

Gcc和Go两个项目的发布计划不是同步的,GCC 4.9版本包含了实现了1.2规范的gccgo,下一个发布版gcc 5.0将可能包含实现了1.4规范的gccgo。

3、internal包(内部包)

Go以package为基本逻辑单元组织代码。Go 1.3及之前版本的Go语言实际上只支持两种形式Package内符号的可见性:本地的(unexported)和全局的(exported)。有些时候 我们希望一些包并非能被所有外部包所导入,但却能被其“临近”的包所导入和访问。但之前的Go语言不具备这种特性。Go 1.4引入了"internal"包的概念,导入这种internal包的规则约束如下:

如果导入代码本身不在以"internal"目录的父目录为root的目录树中,那么 不允许其导入路径(import path)中包含internal元素。

例如:
    – a/b/c/internal/d/e/f只可以被以a/b/c为根的目录树下的代码导入,不能被a/b/g下的代码导入。
    – $GOROOT/src/pkg/internal/xxx只能被标准库($GOROOT/src)中的代码所导入。(注:Go 1.4 取消了$GOROOT/src/pkg,标准库都移到$GOROOT/src下了)。
    – $GOROOT/src/pkg/net/http/internal只能被net/http和net/http/*的包所导入
    – $GOPATH/src/mypkg/internal/foo只能被$GOPATH/src/mypkg包的代码所导入

对于Go 1.4该规则首先强制应用于$GOROOT下。Go 1.5将扩展应用到$GOPATH下。

4、权威导入路径(import paths)

我们经常使用托管在公共代码托管服务中的代码,诸如github.com,这意味着包导入路径包含托管服务名,比如github.com/rsc /pdf。一些场景下为了不破坏用户代码,我们用rsc.io/pdf,屏蔽底层具体哪家托管服务,比如rso.io/pdf的背后可能是 github.com也可能是bitbucket。但这样会引入一个问题,那就是不经意间我们为一个包生成了两个合法的导入路径。如果一个程序中 使用了这两个合法路径,一旦某个路径没有被识别出有更新,或者将包迁移到另外一个不同的托管公共服务下去时,使用旧导入路径包的程序就会报错。

Go 1.4引入一个包字句的注释,用于标识这个包的权威导入路径。如果使用的导入的路径不是权威路径,go命令会拒绝编译。语法很简单:

package pdf // import "rsc.io/pdf"

如果pdf包使用了权威导入路径注释,那么那些尝试使用github.com/rsc/pdf导入路径的程序将会被go编译器拒绝编译。

这个权威导入路径检查是在编译期进行的,而不是下载阶段。

我们举个例子:

我们的包foo以前是放在github.com/bigwhite/foo下面的,后来主托管站换成了tonybai.com/foo,最新的 foo包的代码:

package foo // import "tonybai.com/foo"

import "fmt"

func Echo(a string) {
        fmt.Println("Foo:, a)
}

某个应用通过旧路径github.com/bigwhite/foo导入了该包:

//testcanonicalimportpath.go
package main

import "github.com/bigwhite/foo"

func main() {
        foo.Echo("Hello!")
}

我们编译该go文件,得到以下结果:

code in directory /home/tonybai/Test/Go/src/github.com/bigwhite/foo expects import "tonybai.com/foo"

5、go generate子命令

go 1.4中go工具集合新引入一个子命令:go generate,用于在编译前自动化生成某类代码。例如在.y上运行yacc编译器生成实现该语法的.go源文件。或是使用stringer工 具自动为常量生成String方法。这个命令并非由go tools(build, get等)自动执行,而必须显式执行。

不过我简单测试了一下,似乎这个命令设计文档中的:

// +build generate

并不好用啊。即便将其作为generate directive放入go源文件,该文件依旧会被go编译器当做正常go文件编译。Go 1.4标准库中使用go generate directive的有三个地方:

strconv/quote.go://go:generate go run makeisprint.go -output isprint.go
time/zoneinfo_windows.go://go:generate go run genzabbrs.go -output zoneinfo_abbrs_windows.go
unicode/letter.go://go:generate go run maketables.go -tables=all -output tables.go

通过go generate来实现泛型(generics)似乎不那么优雅啊。虽然设计者并非将其作为Go泛型的实现^_^。

6、源码布局变化

在Go自身源码库($GOROOT下)中,包的源码放在src/pkg中,这样做与其他库不同,包括Go自己的子库,比如go.tools。因此在Go 1.4中,pkg这一层目录树将被去除,比如fmt包的源码曾经放在src/pkg/fmt下,现在则放在src/fmt下。

五、性能

绝大多数程序使用1.4编译后的运行速度会与1.3的一致或略有提升,有些可能也会变得慢些。这次修改的较多,很难准确预测。

这次许多runtime的代码由C变为Go,这将导致一些heap大小有所缩减。另外这样做后有利于Go编译器的优化,诸如内联,会带来性能上的小幅提升。

垃圾回收器一方面得到了加速,使得重度依赖垃圾收集的程序得到可衡量的提升。但另外一方面,新的write barrier又引起了性能下降。提升和下降的量的多少取决于程序的行为。

玩转top

相信很多人和我一样,top是自己日常使用最多的linux资源查看工具。不过仅限于一些简单的日常场景罢了:敲入top命令,看看哪些进程占用 CPU较多,然后对这些CPU占用较多的进程逐一处理一下。显然这样使用top有些大才小用了。

以前在监控工具使用方面总是浅尝辙止,并未做过多深入研究。近来愈来愈觉得有必要针对几种常用工具好好学习一下了。而top便首当其冲。top是一款 以查看进程(task)信息为中心的Linux系统性能监控工具,通过top我们可以查看到进程相关的cpu和内存占用相关的实时采样信息,因此 top尤其适合用于持续跟踪分析某些进程对系统cpu和内存的占用情况以及对系统负荷的影响。

入门

top的入门使用极其简单,就像前面所说的简单地的输入"top",我们就能看到top的输出了。

top – 06:35:47 up 7 min,  3 users,  load average: 1.00, 1.18, 0.67
Tasks: 189 total,   2 running, 186 sleeping,   0 stopped,   1 zombie
Cpu(s): 30.5%us,  7.6%sy,  0.0%ni, 60.5%id,  1.5%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:   1534164k total,  1423392k used,   110772k free,    67328k buffers
Swap:   999420k total,      144k used,   999276k free,   576924k cached

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                      
 1954 tonybai   20   0  316m  55m  26m S   26  3.7   0:36.53 compiz                                       
 2308 tonybai   20   0  499m  84m  39m S   13  5.6   1:07.63 chrome
… …

top的输出大致分为上下两个部分,上半部分输出到是系统的总体负荷信息,下半部分则是分进程列出进程的各种属性信息。

总体负荷信息由五行组成:

第一行:top – 06:35:47 up 7 min,  3 users,  load average: 1.00, 1.18, 0.67。
这行的输出与uptime命令是一样一样的,不信你可以单独执行一下uptime命令。我怀疑top就是直接调用uptime或使用uptime部分代码 得到的,毕竟它们都是procps(procps is the package that has a bunch of small useful utilities that give information about processes using the /proc filesystem.)工具集合的一员。这行输出了当前时间( 06:35:47)、自系统启动以来的累计时间(7 min),当前系统用户数(3 users),1分钟,5分钟以及15分钟的平均负荷( load average: 1.00, 1.18, 0.67)。

第二行:Tasks: 189 total,   2 running, 186 sleeping,   0 stopped,   1 zombie。
系统的进程信息汇总,包括总数以及处于各种状态的进程数量。

第三行:Cpu(s): 30.5%us,  7.6%sy,  0.0%ni, 60.5%id,  1.5%wa,  0.0%hi,  0.0%si,  0.0%st。
系统的CPU信息汇总,包括us(CPU用于运行用户空间进程的时间所占比例,不包括renice的用户进程)、sy(CPU用于运行内核进程的时间所占 比例)、ni(CPU用于运行用户空间被renice的进程的时间所占比例)、id(CPU空闲时间所占比例)、wa(CPU等待I/O完成时间所占用的 比例)、hi(处理硬件中断时间所占比例)、si(处理软中断时间所占比例)、st(虚拟机管理程序为其他task而从本虚拟机'偷取'的CPU时间所占 比例)。

第四行和第五行:
Mem:   1534164k total,  1423392k used,   110772k free,    67328k buffers
Swap:   999420k total,      144k used,   999276k free,   576924k cached

系统的内存以及交换区信息汇总,包括内存总量(mem total)、已使用内存(mem used)、空闲内存(mem free)以及交换区总量(swap total)、交换区使用量(swap used)、交换区空闲(swap free)。这里还有两个值buffers和cache,它们是内核使用的内存缓存,均是用于减少磁盘读取,提升系统性能的。buffers代表有多少内 存用于缓存磁盘数据块,目的是减少写磁盘次数;cache用于缓存从磁盘文件读取的数据,以减少读磁盘次数。

下半部分是进程属性信息展示区。默认情况输出的进程属性包括:
    PID(进程ID)
    USER(进程所有者的用户名)
    PR(进程的动态优先级)
    NI(Nice值,进程的base priority)
    VIRT (进程的虚拟内存用量,包括进程的二进制映像大小、数据区以及所有加载的共享库占用的size, = SWAP + RES)
    RES(进程使用的、未被换出的物理内存大小,= CODE + DATA)
    SHR(共享内存区域大小)
    S(进程状态)
    %CPU(上次刷新到现在运行该task的CPU时间所占百分比)
    %MEM(当前task所占用的内存百分比)
    TIME+  (自task启动后所使用的CPU时间累计)
    COMMAND (task对应的二进制程序名)

定制输出

top提供了强大的输出定制功能,无论是上半部分的系统整体负荷信息还是下半部分的进程属性信息展示都是可以根据使用的需求定制的。

整体负荷信息展示区的定制:
- 第一行展示/隐藏:通过点击键盘上的'l'键可以展示或隐藏第一行信息输出
- Task和CPU信息展示/隐藏:通过点击键盘上的't'键可以展示或隐藏Task和CPU行输出
- Mem和Swap信息展示/隐藏:通过点击键盘上的'm'键可以展示或隐藏Mem和Swap行输出

进程属性信息的显示定制:
默认情况下,我们可以看到top会显示进程的若干属性,包括PID、USER、PR、NI 、VIRT 、RES 、SHR、S、%CPU以及%MEM等。不过这些也仅仅是默认的而已,如果你不关住其中一些属性或关注其他一些属性,你完全可以自定义输出显示的进程属 性。点击键盘上的'f'键,top将为我们打开field选择页面:

Current Fields:  AEHIOQTWKNMbcdfgjplrsuvyzX  for window 1:Def
Toggle fields via field letter, type any other key to return

* A: PID        = Process Id                           0×00002000  PF_FREE_PAGES (2.5)
* E: USER       = User Name                            0×00008000  debug flag (2.5)
* H: PR         = Priority                             0×00024000  special threads (2.5)
… …

页面左侧列出了可选的所有进程属性。其中前面有*前缀的是当前已经选择的属性,比如PID。不过你可以通过点击PID对应的开关键'A'来取消对PID的 选择;同样你也可以点击未选择属性前面的开关键来选择对应的属性,比如敲击'p'来选择SWAP属性。定制完毕后回车回到top主页面,你就会看到你定制 后的结果了。

保存你的定制

如果你不想每次都在top启动后重新做定制操作,那就将你的定制保存到top的用户配置文件中。在定制后的top主页面上输入:'W',top会提示你:Wrote configuration to '/home/tonybai/.toprc,也就是说top会将你的定制保存在你的~/.toprc中。重启top看看,是否依旧是上次你定制后的结果呢^_^。

多视图

默认情况下top为我们打开了一个视图。不过top可不止支持一个视图。敲入'A'看看会发生什么?没错,你会看到上下分割的四副视图,另外在整个窗口的 左上角会出现反白的'1:Def',这是一个active视图的提示文字。反复输入'w',top会在各个视图间切换,左上角会在'1:Def'、 '2:Job'、'3:Mem'和'4:Usr'之间切换。‘1:Def'是默认视图,以CPU占用高低对task进行排序;'2:Job'这个视图看起 来比较陌生,里面展示的task多是些系统服务或内核线程;'3:Mem'视图则是以Mem占用高低对task进行排序;'4:Usr'视图则是按用户名 展示task。用'w'切换到某个视图后,可以输入'A'将该active视图放大为单视图铺满窗口。在多视图展示的情况下,还可以输入'-'来隐藏/展 示某种视图。另外这种多视图的配置也是可以保存在.toprc中的。

批处理模式

平时我们更多用的是在交互模式下运行的top,但交互模式下的数据无法记录下来,不便于事后分析,不过top的批处理模式可弥补这一不足。

执行top -b,即可让top以批处理模式运行。默认情况下top会不断重复执行,似乎批处理模式意义不大。不过我们可以限定批处理模式的运行间隔和运行次数,默认情况下top运行/更新间隔为3s,运行次数为无限制。我们可以通过一些命令行参数来设定这两个值,比如:

$> top -b  -d 1 -n 10

-d 用来设置更新间隔为1s;而-n 则设置批处理运行10次。

默认情况下top输出的task太多,我们可以通过指定相关进程或指定user来将关注面缩小,比如:

$> top -b -p 2500 -p 2501 -d 1 -n 10

这个命令只是会输出2500和2501这两个进程的相关信息。

$> top -b -u www-data -d 1 -n 10

这个命令只会输出www-data这个用户下的所有进程相关信息。

即便在批处理模式下,top依旧会输出整体负荷信息。这样一来对后续的数据后处理会带来些麻烦。一个好的方法是先定制top,再做批处理执行。比如先用 l,m,t把top的整体负荷信息都关闭掉,再定制好要关注的进程属性,保存到toprc中;之后再批处理运行top(可将输出结果重定向到某个数据文件 中),我们得到的数据就会比较规整,处理起来也十分方便了。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats