从Golang调用C函数

xakepp35:

我想在Golang中编写控制器逻辑并处理Golang中的json和数据库,同时在C中使用数学处理模型。我认为调用C函数的开销必须与设置寄存器rcx,rdx,rsi,rdi,doin一样低fastcall和摆脱rax价值。但是我听说过CGO的开销很大

假设我有通用的fastcall x64 c函数int64 f(int64 a,b,c,d){return a+b+c+d}如何在go testing.B基准测试中从头开始调用它,以获得最高的潜在基准测试分数

PS没有指针传递,没有技巧,只是对如何以最可靠的方式访问C接口感兴趣

成本:

在我看来,调用C函数的开销必须与设置寄存器rcx,rdx,rsi,rdi,进行一些快速调用并获取rax值一样低。但是我听说过cgo <...>的开销很大

您的意见是没有根据的。
从Go到C的调用具有明显的开销的原因是由于以下原因。

让我们首先考虑C

尽管语言没有任何要求,但由典型的编译器编译并作为常规进程在典型OS上运行的典型C程序在很大程度上依赖于OS来执行其运行时环境的某些方面。
所谓的最可见和最重要的方面是堆栈:内核负责在加载和初始化程序映像之后以及将执行转移到新生进程的代码入口点之前对其进行设置。

同样,另一个关键点是,尽管不是严格要求,但大多数C程序都依赖于OS本地线程来通过程序代码实现多个并发执行的流。

在C代码中执行的函数调用通常使用与操作系统和硬件实现的目标组合相同的ABI进行编译(当然,除非程序员已明确设法告诉编译器否则进行此操作-例如,标记特定的具有不同的调用约定)。

C没有自动方法来管理非堆栈内存(“堆”)。
这种管理通常是通过C系列的标准库功能完成的malloc(3)这些函数管理堆,并将通过它们分配的任何内存都视为“它们”(这很合逻辑)。
C不提供自动垃圾收集。

让我们回顾一下:一个用C编译的典型程序:使用OS提供的线程,并在这些线程中使用OS提供的堆栈。大多数情况下,函数调用遵循平台的ABI;堆内存由特殊的库代码管理;没有GC。

现在让我们考虑

  • Go代码的任何部分(程序的代码和运行时的代码)都在所谓的goroutine中运行,它们就像超轻量级线程一样。
  • Go运行时提供的goroutine调度程序(已编译/链接到Go中编写的任何程序中)实现了goroutine的所谓M×N调度-其中M个goroutine多路复用到N个OS提供的线程上,其中M通常是比N高很多
  • Go中的函数调用不遵循目标平台的ABI。
    具体来说,AFAIK当代版本的Go会传递堆栈中的所有调用参数。
  • goroutine始终 OS提供的线程运行
    等待Go运行时管理的某些资源(例如,通道,计时器,网络套接字等)上的goroutine不会占用OS线程。
    当调度程序选择执行例程时,它必须将其分配给Go运行时拥有的空闲OS线程。尽管调度程序努力将goroutine置于挂起之前正在执行的同一个线程上,但这并不总是成功,因此goroutine可以在不同的OS线程之间自由迁移。

以上几点自然导致goroutines具有自己的堆栈,这些堆栈完全独立于操作系统为其线程提供的堆栈。
与C不同,这些堆栈是可增长和可重新分配的。

堆内存由Go运行时自动管理,并直接完成,因此不使用C stdlib。
Go具有GC,并且该GC是并发的,因为它与执行程序代码的goroutines完全同时运行。

让我们来回顾一下:goroutine具有自己的堆栈,使用与平台的ABI或C都不兼容的调用约定,并且可能在不同的操作系统线程上执行。
Go运行时直接管理堆内存,并具有完全并行的GC。

现在考虑从Go到C的呼叫

正如您现在应该看到的那样,运行Go和C代码的运行时环境的“世界”足够不同,以至于存在很大的“阻抗不匹配”,这在执行FFI时需要一定的网关连接,而成本不为零。

特别是,当Go代码将要调用C时,必须执行以下操作:

  1. 必须将goroutine锁定到当前正在其上运行的OS线程(“固定”)。
  2. 由于必须根据平台的ABI完成目标C调用,因此必须保存当前的执行上下文-至少保存那些将被调用丢弃的寄存器。
  3. cgo机器必须验证将被传递给目标C调用不包含指向围棋管理的其他内存块的任何记忆,递归这是为了让Go的GC继续工作的同时。
  4. 必须将执行从goroutine堆栈切换到线程的堆栈:必须在线程堆栈上创建一个新的堆栈框架,并且必须根据平台的ABI将目标C调用的参数放置在该位置(和在寄存器中)。
  5. 拨打电话。
  6. 返回后,必须将执行切换回goroutine的堆栈-再次将所有返回的结果通过网关传递回执行的goroutine的堆栈框架。

正如您可能会看到的那样,不可避免的成本是很大的,而在某些CPU寄存器中放置值则是最微不足道的。

有什么可以做的

通常,有两种方法可以解决此问题:

  • 很少调用C。

    也就是说,如果每次对C的调用都执行大量的CPU密集型计算,则可以认为执行这些调用的开销与使这些调用执行的计算速度更快的收益相形见。

  • 在汇编中编写关键功能。

    Go允许直接在目标硬件平台的程序集中编写代码。

一种“技巧”可能会让您获得两全其美的效果,那就是利用大多数工业编译器的功能来输出其编译函数的汇编语言形式。因此,您可以使用C编译器提供的核心功能,例如自动矢量化(针对SSE)和积极的优化,然后抓住生成的所有内容并将其包装在薄薄的汇编层中,这基本上使生成的代码适应本机代码去的ABI。

有很多第三方Go软件包可以执行此操作(例如thisthat),显然Go运行时也可以执行此操作。

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章