【Free Style】CGO: Go与C互操作技术(三):Go调C性能分析及优化

举报
赵志强 发表于 2017/11/17 15:57:42 2017/11/17
【摘要】 测试环境测试机器为物理机,不是虚拟环境。采用ubuntu 16.04, x64。CPU主频2.6GHz,单核测试。Go的版本为1.8.0。原生Go的调用开销这个是测量的原生Go的开销,作为一个基准,测试代码如下\\ main.gopackage mainimport _ "unsafe"//go:noinlinefunc add3(a, b, c int) int {return a + b +

测试环境

测试机器为物理机,不是虚拟环境。采用ubuntu 16.04, x64。CPU主频2.6GHz,单核测试。Go的版本为1.8.0。

原生Go的调用开销

这个是测量的原生Go的开销,作为一个基准,测试代码如下

\\ main.gopackage mainimport _ "unsafe"//go:noinlinefunc add3(a, b, c int) int {return a + b + c
}//go:linkname nanotime runtime.nanotimefunc nanotime() int64 // 如果哪个兄弟想用这个函数,请在目录下建立一个空的asm_amd64.s// 具体原因,在后面的博客中会有介绍// 这个函数与time.Now的区别是没有抢占点,除此之外一模一样const num int = 10000000func main() {var sum int = 0t1 := nanotime()for i := 0; i < num; i ++ {
sum = add3(i, i+1, i+2)
}
t2 := nanotime()println("Time: ", float64(t2-t1)/float64(num), " ns/Op")println("Result:", sum) // println函数是一个builtin的函数,输出到标准错误输出}

输出结果为

time taskset -c 20 ./main
Time:  +3.270027e+000  ns/Op
Result: 30000000

原生Go调C的开销

按照Go的语法使用Go调C

\\ main.gopackage main/*
int __attribute__((noinline)) add3(int a, int b, int c) {
return a + b + c;
}
*/import "C"const num C.int = 10000000func main() {var sum C.int = 0var i C.intt1 := nanotime()for i = 0; i < num; i++ {
sum = C.add3(i, i+1, i+2)
}
t2 := nanotime()println("Time: ", float64(t2-t1)/float64(num), " ns/Op")println("Result:", sum)
}

由于在使用import "C"的文件中,不能使用编译制导语句,所以需要另外一个文件完成nanotime的实现

package mainimport _ "unsafe"//go:linkname nanotime runtime.nanotimefunc nanotime() int64

输出结果为:

time taskset -c 20 ./main
Time:  +8.416507e+001  ns/Op
Result: 30000000

通过Go调C花的时间大约是纯Go的26倍。

优化Go调C:去掉调度设置

具体做法的话,就是把C的函数地址拿到,然后通过asmcgocall直接调用。参数的传递通过结构体传递。代码实现如下:

// main.gopackage main/*
#include <stdio.h>
int __attribute__((noinline)) add3(int a, int b, int c) {
return a + b + c;
}
typedef struct {
int p0;
int p1;
int p2;
int r;
} passer;
int __attribute__((noinline)) x_csub(void *v) {
passer* pv = (passer*)v;
pv->r = pv->p0 + pv->p1 + pv->p2;
}
*/import "C"const num int32 = 10000000func main() {var sum int32 = 0var i int32t1 := nanotime()for i = 0; i < num; i++ {
sum = add3(i, i+1, i+2)
}
t2 := nanotime()println("Time: ", float64(t2-t1)/float64(num), " ns/Op")println("Result:", sum)
}

第二个文件

package mainimport "unsafe"//go:linkname asmcgocall runtime.asmcgocallfunc asmcgocall(fn, arg unsafe.Pointer) int32//go:linkname nanotime runtime.nanotimefunc nanotime() int64//go:cgo_import_static x_csub//go:linkname x_csub x_csub//go:linkname _csub _csubvar x_csub bytevar _csub = unsafe.Pointer(&x_csub)type passer struct {
a, b, c, r int32}var p passerfunc add3(a, b, c int32) (r int32) {
p.a = a
p.b = b
p.c = c
p.r = r
asmcgocall(_csub, unsafe.Pointer(&p))return p.r
}

结果输出:

Time:  +1.800196e+001  ns/Op
Result: 30000000

时间是原生的Go调C的0.21倍。这个结果是仍然有切栈操作的。

优化Go调C:继续去掉切栈操作

继续去掉Go调C的切栈操作,即把一个C函数当成原生的Go函数,去掉切栈操作。代码如下

// main.gopackage main/*
#include <stdio.h>
int __attribute__((noinline)) add3(int a, int b, int c) {
return a + b + c;
}
typedef struct {
int p0;
int p1;
int p2;
int r;
} passer;
int __attribute__((noinline)) x_csub(void *v) {
passer* pv = (passer*)v;
pv->r = pv->p0 + pv->p1 + pv->p2;
}
*/import "C"const num int32 = 10000000func main() {var sum int32 = 0var i int32t1 := nanotime()for i = 0; i < num; i++ {
sum = add3(i, i+1, i+2)
}
t2 := nanotime()println("Time: ", float64(t2-t1)/float64(num), " ns/Op")println("Result:", sum)
}

与上一个例子类似,另外一个文件为:

// lib.gopackage mainimport "mycall"import "unsafe"//go:linkname asmcgocall runtime.asmcgocallfunc asmcgocall(fn, arg unsafe.Pointer) int32//go:linkname nanotime runtime.nanotimefunc nanotime() int64//go:cgo_import_static x_csub//go:linkname x_csub x_csub//go:linkname _csub _csubvar x_csub bytevar _csub = unsafe.Pointer(&x_csub)type passer struct {
a, b, c, r int32}var p passerfunc add3(a, b, c int32) (r int32) {
p.a = a
p.b = b
p.c = c
p.r = r
mycall.Mycall(_csub, unsafe.Pointer(&p))return p.r
}

这里面用到的mycall实现如下:

// mycall.gopackage mycallimport "unsafe"func Mycall(fn, arg unsafe.Pointer)

函数体实现为:

TEXT ·Mycall(SB),0,$0-20MOVQ fn+0(FP), AX
MOVQ arg+8(FP), BX
MOVQ BX, DI
CALL AX
RET

输出结果为:

Time:  +8.875633e+000  ns/Op
Result: 30000000

是原生Go调C的0.105倍。

Go调C优化:把C函数变成Go的汇编

// main.gopackage mainimport _ "unsafe"//go:linkname nanotime runtime.nanotimefunc nanotime() int64func add3(int32, int32, int32) int32const num int32 = 10000000func main() {var sum int32 = 0var i int32t1 := nanotime()for i = 0; i < num; i++ {
sum = add3(i, i+1, i+2)
}
t2 := nanotime()println("Time: ", float64(t2-t1)/float64(num), " ns/Op")println("Result:", sum)
}

add3的函数体

// asm_amd64.sTEXT ·add3+0(SB),4,$0-24MOVL    c+8(FP),AX
ADDL    b+4(FP),AX
ADDL    a+0(FP),AX
MOVL    AX, ret+16(FP)
RET

输出结果为:

Time:  +3.390647e+000  ns/Op
Result: 30000000

以及跟原生的Go很接近了。

总结

操作时间(ns)相对于Go
Go3.271.00
原生Go C84.1625.74
去掉调度18.005.50
去掉切栈8.882.72
变成汇编3.391.04


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。