为啥在这种情况下会生成不同的 go-assembler 代码?

Posted

技术标签:

【中文标题】为啥在这种情况下会生成不同的 go-assembler 代码?【英文标题】:Why is different go-assembler code generated in this case?为什么在这种情况下会生成不同的 go-assembler 代码? 【发布时间】:2021-09-22 17:40:05 【问题描述】:

生成汇编代码时注意到奇怪的事情

func foo(v uint64) (b [8]byte) 
    b[0] = byte(v)
    b[1] = byte(v >> 8)
    b[2] = byte(v >> 16)
    b[3] = byte(v >> 24)
    b[4] = byte(v >> 32)
    b[5] = byte(v >> 40)
    b[6] = byte(v >> 48)
    b[7] = byte(v >> 56)
    return b
 
func foo(v uint64) [8]byte 
    var b [8]byte

    b[0] = byte(v)
    b[1] = byte(v >> 8)
    b[2] = byte(v >> 16)
    b[3] = byte(v >> 24)
    b[4] = byte(v >> 32)
    b[5] = byte(v >> 40)
    b[6] = byte(v >> 48)
    b[7] = byte(v >> 56)
    return b

生成了这个汇编代码

"".foo STEXT nosplit size=20 args=0x10 locals=0x0 funcid=0x0
    0x0000 00000 (main.go:6)    TEXT    "".foo1(SB), NOSPLIT|ABIInternal, $0-16
    0x0000 00000 (main.go:6)    FUNCDATA    $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x0000 00000 (main.go:6)    FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x0000 00000 (main.go:6)    MOVQ    $0, "".b+16(SP)
    0x0009 00009 (main.go:15)   MOVQ    "".v+8(SP), AX
    0x000e 00014 (main.go:15)   MOVQ    AX, "".b+16(SP)
    0x0013 00019 (main.go:16)   RET

"".foo STEXT nosplit size=59 args=0x10 locals=0x10 funcid=0x0
    0x0000 00000 (main.go:6)    TEXT    "".foo(SB), NOSPLIT|ABIInternal, $16-16
    0x0000 00000 (main.go:6)    SUBQ    $16, SP
    0x0004 00004 (main.go:6)    MOVQ    BP, 8(SP)
    0x0009 00009 (main.go:6)    LEAQ    8(SP), BP
    0x000e 00014 (main.go:6)    FUNCDATA    $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x000e 00014 (main.go:6)    FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
    0x000e 00014 (main.go:6)    MOVQ    $0, "".~r1+32(SP)
    0x0017 00023 (main.go:7)    MOVQ    $0, "".b(SP)
    0x001f 00031 (main.go:16)   MOVQ    "".v+24(SP), AX
    0x0024 00036 (main.go:16)   MOVQ    AX, "".b(SP)
    0x0028 00040 (main.go:17)   MOVQ    "".b(SP), AX
    0x002c 00044 (main.go:17)   MOVQ    AX, "".~r1+32(SP)
    0x0031 00049 (main.go:17)   MOVQ    8(SP), BP
    0x0036 00054 (main.go:17)   ADDQ    $16, SP
    0x003a 00058 (main.go:17)   RET

在第二种情况下,你可以看到编译器看到有一个局部变量。为什么会这样? 为什么会生成如此不同的代码?

go version go1.16 windows/amd64

带有asm代码的文件

go tool compile -S mail.go > main.s

https://go.godbolt.org/z/G8K79K48G - 小asm代码

https://go.godbolt.org/z/Yv853E6P3 - 长asm代码

【问题讨论】:

我猜 Go 优化器的改进机会?返回值在调用者的栈帧中分配。第一个版本直接填充。第二个版本填充一个本地数组,然后将其复制给调用者。 这是一个完整的猜测,甚至不是具体的,但我认为总的来说,任何能够随时生成可读堆栈跟踪的语言都需要对可以进行哪些优化施加极端限制done 函数进入和退出。 BP 寄存器是 amd64 特定的被调用者保存,当帧大小非零时自动添加。多年来我没有看过任何 asm,但因为 b 是被调用者的本地,因此被调用者保存。其余的和之前差不多,不同的是数组被复制到调用者,栈指针被恢复 【参考方案1】:

这就是区别

func foo(v uint64) [8]byte 

func foo(v uint64) (b [8]byte) 

当您将返回指定为[8]byte 时,您只是将foo 的返回类型告知编译器。

不过,(b [8]byte)不仅通过指定返回类型,还可以

分配 8 字节内存 声明变量b,类型为[8]byteb初始化为已分配的零填充64位。

当您使用手动复制 (b [8]byte)

var b [8]byte

然后它必须手动检查上面指定的项目符号列表。

0x0000 00000 (main.go:6)    SUBQ    $16, SP
0x0004 00004 (main.go:6)    MOVQ    BP, 8(SP)
0x0009 00009 (main.go:6)    LEAQ    8(SP), BP

【讨论】:

我相信 OP 的问题更多的是“为什么这两个行为相同的函数不能针对同一个程序集进行优化?” (尽管存在现实,但人们可能会认为这是合理的期望)

以上是关于为啥在这种情况下会生成不同的 go-assembler 代码?的主要内容,如果未能解决你的问题,请参考以下文章

SQL 函数在某些情况下会出错,为啥?

为啥 AudioFlinger 在没有编程指示的情况下会失败?

为啥在某些情况下会加密 WCF SoapFault 响应?

为啥 PyQt 在没有信息的情况下会崩溃? (退出代码 0xC0000409)

为啥服务器在非多线程的情况下会同时处理多个客户端?

为啥在 pthread_detach() 之后调用 pthread_exit() 在极少数情况下会导致 SEGV?