说说逃逸分析
逃逸分析(Escape Analysis)是一种编译器优化技术,用于确定变量应该在栈上分配还是在堆上分配。逃逸分析的目的是减少程序的内存分配,提高程序的运行效率。
逃逸分析的工作原理
当编译器进行逃逸分析时,它会检查变量的生命周期和使用情况,以确定变量是否需要在函数返回后继续存在。如果变量在函数返回后仍然被引用,那么它必须在堆上分配,因为栈上的变量在函数返回时会被销毁。这种情况下,变量“逃逸”到了堆上。
逃逸分析的判断依据
编译器会根据以下条件判断变量是否需要逃逸:
- 变量被取地址:如果变量被取地址操作符
&
取地址,那么它必须在堆上分配,因为栈上的变量地址在函数返回后不再有效。 - 变量被传递给函数:如果变量作为参数传递给函数,那么它可能需要逃逸,特别是当函数是闭包或者函数的参数是接口类型时。
- 变量被闭包引用:如果变量被闭包引用,那么它必须在堆上分配,因为闭包可能会在函数返回后继续执行。
- 变量大小超过阈值:如果变量的大小超过编译器设定的阈值,编译器可能会选择在堆上分配,以避免栈溢出。
- 变量是全局变量:全局变量总是分配在堆上。
逃逸分析的好处
逃逸分析的好处包括:
- 减少内存分配:通过将变量分配在栈上,可以减少堆上的内存分配,从而减少垃圾回收的压力。
- 提高性能:栈上的内存分配和回收比堆上更快,因此逃逸分析可以提高程序的运行效率。
- 减少内存碎片:堆上的内存分配更容易导致内存碎片,而栈上的内存分配则更加连续。
逃逸分析的限制
逃逸分析虽然能够带来性能上的提升,但它也有一些限制:
- 编译器的准确性:逃逸分析依赖于编译器的分析,有时编译器可能无法准确判断变量是否需要逃逸。
- 代码的复杂性:复杂的代码逻辑可能使得逃逸分析变得更加困难。
如何查看逃逸分析的结果
在Go语言中,可以通过编译器的-m
标志来查看逃逸分析的结果。例如:
go build -gcflags "-m -m" your_program.go
这将输出编译器对变量逃逸分析的决策过程。
使用go语言自带的objdump工具反编译newInt函数
package main
func main(){
println(*newInt())
}
// go:noinline
func newInt() *int{
var a int
return &a
}
反编译指令
go tool objdump -S -s 'main.newInt$'main
-S:在旁边打印出 Go 源码。
-s :可选,匹配具体要打印的片段,支持正则表达式。
main:编译后的可执行文件。
汇编代码如下
func newInt() *int {
0x45c900 493b6610 CMPQ 0x10(R14), SP
0x45c904 7629 JBE 0x45c92f
0x45c906 4883ec18 SUBQ $0x18, SP
0x45c90a 48896c2410 MOVQ BP, 0x10(SP)
0x45c90f 488d6c2410 LEAQ 0x10(SP), BP
var a int
0x45c914 488d05854b0000 LEAQ type.*+17568(SB), AX
0x45c91b 0f1f440000 NOPL 0(AX)(AX*1)
0x45c920 e8fbeffaff CALL runtime.newobject(SB)
return &a
0x45c925 488b6c2410 MOVQ 0x10(SP), BP
0x45c92a 4883c418 ADDQ $0x18, SP
0x45c92e c3 RET
func newInt() *int {
0x45c92f e8ec85ffff CALL runtime.morestack_noctxt.abi0(SB)
0x45c934 ebca JMP main.newInt(SB)
runtime.newobject(SB)函数是go语言内置函数new()的具体实现,用来在运行阶段分配单个对象。CALL指令之后的MOVQ指令通过BP寄存器中转,把runtime.newobject()函数的返回值复制给了newInt()函数的返回值,这个返回值就是动态分配的int型变量的地址。
补充:
鄙人在这里推荐一本书《深度探索Go语言—对象模型与runtime的原理、特性及应用》
channel有缓冲和无缓冲的区别
通道(channel)可以是有缓冲的(buffered)或无缓冲的(unbuffered)。这两种类型的通道在数据传输和同步行为上有所不同。
无缓冲通道(Unbuffered Channel)
- 同步行为:无缓冲通道在发送和接收操作之间提供同步机制。当一个值被发送到无缓冲通道时,发送操作会阻塞,直到另一个协程(goroutine)从该通道接收值。同样,当从无缓冲通道接收值时,接收操作也会阻塞,直到有值被发送到该通道。
- 使用场景:无缓冲通道通常用于需要确保数据在发送和接收之间同步的场景,例如在两个协程之间同步执行点。
有缓冲通道(Buffered Channel)
- 异步行为:有缓冲通道在发送和接收操作之间提供异步机制。有缓冲通道有一个内部队列,可以存储一定数量的值。当发送值到有缓冲通道时,如果通道未满,发送操作会立即完成,不会阻塞。如果通道已满,发送操作会阻塞,直到有空间可用。从有缓冲通道接收值时,如果通道非空,接收操作会立即完成;如果通道为空,接收操作会阻塞,直到有值被发送到该通道。
- 使用场景:有缓冲通道适用于不需要即时同步的场景,例如在生产者-消费者模式中,生产者可以继续生产数据,而消费者可以按自己的节奏消费数据。
示例
// 无缓冲通道
ch := make(chan int) // 创建一个无缓冲的整型通道
// 发送操作
ch <- 10 // 如果没有协程接收,发送操作会阻塞
// 接收操作
value := <-ch // 如果没有协程发送,接收操作会阻塞
// 有缓冲通道
chBuffered := make(chan int, 3) // 创建一个容量为3的有缓冲整型通道
// 发送操作
chBuffered <- 10 // 如果通道未满,发送操作立即完成
chBuffered <- 20 // 如果通道未满,发送操作立即完成
chBuffered <- 30 // 如果通道未满,发送操作立即完成
// 接收操作
value1 := <-chBuffered // 如果通道非空,接收操作立即完成
value2 := <-chBuffered // 如果通道非空,接收操作立
map并发访问会怎么样?这个异常可以捕获吗?
ap
类型不是并发安全的。这意味着如果多个协程(goroutine)同时读写同一个map
,可能会导致数据竞争(race condition),从而导致不可预测的行为,包括数据损坏、程序崩溃等。
并发访问map
的问题
当多个协程并发访问同一个map
时,可能会出现以下问题:
- 数据竞争:如果一个协程正在写入
map
,而另一个协程同时读取或写入同一个map
,可能会导致数据竞争。 - 不一致的状态:并发读写
map
可能导致map
处于不一致的状态,这可能导致程序逻辑错误。 - 程序崩溃:在某些情况下,数据竞争可能导致程序崩溃。
异常捕获
在Go中,map
并发访问导致的数据竞争通常不能通过recover
来捕获,因为recover
只能捕获由panic
引起的异常。数据竞争导致的问题通常在运行时不可见,直到它们导致程序行为异常。
GMP模型,GMP模型中什么时候把G放全局队列?
在Go语言的调度模型中,GMP模型指的是Goroutine、M(机器线程)和P(处理器)的交互。这个模型负责管理Go程序的并发执行。在GMP模型中,全局队列(Global Queue)是用于存放等待运行的Goroutine的队列。
Goroutine何时放入全局队列
Goroutine可能会被放入全局队列的几种情况包括:
- 本地队列已满:
当一个P(处理器)的本地队列已满时,新创建的Goroutine或者从其他P的本地队列中窃取的Goroutine会被放入全局队列。这样可以防止本地队列无限增长,同时保证了全局队列中也有待运行的Goroutine。 - 工作窃取:
当一个M(机器线程)在尝试从它关联的P的本地队列中获取Goroutine时,如果本地队列为空,它会尝试从全局队列中获取Goroutine。如果全局队列也为空,它会尝试从其他P的本地队列中窃取Goroutine。 - 系统调用阻塞:
当一个Goroutine执行系统调用(如I/O操作)而阻塞时,它会被放入全局队列。这样,其他Goroutine可以继续运行,而不会因为一个阻塞的Goroutine而浪费M的资源。 - 垃圾回收:
在垃圾回收期间,为了减少垃圾回收对程序运行的影响,一些Goroutine可能会被放入全局队列,以便在垃圾回收完成后重新调度。
总结
全局队列在GMP模型中扮演着重要的角色,它确保了即使在某些P的本地队列已满或某些Goroutine被阻塞的情况下,仍然有Goroutine可以被调度执行。通过将Goroutine放入全局队列,Go的调度器可以更灵活地管理Goroutine的执行,从而提高程序的并发性能和资源利用率。
go的gc,gc扫描是并发的吗?gc中的根对象是什么?
圾回收(GC)是并发进行的,这意味着它在程序运行的同时进行,尽量减少对程序执行的影响。Go的垃圾回收器使用三色标记算法来追踪和回收不再被使用的对象。在GC过程中,根对象的扫描是并发进行的,但某些阶段需要短暂的停顿(Stop-The-World,STW)以确保一致性。
GC扫描的并发性
Go的垃圾回收器在执行过程中,会尽量利用并发来减少对程序执行的影响。GC的并发阶段包括:
- 标记阶段:在这个阶段,垃圾回收器会并发地标记所有可达的对象。这个阶段是并发进行的,以减少对程序执行的影响。
- 清扫阶段:在标记阶段完成后,清扫阶段会回收那些未被标记为可达的对象。这个阶段通常也是并发进行的。
根对象
在Go的垃圾回收中,根对象(Root Objects)是垃圾回收器在标记过程中最先检查的对象。根对象是程序中直接或间接引用的全局变量、栈上的变量、寄存器中的指针等。根对象是垃圾回收的起点,因为它们是程序中活跃引用的源头。
根对象包括:
- 全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
- 执行栈:每个goroutine都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。
- 寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。
总结
Go的垃圾回收器使用并发的三色标记算法来追踪和回收不再被使用的对象。根对象是垃圾回收的起点,包括全局变量、执行栈和寄存器中的指针。尽管GC的大部分过程是并发进行的,但为了保证一致性,某些阶段仍然需要短暂的STW停顿。通过这种方式,Go的垃圾回收器能够在保证程序性能的同时,有效地管理内存。