>  기사  >  백엔드 개발  >  Go의 새로운 함수 호출 규칙은 얼마나 더 빨라질 수 있나요?

Go의 새로운 함수 호출 규칙은 얼마나 더 빨라질 수 있나요?

Go语言进阶学习
Go语言进阶学习앞으로
2023-07-21 13:18:39914검색
컴파일러와 런타임을 지속적으로 최적화하면 Go 프로그램의 구성과 실행이 더 원활해집니다. Go 1.18 릴리스 노트에서 Chopper는 Go의 새로운 함수 호출 규칙(레지스터 기반)이 arm64 아키텍처(amd64는 이미 지원됨)로 확장되고 성능이 10% 이상 향상될 것이라는 점을 발견했습니다. 를 향해서.

이 글에서는 함수 호출 규칙의 변경이 Go에 얼마나 많은 이점을 가져올 수 있는지 살펴봅니다.

Go의 새로운 함수 호출 규칙은 얼마나 더 빨라질 수 있나요?

함수 호출 규칙

문서 Go 함수 호출 규칙(이 섹션에 익숙하지 않은 독자는 이 문서를 먼저 읽는 것이 좋습니다)에서 함수 호출 규칙에 대해 논의했습니다. Go 언어의

소위 함수 호출 규칙은 함수 호출자와 호출 수신자가 준수해야 하는 특정 계약을 말하며 주로 함수의 매개변수를 전달하는 방법, 전달하는 순서 등을 포함합니다.

매개변수 전달 방법은 일반적으로 레지스터 전달과 스택 전달의 두 가지 상황으로 구분됩니다.

Go 1.17 이전에는 Go 언어는 서로 다른 CPU 레지스터 간의 차이를 피하기 위해 스택 전송을 사용했습니다. 이 접근 방식의 가장 큰 장점은 구현이 간단하고 컴파일러 유지 관리가 쉽다는 것입니다. 그러나 단점도 분명합니다. 일부 성능이 희생됩니다. CPU 액세스 레지스터의 속도는 메모리의 속도보다 훨씬 높기 때문입니다.

改变

基于性能考虑,寄存器的调用惯例,是大多数语言采纳的方式。Go 也准备做点改变,在 1.17 版本中,对于 linux/amd64, darwin/amd64, windows/amd64 系统,首先实现了新的基于寄存器的调用惯例。

package main

//go:noinline
func add(i, j int) int {
 return i + j
}

func main() {
 add(100, 200)
}

我们在 darwin/amd64 系统上,分别使用 Go 1.17 和 Go 1.16 的代码进行编译,得到它们的汇编语句分别如下。

Go 1.17 汇编语句

$ go version
go version go1.17 darwin/amd64
$ go tool compile -S main.go
"".add STEXT nosplit size=4 args=0x10 locals=0x0 funcid=0x0
 0x0000 00000 (main.go:4) TEXT "".add(SB), NOSPLIT|ABIInternal, $0-16
 0x0000 00000 (main.go:4) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
 0x0000 00000 (main.go:4) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
 0x0000 00000 (main.go:4) FUNCDATA $5, "".add.arginfo1(SB)
 0x0000 00000 (main.go:5) ADDQ BX, AX
 0x0003 00003 (main.go:5) RET
 0x0000 48 01 d8 c3                                      H...
"".main STEXT size=54 args=0x0 locals=0x18 funcid=0x0
 0x0000 00000 (main.go:8) TEXT "".main(SB), ABIInternal, $24-0
 0x0000 00000 (main.go:8) CMPQ SP, 16(R14)
 0x0004 00004 (main.go:8) PCDATA $0, $-2
 0x0004 00004 (main.go:8) JLS 47
 0x0006 00006 (main.go:8) PCDATA $0, $-1
 0x0006 00006 (main.go:8) SUBQ $24, SP
 0x000a 00010 (main.go:8) MOVQ BP, 16(SP)
 0x000f 00015 (main.go:8) LEAQ 16(SP), BP
 0x0014 00020 (main.go:8) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
 0x0014 00020 (main.go:8) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
 0x0014 00020 (main.go:9) MOVL $100, AX
 0x0019 00025 (main.go:9) MOVL $200, BX
 0x001e 00030 (main.go:9) PCDATA $1, $0
 0x001e 00030 (main.go:9) NOP
 0x0020 00032 (main.go:9) CALL "".add(SB)
 0x0025 00037 (main.go:10) MOVQ 16(SP), BP
 0x002a 00042 (main.go:10) ADDQ $24, SP
 0x002e 00046 (main.go:10) RET
 0x002f 00047 (main.go:10) NOP
 0x002f 00047 (main.go:8) PCDATA $1, $-1
 0x002f 00047 (main.go:8) PCDATA $0, $-2
 0x002f 00047 (main.go:8) CALL runtime.morestack_noctxt(SB)
 0x0034 00052 (main.go:8) PCDATA $0, $-1
 0x0034 00052 (main.go:8) JMP 0
 ...

Go 1.16 汇编语句

$ go1.16.4 version
go version go1.16.4 darwin/amd64
$ go1.16.4 tool compile -S main.go
"".add STEXT nosplit size=19 args=0x18 locals=0x0 funcid=0x0
 0x0000 00000 (main.go:4) TEXT "".add(SB), NOSPLIT|ABIInternal, $0-24
 0x0000 00000 (main.go:4) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
 0x0000 00000 (main.go:4) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
 0x0000 00000 (main.go:5) MOVQ "".j+16(SP), AX
 0x0005 00005 (main.go:5) MOVQ "".i+8(SP), CX
 0x000a 00010 (main.go:5) ADDQ CX, AX
 0x000d 00013 (main.go:5) MOVQ AX, "".~r2+24(SP)
 0x0012 00018 (main.go:5) RET
 0x0000 48 8b 44 24 10 48 8b 4c 24 08 48 01 c8 48 89 44  H.D$.H.L$.H..H.D
 0x0010 24 18 c3                                         $..
"".main STEXT size=71 args=0x0 locals=0x20 funcid=0x0
 0x0000 00000 (main.go:8) TEXT "".main(SB), ABIInternal, $32-0
 0x0000 00000 (main.go:8) MOVQ (TLS), CX
 0x0009 00009 (main.go:8) CMPQ SP, 16(CX)
 0x000d 00013 (main.go:8) PCDATA $0, $-2
 0x000d 00013 (main.go:8) JLS 64
 0x000f 00015 (main.go:8) PCDATA $0, $-1
 0x000f 00015 (main.go:8) SUBQ $32, SP
 0x0013 00019 (main.go:8) MOVQ BP, 24(SP)
 0x0018 00024 (main.go:8) LEAQ 24(SP), BP
 0x001d 00029 (main.go:8) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
 0x001d 00029 (main.go:8) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
 0x001d 00029 (main.go:9) MOVQ $100, (SP)
 0x0025 00037 (main.go:9) MOVQ $200, 8(SP)
 0x002e 00046 (main.go:9) PCDATA $1, $0
 0x002e 00046 (main.go:9) CALL "".add(SB)
 0x0033 00051 (main.go:10) MOVQ 24(SP), BP
 0x0038 00056 (main.go:10) ADDQ $32, SP
 0x003c 00060 (main.go:10) RET
 0x003d 00061 (main.go:10) NOP
 0x003d 00061 (main.go:8) PCDATA $1, $-1
 0x003d 00061 (main.go:8) PCDATA $0, $-2
 0x003d 00061 (main.go:8) NOP
 0x0040 00064 (main.go:8) CALL runtime.morestack_noctxt(SB)
 0x0045 00069 (main.go:8) PCDATA $0, $-1
 0x0045 00069 (main.go:8) JMP 0

看到这么多汇编代码,不要紧张。这里我们需要留意的就以下这么几行

// Go 1.17 汇编参数调用代码
"".add STEXT nosplit size=4 args=0x10 locals=0x0 funcid=0x0
...
0x0000 00000 (main.go:5) ADDQ BX, AX
...
"".main STEXT size=54 args=0x0 locals=0x18 funcid=0x0
...
 0x0014 00020 (main.go:9) MOVL $100, AX
 0x0019 00025 (main.go:9) MOVL $200, BX
 0x001e 00030 (main.go:9) PCDATA $1, $0
 0x001e 00030 (main.go:9) NOP
 0x0020 00032 (main.go:9) CALL "".add(SB)
...

// Go 1.16 汇编参数调用代码
"".add STEXT nosplit size=19 args=0x18 locals=0x0 funcid=0x0
...
 0x0000 00000 (main.go:5) MOVQ "".j+16(SP), AX
 0x0005 00005 (main.go:5) MOVQ "".i+8(SP), CX
 0x000a 00010 (main.go:5) ADDQ CX, AX
 0x000d 00013 (main.go:5) MOVQ AX, "".~r2+24(SP)
...
"".main STEXT size=71 args=0x0 locals=0x20 funcid=0x0
...
 0x001d 00029 (main.go:9) MOVQ $100, (SP)
 0x0025 00037 (main.go:9) MOVQ $200, 8(SP)
 0x002e 00046 (main.go:9) PCDATA $1, $0
 0x002e 00046 (main.go:9) CALL "".add(SB)
...

看出差异了吗?

在 Go 1.17 的汇编代码中,参数值 100 和 200 直接基于寄存器 AX 和 BX 来操作。而 Go 1.16 中,参数值是通过指向栈顶的栈指针寄存器SP的偏移量来表示和传递的。

在 Go 1.17 的release notes中,编译器的此项改变会让 Go 程序运行性能和二进制大小两个方面得到优化,

二进制大小

首先,我们比较编译后的二进制大小。

$ go build -o main1.17 main.go
$ go1.16.4 build -o main1.16 main.go
$ ls -al main1.17 main1.16
-rwxr-xr-x  1 slp  staff  1200640 Dec 26 21:09 main1.16
-rwxr-xr-x  1 slp  staff  1142208 Dec 26 21:09 main1.17

可以看出,Go 1.17 基于寄存器传递的函数调用惯例编译出的二进制,相较于 Go 1.16 基于栈传递的减少 4.8% 的大小。

性能

通过 benchmark 比较程序执行效率

// Go 1.17
$ go test -bench=.
goos: darwin
goarch: amd64
pkg: workspace/add
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
BenchmarkIt-8    918887481          1.257 ns/op
PASS
ok   workspace/add 1.299s

// Go 1.16
$ go1.16.4 test -bench=.
goos: darwin
goarch: amd64
pkg: workspace/add
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
BenchmarkIt-8    801041754          1.469 ns/op
PASS
ok   workspace/add 1.336s

从 1.469 ns/op 提升至 1.257 ns/op,大约提升了 14%。

总结

我们常谈论到,Go 是在不断优化迭代的,我们值得期待与建设更好的 Go 语言。

스택 기반 전송의 성능 손실을 줄이기 위해 Go 1.17부터 레지스터 전송 기반 컴파일 변경 사항이 도입되었으며, 이는 현재 amd64 플랫폼만 지원합니다. 하지만 Go 1.18에서는 arm64, ppc64 및 ppc64le 플랫폼에 대한 지원이 확장됩니다.

Go의 릴리스 노트에서 언급했듯이 새로운 함수 호출 규칙은 두 가지 측면에서 개선을 가져옵니다. 즉, 컴파일된 바이너리 크기가 더 작아지고 실행 효율성이 향상됩니다. 동시에 기존 어셈블리 함수와의 호환성을 유지하기 위해 컴파일러는 이전 호출 규칙과 새 호출 규칙 간에 변환하는 어댑터 함수를 생성합니다.

위 내용은 Go의 새로운 함수 호출 규칙은 얼마나 더 빨라질 수 있나요?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 Go语言进阶学习에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제