如何使用cgo封装推理模型

基本原理

package main

//int sum(int a, int b) { return a+b; }
import "C"

func main() {
	println(C.sum(1, 1))
}

img

生成的桩代码用于实现Go语言到C语言函数跨界调用:

main.go::C.sum
	_cgo_types.go::_Cfunc_sum	
		runtime.cgo._cgo_runtime_cgocall(_Cfunc_sum)
			
		main.cgo.2.c::_cgo_8ded9d269593_Cfunc_sum 
		main.cgo.2.c::sum()
    _cgo_types.go::_Cfunc_sum
return

使用方法

假设现在有一个C++的库 libeng.so ,现在展示如何使用 cgo 调用 libeng.so 中函数:

namespace eng {
void callEngine() {}
void init(string& protofile) {}   
}
  1. 首先我们需要构建出一个不含C++语言特性的静态库 libapi.a

    1. 由于 cgo 不能识别 C++ 特性,因此需要将涉及到 C++ 特性的地方用C语言转化出来。这里需要构建一个 wrapper.h 将 libapi.a 中的函数转化为不含C++特性的函数。

      // wrapper.h
      #include "eng.h"
      void init(char* protofile) {
          eng::init(string(protofile));
      }
      void callEngine() {
          eng::callEngine();
      }
      
    2. 然后构建 libapi.a 所需要的源文件

      // apic.cc
      #include <errno.h>
      // 这里的 errno 可用于生成golang中调用cgo函数,返回error
      extern "C" {
          #include "wrapper.h"
      }
      
      void api_init() {
          init();
      }
      
      void api_call_engine() {
          callEngine();
      }
      
      
      // apic.h
      void api_init();
      void api_call_engine();
      
    3. 使用 gcc 编译静态库

      gcc -c -o api.o apic.cc -I ./
      ar rcs libapi.a api.o
      
  2. 在 go 代码中创建一个包 engine ,对外提供 go 语言风格的函数

    1. 一般而言会创建两个 .go 文件,一个用于 cgo 代码生成闭包函数,另外一个用于封装,提供可导出函数

      // engine.go
      package engine
      
      /*
      #cgo CXXFLAGS: -std=c++11
      #cgo CFLAGS: -Ipath_to_apic.h
      #cgo LDFLAGS: -L${SRCDIR}/relpath_to_api.a/ -lapi -lstdc++
      #cgo LDFLAGS: -L${SRCDIR}/relpath_to_libeng.so/ -leng
      
      #include "apic.h"
      #include <errno.h>
      */
      import "C"
      
      func cgo_init() error {
      	return C.api_init()
      }
      
      func cgo_call_engine(s string) error {
      	return C.api_call_engine(C.CString(s))
      }
      
      
      
      // api.go
      package engine
      
      import "fmt"
      
      func Init() error {
      	return cgo_init()
      }
      
      func CallEngine(protofile string) error {
          return cgo_call_engine(protofile)
      }
      

性能损耗

  1. C语言函数调用产生的额外开销

    将C语言函数调用次数压缩,尽可能少开放接口

  2. C语言函数阻塞导致该线程阻塞,Go无法取得对该线程的管理,因此这个线程上面无法开辟新的goroutine

    线程数可能会暴涨

  3. 其他开销:C语言仍需要手动GC、Go工具无法获取C语言代码的相关堆栈信息导致调试困难

分别使用cgo和cpp调用封装好的模型调用接口的耗时情况为

>>>>> cgo
[model] warm up model ok
[model] total iter numbers: 6
[model] iter: 0, time : 5409 ms
[model] iter: 1, time : 2012 ms
[model] iter: 2, time : 1848 ms
[model] iter: 3, time : 1843 ms
[model] iter: 4, time : 1844 ms
[model] iter: 5, time : 1845 ms
write done
cgo cost 32861 ms


>>>>> cpp
[model] warm up model ok
[model] total iter numbers: 6
[model] iter: 0, time : 5370 ms
[model] iter: 1, time : 1993 ms
[model] iter: 2, time : 1824 ms
[model] iter: 3, time : 1828 ms
[model] iter: 4, time : 1828 ms
[model] iter: 5, time : 1830 ms
write done
c++ cost 32804 ms

注意这里的函数为 void foo() {...} ,调用没有涉及到Go和C变量之间的类型转换。

另外,cgo 在运行时会出现到 C++ 检测不到的一些运行时错误,例如:free(): invalid pointer

参考

  • Go语言高级编程

  • Go语言圣经

  • Go语言精进之路

  • C++组件的输入输出是什么?Go 是强类型语言,所以 cgo 中传递的参数类型必须与声明的类型完全一致,而且传递前必须用”C” 中的转化函数转换成对应的 C 类型,不能直接传入 Go 中类型的变量。

    package main
    
    import "C"
    
    func main() {
        v := 42
        C.printint(C.int(v))
    }
    

    例如:指针、引用、结构体等类型之间的转化

  • 在Go语言中,每个Go协程都有自己的栈空间和调用栈。当调用C语言函数时,Go语言会将当前协程的栈空间和调用栈保存起来,并创建一个新的C语言栈空间和调用栈。然后,将C语言函数的参数和返回值从Go语言栈空间中复制到C语言栈空间中,并调用C语言函数。当C语言函数返回时,将返回值从C语言栈空间中复制到Go语言栈空间中,并恢复当前协程的栈空间和调用栈。

  •   package main
      
      //static const char* cs = "hello";
      import "C"
      import "./cgo_helper"
      
      func main() {
          cgo_helper.PrintCString(C.cs)
      }
    

    这段代码是不能正常工作的,因为当前 main 包引入的 C.cs 变量的类型是当前 main 包的 cgo 构造的虚拟的 C 包下的 *char 类型(具体点是 *C.char,更具体点是 *main.C.char),它和 cgo_helper 包引入的 *C.char 类型(具体点是 *cgo_helper.C.char)是不同的。在 Go 语言中方法是依附于类型存在的,不同 Go 包中引入的虚拟的 C 包的类型却是不同的(main.C 不等 cgo_helper.C),这导致从它们延伸出来的 Go 类型也是不同的类型(*main.C.char 不等 *cgo_helper.C.char),这最终导致了前面代码不能正常工作。

    有 Go 语言使用经验的用户可能会建议参数转型后再传入。但是这个方法似乎也是不可行的,因为 cgo_helper.PrintCString 的参数是它自身包引入的 *C.char 类型,在外部是无法直接获取这个类型的。换言之,一个包如果在公开的接口中直接使用了 *C.char 等类似的虚拟 C 包的类型,其它的 Go 包是无法直接使用这些类型的,除非这个 Go 包同时也提供了 *C.char 类型的构造函数。因为这些诸多因素,如果想在 go test 环境直接测试这些 cgo 导出的类型也会有相同的限制。

    对C模块的封装包,导出函数的输入输出应该都要是Go语言类型的

  • #cgo 语句主要影响 CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS 和 LDFLAGS 几个编译器环境变量。LDFLAGS 用于设置链接时的参数,除此之外的几个变量用于改变编译阶段的构建参数 (CFLAGS 用于针对 C 语言代码设置编译参数)。

  • 更严谨的做法是为 C 语言函数接口定义严格的头文件,然后基于稳定的头文件实现代码。

  • 使用到的调试命令:

    ldd ./libs/libapi.so # 查看链接依赖
    objdump -p api.a	 # 查看函数签名
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值