为什么cgo的性能如此之慢?我的测试代码有问题吗?

22

我正在做一个测试:比较cgo和纯Go函数每个运行1亿次所需的执行时间。与Golang函数相比,cgo函数需要更长的时间,这个结果让我感到困惑。我的测试代码如下:

package main

import (
    "fmt"
    "time"
)

/*
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void show() {

}

*/
// #cgo LDFLAGS: -lstdc++
import "C"

//import "fmt"

func show() {

}

func main() {
    now := time.Now()
    for i := 0; i < 100000000; i = i + 1 {
        C.show()
    }
    end_time := time.Now()

    var dur_time time.Duration = end_time.Sub(now)
    var elapsed_min float64 = dur_time.Minutes()
    var elapsed_sec float64 = dur_time.Seconds()
    var elapsed_nano int64 = dur_time.Nanoseconds()
    fmt.Printf("cgo show function elasped %f minutes or \nelapsed %f seconds or \nelapsed %d nanoseconds\n",
        elapsed_min, elapsed_sec, elapsed_nano)

    now = time.Now()
    for i := 0; i < 100000000; i = i + 1 {
        show()
    }
    end_time = time.Now()

    dur_time = end_time.Sub(now)
    elapsed_min = dur_time.Minutes()
    elapsed_sec = dur_time.Seconds()
    elapsed_nano = dur_time.Nanoseconds()
    fmt.Printf("go show function elasped %f minutes or \nelapsed %f seconds or \nelapsed %d nanoseconds\n",
        elapsed_min, elapsed_sec, elapsed_nano)

    var input string
    fmt.Scanln(&input)
}

结果是:

cgo show function elasped 0.368096 minutes or 
elapsed 22.085756 seconds or 
elapsed 22085755775 nanoseconds

go show function elasped 0.000654 minutes or 
elapsed 0.039257 seconds or 
elapsed 39257120 nanoseconds

结果显示调用 C 函数比调用 Go 函数慢。我的测试代码有问题吗?

我的系统是:Mac OS X 10.9.4(13E28)


你为什么认为从Go调用C函数比从Go调用Go函数更快呢? - Volker
我期望Go代码会内联show()的Go版本,这是Go代码比C.show()更具优势的进一步体现。 - Nick Craig-Wood
2
这个内容有点过时,但是被接受的答案所描述的性能损失仍然存在,而且成本大致相同(恒定)。问题中的示例夸大了成本 - 很多! - 这是因为所讨论的函数正在进行内联。这不是一个苹果对苹果的比较。 - BadZen
4个回答

45

正如你所发现的那样,通过CGo调用C/C++代码存在相当高的开销。因此,通常情况下,你最好尽量减少使用CGo调用的数量。对于上面的示例,与其在循环中反复调用CGo函数,不如将循环移到C中。

Go运行时设置线程的几个方面可能会破坏许多C代码的预期:

  1. 协程在相对较小的堆栈上运行,通过分段堆栈(旧版本)或拷贝(新版本)来处理堆栈增长。
  2. 由Go运行时创建的线程可能无法与libpthread的本地存储实现正确交互。
  3. Go运行时的UNIX信号处理程序可能会干扰传统的C或C++代码。
  4. Go重用操作系统线程来运行多个协程。如果C代码调用了阻塞的系统调用或以其他方式垄断了线程,则可能会对其他协程产生不利影响。

由于这些原因,CGo选择安全的方法,在一个具有传统堆栈的单独线程中运行C代码。

如果你来自像Python这样的语言,重新编写热点代码作为加速程序的一种方式并不罕见,那么你会感到失望。但同时,等效的C和Go代码之间性能差距较小。

通常情况下,我将CGo保留用于与现有库进行交互,可能使用小型C包装函数来减少我需要从Go调用的数量。


谢谢,这确实帮了我很多! - 习明昊
1
这可能已经过时了:https://groups.google.com/forum/#!topic/golang-nuts/RTtMsgZi88Q - gavv

29

更新 James 的答案:当前实现中似乎没有线程切换。

请参考 golang-nuts 上的 这个帖子

总会有一些开销。 它比一个简单的函数调用更昂贵,但是比上下文切换更便宜 (agl 记得早期版本中有一次实现; 我们在公开发布之前取消了线程切换)。 目前的开销基本上只涉及到整个寄存器集合的切换(没有内核参与)。 我猜想这相当于十次函数调用。

此外,参考 这个答案,它链接了 "cgo is not Go" 博客文章。

C 并不知道 Go 的调用约定或可增长的堆栈,因此向 C 代码调用必须记录 goroutine 堆栈的所有细节,切换到 C 栈并运行 C 代码,它不知道如何调用或管理程序的较大 Go 运行时。

因此,cgo 的开销是由于它执行了堆栈切换,而不是线程切换。

当调用 C 函数时,它保存和恢复 所有 寄存器,而在调用 Go 函数或汇编函数时则不需要这样做。


除此之外,cgo 的调用约定禁止直接将 Go 指针传递给 C 代码,常见的解决方法是使用 C.malloc,从而引入额外的内存分配。请参阅这个问题获取详细信息。


-1

我支持gavv,

在Windows上:

/*
#include "stdio.h"
#include  <Windows.h>

unsigned long CTid(void){
    return GetCurrentThreadId();
}

*/
import "C"
import (
    "fmt"
    "time"

    "golang.org/x/sys/windows"
)

func main() {

    fmt.Println(uint32(C.CTid()))

    fmt.Println(windows.GetCurrentThreadId())

    time.Sleep(time.Second * 5)
}

go和cgo获取相同的TID。


-4

从Go调用C函数会有一些开销,这是无法改变的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接