1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-24 12:32:21 +00:00

Merge pull request #156 from fuwensun/pr3-5-5

ch3-05-fix typo
This commit is contained in:
chai2010 2018-06-29 19:04:37 +08:00 committed by GitHub
commit 41d0c2deb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,6 +1,6 @@
# 3.6. 再论函数
在前面的章节中我们已经简单讨论过Go的汇编函数但是那些主要是叶子函数。叶子函数的最大特点是不会调用其他函数也就是栈的大小是可以预期的叶子函数也就是可以基本忽略爆栈的问题如果已经爆了那也是上级函数的问题。如果没有爆栈问题那么也就是不会有栈的分裂问题如果没有栈的分裂也就不需要移动栈上的指针也就不会有栈上指针管理的问题。但是是现实中Go语言的函数是可以任意深度调用的永远不用担心爆栈的风险。那么这些近似黑科技的特是如何通过低级的汇编语言实现的呢?这些都是本节尝试讨论的问题。
在前面的章节中我们已经简单讨论过Go的汇编函数但是那些主要是叶子函数。叶子函数的最大特点是不会调用其他函数也就是栈的大小是可以预期的叶子函数也就是可以基本忽略爆栈的问题如果已经爆了那也是上级函数的问题。如果没有爆栈问题那么也就是不会有栈的分裂问题如果没有栈的分裂也就不需要移动栈上的指针也就不会有栈上指针管理的问题。但是是现实中Go语言的函数是可以任意深度调用的永远不用担心爆栈的风险。那么这些近似黑科技的特是如何通过低级的汇编语言实现的呢?这些都是本节尝试讨论的问题。
## 递归函数: 1到n求和
@ -16,7 +16,7 @@ func sum(n int) int {
}
```
然后通过if/goto构型重新上面的递归函数,以便于转义为汇编版本:
然后通过if/goto重构上面的递归函数,以便于转义为汇编版本:
```go
func sum(n int) (result int) {
@ -40,7 +40,7 @@ L_END:
}
```
在改写之后,递归调用的参数需要引入局部变量,保存中间结果也需要引入局部变量。而通过栈来保存中间的调用状态正是递归函数的核心。因为输入参数也在栈上,因为我们可以通过输入参数来保存少量的状态。同时我们模拟定义了AX和BX寄存器寄存器在使用前需要初始化并且在函数调用后也需要重新初始化。
在改写之后,递归调用的参数需要引入局部变量,保存中间结果也需要引入局部变量。而通过栈来保存中间的调用状态正是递归函数的核心。因为输入参数也在栈上,所以我们可以通过输入参数来保存少量的状态。同时我们模拟定义了AX和BX寄存器寄存器在使用前需要初始化并且在函数调用后也需要重新初始化。
下面继续改造为汇编语言版本:
@ -72,13 +72,13 @@ L_END:
在汇编版本函数中并没有定义局部变量只有用于调用自身的临时栈空间。因为函数本身的参数和返回值有16个字节因此栈帧的大小也为16字节。L_STEP_TO_END标号部分用于处理递归调用是函数比较复杂的部分。L_END用于处理递归终结的部分。
调用sum函数的参数在`0(SP)`位置,调用结束后的返回值在`8(SP)`位置。在函数调用之后要需要重新为需要的寄存器注入值,因为被调用的函数内部很可能会破坏了寄存器的状态。同时调用函数的参数值也可信任的,输入参数也可能在被调用函数内部被修改了
调用sum函数的参数在`0(SP)`位置,调用结束后的返回值在`8(SP)`位置。在函数调用之后要需要重新为需要的寄存器注入值,因为被调用的函数内部很可能会破坏了寄存器的状态。同时调用函数的参数值也是不可信任的,输入参数也可能在被调用函数内部被修改了。
总得来说用汇编实现递归函数和普通函数并没有什么区别当然是在没有考虑爆栈的前提下。我们的函数应该可以对较小的n进行求和但是当n大到一定也就是栈达到一定的深度必然会出现爆栈的问题。爆栈是C语言的特性不应该在哪怕是Go汇编语言中出现。
总得来说用汇编实现递归函数和普通函数并没有什么区别当然是在没有考虑爆栈的前提下。我们的函数应该可以对较小的n进行求和但是当n大到一定也就是栈达到一定的深度必然会出现爆栈的问题。爆栈是C语言的特性不应该在哪怕是Go汇编语言中出现。
## 栈的扩容和收缩
Go语言的编译器在生成函数的机器代码时会在开头插入小段代码。插入的代码可以做很多事情包括触发runtime.Gosched进行协作式调度还包括栈的动态增长等。其实栈等扩容工作主要在runtime包的runtime·morestack_noctxt函数实现这是一个底层函数只有汇编层面才可以调用。
Go语言的编译器在生成函数的机器代码时会在开头插入小段代码。插入的代码可以做很多事情包括触发runtime.Gosched进行协作式调度还包括栈的动态增长等。其实栈等扩容工作主要在runtime包的runtime·morestack_noctxt函数实现,这是一个底层函数,只有汇编层面才可以调用。
在新版本的sum汇编函数中我们在开头和末尾都引入了部分代码
@ -102,7 +102,7 @@ L_MORE_STK:
其中NO_LOCAL_POINTERS表示没有局部指针。因为新引入的代码可能导致调用runtime·morestack_noctxt函数而栈的扩容必然要涉及函数参数和局部编指针的调整如果缺少局部指针信息将导致扩容工作无法进行。不仅仅是栈的扩容需要函数的参数和局部指针标记表格在GC进行垃圾回收时也将需要。函数的参数和返回值的指针状态可以通过在Go语言中的函数声明中获取函数的局部变量则需要手工指定。因为手工指定指针表格是一个非常繁琐的工作因此一般要避免在手写汇编中出现局部指针。
喜欢深究的读者可能会有一个问题:如果进行垃圾回收或栈调整时,寄存器中的指针时如何维护的前文说过Go语言的函数调用时通过栈进行传递参数的,并没有使用寄存器传递参数。同时函数调用之后所有的寄存器视为失效。因此在调整和维护指针时,只需要扫描内存中的指针数据,寄存器中的数据在垃圾回收器函数返回后都需要重新加载,因此寄存器是不需要扫描的。
喜欢深究的读者可能会有一个问题:如果进行垃圾回收或栈调整时,寄存器中的指针是如何维护的前文说过Go语言的函数调用是通过栈进行传递参数的,并没有使用寄存器传递参数。同时函数调用之后所有的寄存器视为失效。因此在调整和维护指针时,只需要扫描内存中的指针数据,寄存器中的数据在垃圾回收器函数返回后都需要重新加载,因此寄存器是不需要扫描的。
在Go语言的Goroutine实现中每个TlS线程局部变量会保存当前Goroutine的信息结构体的指针。通过`MOVQ TLS, CX``MOVQ 0(CX)(TLS*1), AX`两条指令将表示当前Goroutine信息的g结构体加载到CX寄存器。g结构体在`$GOROOT/src/runtime/runtime2.go`文件定义,开头的结构成员如下:
@ -135,9 +135,9 @@ type stack struct {
}
```
在g结构体中的stackguard0成员是出现爆栈前的警戒线。stackguard0的偏移量是16个字节因此上述代码中的`CMPQ SP, 16(AX)`表示将当前的真实SP和爆栈警戒线比较如果超出警戒线则表示需要进行栈扩容也就是跳转到L_MORE_STK。在L_MORE_STK标号处线调用runtime·morestack_noctxt进行栈扩容然后又跳回到函数到开始位置此时此刻函数到栈已经调整了。然后再进行一次栈大小到检测,如果依然不足则继续扩容,直到栈足够大为止。
在g结构体中的stackguard0成员是出现爆栈前的警戒线。stackguard0的偏移量是16个字节因此上述代码中的`CMPQ SP, 16(AX)`表示将当前的真实SP和爆栈警戒线比较如果超出警戒线则表示需要进行栈扩容也就是跳转到L_MORE_STK。在L_MORE_STK标号处线调用runtime·morestack_noctxt进行栈扩容然后又跳回到函数到开始位置此时此刻函数的栈已经调整了。然后再进行一次栈大小的检测,如果依然不足则继续扩容,直到栈足够大为止。
以上是栈的扩容但是栈到收缩是在何时处理到呢我们知道Go运行时会定期进行垃圾回收操作这其中栈的回收工作。如果栈使用到比例小于一定到阈值则分配一个较小到栈空间然后将栈上面到数据移动到新的栈中栈移动的过程和栈扩容的过程类似。
以上是栈的扩容但是栈到收缩是在何时处理到呢我们知道Go运行时会定期进行垃圾回收操作这其中包含栈的回收工作。如果栈使用到比例小于一定到阈值,则分配一个较小到栈空间,然后将栈上面到数据移动到新的栈中,栈移动的过程和栈扩容的过程类似。
## PCDATA和FUNCDATA