Golang 函數(shù)和 C 函數(shù)深度對比
無論是什么語言,函數(shù)都是最常被使用到的東西。
我們對比一下 Golang 和 C 這兩種語言的函數(shù)實現(xiàn),進而我們能真正理解以下兩個問題。
為什么 C 語言只能有一個返回值,而 Golang 中可以返回多個? Golang 函數(shù)調(diào)用在性能上和 C 比有何差異?
一、C 語言函數(shù)深究
我們準備一段簡單的函數(shù)調(diào)用代碼。
#include <stdio.h>
int func(int p){
return 1;
}
int main()
{
int i;
for(i=0; i<100000000; i++){
func(2);
}
return 0;
}
用 gcc 來查看下匯編代碼。
# gcc -S main.c
匯編源碼如下:
func:
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl %edx, -12(%rbp)
movl %ecx, -16(%rbp)
movl %r8d, -20(%rbp)
movl $1, %eax
main:
movl $5, %r8d
movl $4, %ecx
movl $3, %edx
movl $2, %esi
movl $1, %edi
call func
movl $0, %eax
可以看到,在C語言中:
主要通過寄存器傳遞參數(shù)
所以,C 語言函數(shù)的性能杠杠的。寄存器是整個計算機體系結(jié)構(gòu)中訪問最最快的存儲了。只有當(dāng)參數(shù)數(shù)量大于 6 的時候,才開始使用棧。
固定 eax 寄存器返回數(shù)據(jù)
因為固定使用 eax 寄存器做返回數(shù)據(jù)之用,所以在 C 語言中無法支持多個返回值。我們接下來看看 Golang 是如何支持多個返回值的。
二、Golang 函數(shù)深究
同樣先寫一段最簡單的函數(shù)調(diào)用代碼。
package main
func myFunction(p1, p2, p3,p4, p5 int) (int,int) {
var a int = p1+p2+p3+p4+p5
var b int = 3
return a,b
}
func main() {
myFunction(1, 2, 3, 4, 5)
}
然后查看其匯編代碼。
//為了方便查看,使用-N -l 參數(shù),能阻止編譯器對匯編代碼的優(yōu)化
#go tool compile -S -N -l main.go > main.s
結(jié)果是這樣的:
"".main STEXT size=95 args=0x0 locals=0x38
0x000f 00015 (main.go:7) SUBQ $56, SP //在棧上分配56字節(jié)
0x0013 00019 (main.go:7) MOVQ BP, 48(SP) //保存BP
0x0018 00024 (main.go:7) LEAQ 48(SP), BP
0x001d 00029 (main.go:8) MOVQ $1, (SP) //第一個參數(shù)入棧
0x0025 00037 (main.go:8) MOVQ $2, 8(SP) //第二個參數(shù)入棧
0x002e 00046 (main.go:8) MOVQ $3, 16(SP) //第三個參數(shù)入棧
0x0037 00055 (main.go:8) MOVQ $4, 24(SP) //第四個參數(shù)入棧
0x0040 00064 (main.go:8) MOVQ $5, 32(SP) //第五個參數(shù)入棧
0x0049 00073 (main.go:8) CALL "".myFunction(SB)
"".myFunction STEXT nosplit size=99 args=0x38 locals=0x18
0x000e 00014 (main.go:3) MOVQ $0, "".~r5+72(SP)
0x0017 00023 (main.go:3) MOVQ $0, "".~r6+80(SP)
0x0020 00032 (main.go:4) MOVQ "".p1+32(SP), AX
0x0025 00037 (main.go:4) ADDQ "".p2+40(SP), AX
0x002a 00042 (main.go:4) ADDQ "".p3+48(SP), AX
0x002f 00047 (main.go:4) ADDQ "".p4+56(SP), AX
0x0034 00052 (main.go:4) ADDQ "".p5+64(SP), AX
0x004b 00075 (main.go:6) MOVQ AX, "".~r5+72(SP)
0x0054 00084 (main.go:6) MOVQ AX, "".~r6+80(SP)
可以看到,在Golang中:
使用棧來傳遞參數(shù)
棧是位于內(nèi)存之中的,雖然有 CPU 中 L1、L2、L3的幫助,但平均每次訪問性能仍然和寄存器沒法比。所以 Golang 的函數(shù)調(diào)用開銷肯定會比 C 語言要高。后面我們將用一個實驗來進行量化的比較。
使用棧來返回數(shù)據(jù)
不像 C 語言那樣固定使用一個 eax 寄存器,Golang 是使用棧來返回值的。這就是為啥 Golang 可以返回多個值的根本原因。
最后,性能開銷對比
我們的測試方法簡單粗暴,直接調(diào)用空函數(shù) 1 億次,再統(tǒng)計計算平均耗時。
C函數(shù)編譯運行測試:
#include <stdio.h>
int func(int p){
return 1;
}
int main()
{
int i;
for(i=0; i<100000000; i++){
func(2);
}
return 0;
}
# gcc main.c -o main
# time ./main
第一次執(zhí)行耗時大約是 0.339 s。
但這個耗時中包含了兩塊。一塊是函數(shù)調(diào)用開銷,另外一塊是 for 循環(huán)的開銷(其它的代碼調(diào)用因為只有 1 次,而函數(shù)調(diào)用和 for 循環(huán)都有 1 億次,所以直接就可以忽略了)。
所以我們得減去 for 循環(huán)的開銷。接著我手工注釋掉對函數(shù)的調(diào)用,只是空循環(huán) 100000000 次。
int main()
{
int i;
for(i=0; i<100000000; i++){
// func(2);
}
return 0;
}
這次總耗時是 0.314 s。
這樣就計算出平均每次函數(shù)調(diào)用耗時 = (0.339s - 0.314s) / 100000000 = 0.25ns
Golang函數(shù)編譯運行
func hello(a int) int {
return 2
}
func main(){
for i:=0; i<100000000; i++ {
hello(1)
}
}
# go build -gcflags="-m -l" main.go
同樣采用上述方法測出平均每次函數(shù)調(diào)用耗時 = (0.302s - 0.056 s) / 100000000 = 2.46ns
可見 Golang 的函數(shù)調(diào)用性能還是比 C 要差一些。但再給大家個參考一下 PHP 的數(shù)據(jù),之前我測過 PHP7 每次函數(shù)調(diào)用開銷大約在 50 ns 左右。所以 Golang 雖然比不上 C,但總的來說性能還是不錯的。
推薦閱讀
