IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    尝试让 cgo 调用绕过线程池

    神仙发表于 2015-05-28 14:00:13
    love 0

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



沪ICP备19023445号-2号
友情链接