原文地址:https://www.jianshu.com/p/d70e6f6b13c8

本文只是对原文的简单总结,查看详细信息请移步原文


前言

为什么选择GO语言

最常见的回答:

  • Concurrency (并发)
  • Ease of deployment (部署容易)
  • Performance (性能)

下面我们来讨论下对Go语言性能有所帮助的5个特性


1. 变量的处理和存储

  • 存储:以 int32 为例,Go中占4字节,Python中占24字节,Java虽然int类型也占用4字节,但是在List或者Map这样的集合里存储int的话,编译器会把它转换成Integer对象(32位JVM上占16字节,64位JVM上占24字节)
  • Go语言让你定义非常紧凑的数据结构,避免了无谓的指针跳转。紧凑的数据结构能够使缓存更加有效。高效的缓存最终给我们带来了更高的效率。

2. 函数调用不是免费的

函数调用的三个步骤:

  • 创建一个新的堆栈框(stack frame)并把调用者的详细信息记录下来。
  • 把任何会被被调用函数用到的寄存器内容保存到堆栈。
  • 计算被调用函数的地址,并执行跳转指令到那个新的地址。

内联(inlining): Go语言的实现非常简单。当一个包(package)被编译的时候,任何适合内联的小函数都被标记并且按正常情况编译。然后将源代码和编译后的二进制同时保存下来。Go编译器能够自动在多个文件或者包(package)之间实现函数内联。如果某些代码调用了来自标准库的可内联函数,Go编译器同样可以将这些函数内联进来。


3. 垃圾回收机制(Garbage Collection)

  • 分配在堆(Heap)上的需要垃圾回收,分配在栈(Stack)上的不需要垃圾回收;

  • 无论垃圾回收机制多么高效,栈的分配总是比堆的分配要快;

  • 逃逸分析(Escape Analysis)技术能自动判断是否需要在堆(Heap)上分配空间,如果不需要的话就分配在栈(Stack)上,即使是使用new,make等创建的

  • 三色标记算法:go 1.5 版本开始采用 “非分代的、非移动的、并发的、三色的标记清除垃圾收集器”


4. Goroutines

开销大小:进程 > 线程(共享内存) > 协程

每个Go进程只需要少量的操作系统线程。Go的运行环境来负责将可运行的goroutine分配到空闲的操作系统线程上


5. Goroutine的栈管理

  • 保护页机制:Go语言没有使用保护页机制。Go的编译器会在每个函数调用的时候插入一段代码来检查是否有足够的栈空间来运行被调用的函数。如果空间不足,Go的运行环境就会分配更多的栈空间。因为有了这个检查机制,一个goroutine的初始栈可以很小。这样Go程序员就可以把goroutine作为相对廉价的资源来使用。

  • 热分裂问题(hot split problem):当G调用H的时候,没有足够的栈空间来让H运行,这时候Go运行环境就会从堆里分配一个新的栈内存块去让H运行。在H返回到G之前,新分配的内存块被释放回堆。这种管理栈的方法一般都工作得很好。但对有些代码,特别是递归调用,它会造成程序不停地分配和释放新的内存空间。举个例子,在一个程序里,函数G会在一个循环里调用很多次H函数。每次调用都会分配一块新的内存空间。这就是热分裂问题(hot split problem)。

  • 解决方法:Go 1.3采用了新的栈管理方法。如果goroutine的栈太小了,它会去分配一块新的更大的栈,而不是分配和释法额外的内存空间。老的栈里的内容被复制到新的栈里,goroutine会在新的栈上继续执行。在第一次调用H函数之后,将会有足够大的栈空间,这样以后的栈空间大小检查都不会有问题了。这就解决了热分裂问题。


总结:

这些特性个个都很有效,他们之间还相互依赖。比如,如果没有了可衍生的栈,运行环境将多个goroutine复用到线程上面就不会很有效。内联在把多个小函数合并成大函数的时候也避免了栈大小的检查开销。逃逸分析用栈代替堆来存储局部变量,这样也减少来垃圾回收机制的压力。逃逸分析还提升了缓存的性能(cache locality)。没有可衍生的栈,逃逸分析又会对栈造成很大的压力。

11-07 03:30