Tag Archives: cgo

尝试让 cgo 调用绕过线程池

go 语言中,可以通过 cgo 来调用 C 库。但是由于 goroutine 的机制,外部的 C 函数调用可能能够很快返回,也可能执行很长时间。为了 goroutine 调度不被阻塞,就一律对每个 cgo 调用都从线程池中取一个线程来执行,完成后再返回原 goroutine。这样一来,每个 cgo 调用都带来了巨大的额外开销。所以 go 的很多库在实现时,都没有通过包装 C 库,而是选择完全用 go 来实现。这就使得 go 少了大量现有的 C 库可以利用。

读过 go 的代码后发现,要让 cgo 调用不通过线程池调用并不算麻烦,所以就自己修改了一下 cgo 命令。如下面的代码中

package cgo

//
// int add(int a, int b) {
//   int ret = a + b;
//   return ret;
// }
//
import "C"

func CAdd(a, b int) int {
  return int(C.add(C.int(a), C.int(b)))
}

func AsmCAdd(a, b int) int {
  return int(c.add(C.int(a), C.int(b)))
}

C.add 是传统的 cgo 调用方式。c.add 则是修改后不经过线程池的方式。两者可以并存,程序员可以自己判断 C 函数的执行时间,来考虑使用哪种方式。

性能测试代码:

import "testing"

func BenchmarkNormal(b *testing.B) {
  for i:= 0; i < b.N; i++ {
    CAdd(i, i);
  }
}

func BenchmarkDirect(b *testing.B) {
  for i:= 0; i < b.N; i++ {
    AsmCAdd(i, i);
  }
}

测试结果:

testing: warning: no tests to run
PASS
BenchmarkNormal	5000000	      307 ns/op
BenchmarkDirect	50000000	       31.0 ns/op
ok  	cgo	3.437s

可以看出,直接调用的性能大约是传统调用方式的 10 倍。

虽然目前的实现不算很漂亮,但是这玩意给了 go 更强的能力。而且只修改了 cgo 工具,并没有影响 go runtime 和标准库,不破坏兼容性。

代码在此,基于 go 1.4 修改。

https://github.com/xiezhenye/go/tree/directly-cgo/src/cmd/cgo

但是进一步实验发现这样还是有问题。

首先无法通过正常的方式调用 export 出来的 go 函数。这是因为正常的 cgo 调用会先 entersyscall,完成后 exitsyscall。在回调 go 时,会先 exitsyscall,然后 reentersyscall。既然之前跳过了那一步,这里自然就会出错。这点要解决就得改 runtime 了。

然后是个更要命的问题。这种方式下运行的 C 函数是运行在 goroutine 的栈里的。而 goroutine 的栈是由 go runtime 管理的。初始很小,按需扩大。但是 runtime 不知道 C 函数的情况,无法为其扩栈。如果 C 函数使用的栈空间超过了 goroutine 剩余的栈空间可能破坏其他 goroutine 的栈。这一点就基本不可能解决了。因为即使在 cgo 调用前预先扩栈,也不知道究竟需要扩多少才够,也无法对其进行保护,避免越界。