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 调用前预先扩栈,也不知道究竟需要扩多少才够,也无法对其进行保护,避免越界。