Golang 函数和 C 函数深度对比

2022年2月13日 315点热度 0人点赞 0条评论

无论是什么语言,函数都是最常被使用到的东西。

我们对比一下 Golang 和 C 这两种语言的函数实现,进而我们能真正理解以下两个问题。

  • 为什么 C 语言只能有一个返回值,而 Golang 中可以返回多个?
  • Golang 函数调用在性能上和 C 比有何差异?

一、C 语言函数深究

我们准备一段简单的函数调用代码。

#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语言中:

主要通过寄存器传递参数
所以,C 语言函数的性能杠杠的。寄存器是整个计算机体系结构中访问最最快的存储了。只有当参数数量大于 6 的时候,才开始使用栈。

固定 eax 寄存器返回数据
因为固定使用 eax 寄存器做返回数据之用,所以在 C 语言中无法支持多个返回值。我们接下来看看 Golang 是如何支持多个返回值的。

二、Golang 函数深究

同样先写一段最简单的函数调用代码。

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(12345)
}

然后查看其汇编代码。

//为了方便查看,使用-N -l 参数,能阻止编译器对汇编代码的优化
#go tool compile -S -N -l main.go > main.s

结果是这样的:

"".main STEXT size=95 args=0x0 locals=0x38
  0x000f 00015 (main.go:7)  SUBQ $56, SP  //在栈上分配56字节
  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) //第一个参数入栈
        0x0025 00037 (main.go:8)        MOVQ    $28(SP) //第二个参数入栈
        0x002e 00046 (main.go:8)        MOVQ    $316(SP) //第三个参数入栈
        0x0037 00055 (main.go:8)        MOVQ    $424(SP) //第四个参数入栈
        0x0040 00064 (main.go:8)        MOVQ    $532(SP) //第五个参数入栈
        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中:

使用栈来传递参数
栈是位于内存之中的,虽然有 CPU 中 L1、L2、L3的帮助,但平均每次访问性能仍然和寄存器没法比。所以 Golang 的函数调用开销肯定会比 C 语言要高。后面我们将用一个实验来进行量化的比较。

使用栈来返回数据
不像 C 语言那样固定使用一个 eax 寄存器,Golang 是使用栈来返回值的。这就是为啥 Golang 可以返回多个值的根本原因。

最后,性能开销对比

我们的测试方法简单粗暴,直接调用空函数 1 亿次,再统计计算平均耗时。

C函数编译运行测试:

#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

第一次执行耗时大约是 0.339 s。

但这个耗时中包含了两块。一块是函数调用开销,另外一块是 for 循环的开销(其它的代码调用因为只有 1 次,而函数调用和 for 循环都有 1 亿次,所以直接就可以忽略了)。

所以我们得减去 for 循环的开销。接着我手工注释掉对函数的调用,只是空循环 100000000 次。

int main()  
{  
    int i;  
    for(i=0; i<100000000; i++){  
    //    func(2);  
    }  
    return 0;  
}

这次总耗时是 0.314 s。

这样就计算出平均每次函数调用耗时 = (0.339s - 0.314s) / 100000000 = 0.25ns

Golang函数编译运行

func hello(a int) int {
 return 2
}

func main(){
 for i:=0; i<100000000; i++ {
  hello(1)
 }
}
# go build -gcflags="-m -l" main.go

同样采用上述方法测出平均每次函数调用耗时 = (0.302s - 0.056 s) / 100000000 = 2.46ns

可见 Golang 的函数调用性能还是比 C 要差一些。但再给大家个参考一下 PHP 的数据,之前我测过 PHP7 每次函数调用开销大约在 50 ns 左右。所以 Golang 虽然比不上 C,但总的来说性能还是不错的。


推荐阅读

福利
我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。

图片

45760Golang 函数和 C 函数深度对比

这个人很懒,什么都没留下

文章评论