【问题标题】:Why cgo's performance is so slow? is there something wrong with my testing code?为什么cgo的性能这么慢?我的测试代码有问题吗?
【发布时间】:2015-04-01 01:51:01
【问题描述】:

我正在做一个测试:比较 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 函数更快?
  • 我希望 go 代码内联 show() 的 go 版本,这是 go 代码相对于 C.show() 的进一步优势。
  • 这有点过时了,但接受的答案描述的性能仍然存在,成本大致相同(恒定)。问题中的示例夸大了成本的原因 - 很多! - 是因为有问题的函数被内联了。这不是苹果对苹果的比较。

标签: c performance go cgo


【解决方案1】:

正如您所发现的,通过 CGo 调用 C/C++ 代码的开销相当高。所以一般来说,你最好尽量减少 CGo 调用的次数。对于上面的示例,与其在循环中重复调用 CGo 函数,不如将循环向下移动到 C。

Go 运行时如何设置其线程的许多方面可能会打破许多 C 代码的预期:

  1. Goroutines 在相对较小的堆栈上运行,通过分段堆栈(旧版本)或复制(新版本)来处理堆栈增长。
  2. Go 运行时创建的线程可能无法与libpthread 的线程本地存储实现正确交互。
  3. Go 运行时的 UNIX 信号处理程序可能会干扰传统的 C 或 C++ 代码。
  4. Go 重用操作系统线程来运行多个 Goroutine。如果 C 代码调用阻塞系统调用或以其他方式独占线程,则可能对其他 goroutine 有害。

出于这些原因,CGo 选择了在使用传统堆栈设置的单独线程中运行 C 代码的安全方法。

如果您来自 Python 等语言,在 C 中重写代码热点以加快程序速度的情况并不少见,您会感到失望。但与此同时,等效的 C 代码和 Go 代码之间的性能差距要小得多。

一般而言,我保留 CGo 用于与现有库的接口,可能使用小型 C 包装函数,可以减少我需要从 Go 进行的调用次数。

【讨论】:

【解决方案2】:

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

在 golang-nuts 上查看 this thread

总会有一些开销。 它比简单的函数调用更昂贵,但 比上下文切换便宜得多 (agl 正在记住早期的​​实现; 我们在公开发布之前删除了线程切换)。 现在的费用基本上只需要 做一个完整的寄存器集切换(不涉及内核)。 我猜它相当于十个函数调用。

另请参阅 this answer 链接 "cgo is not Go" 博客文章。

C 对 Go 的调用约定或可增长堆栈一无所知,因此对 C 代码的调用必须记录 goroutine 堆栈的所有细节,切换到 C 堆栈,并运行不知道如何执行的 C 代码它被调用,或者是负责程序的更大的 Go 运行时。

因此,cgo 有开销,因为它执行 堆栈切换,而不是线程切换。

调用C函数时保存和恢复所有寄存器,调用Go函数或汇编函数时不需要。


除此之外,cgo 的调用约定禁止将 Go 指针直接传递给 C 代码,常见的解决方法是使用C.malloc,因此引入了额外的分配。详情请见this question

【讨论】:

    【解决方案3】:

    从 Go 调用 C 函数有一点开销。这是无法更改的。

    【讨论】:

      猜你喜欢
      • 2023-03-28
      • 1970-01-01
      • 2015-02-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多