使用CGO进行GC优化的注意事项

前阵子我利用cgo对游戏内存数据库的数据存储方式做了优化,减少了对象数量。但是程序放到线上环境后出现了段错误,直接导致进程退出,只好临时又把优化的部分去掉,去掉后程序又继续稳定运行了两周。

优化代码撤下来后,我重新整理了代码。整理下来,我觉得对含有字符串字段的表的优化逻辑太过复杂了,并且很难控制边界情况。

这里举个例子:

type MyTable struct {
    Name string
}

func InsertMyTable(myTable MyTable) {
    nameLen := C.size_t(len(myTable.Name))
    name := C.calloc(1, nameLen)
    C.memcpy(name, unsafe.Pointer((*reflect.StringHeader)(unsafe.Pointer(&myTable.Name)).Data), nameLen)
    (*reflect.StringHeader)(unsafe.Pointer(&myTable.Name)).Data = uintptr(name)
}

func UpdateMyTable(myTable MyTable) {
    nameLen := C.size_t(len(myTable.Name))
    name := C.calloc(1, nameLen)
    C.memcpy(name, unsafe.Pointer((*reflect.StringHeader)(unsafe.Pointer(&myTable.Name)).Data), nameLen)
    (*reflect.StringHeader)(unsafe.Pointer(&myTable.Name)).Data = uintptr(name)

    C.free(unsafe.Pointer((*reflect.StringHeader)(unsafe.Pointer(&oldMyTable.Name)).Data))
}

func DeleteMyTable(myTable MyTable) {
    C.free(unsafe.Pointer((*reflect.StringHeader)(unsafe.Pointer(&myTable.Name)).Data))
}

上面的代码是对项目中遇到的问题的模拟,不是真实代码,真实代码其实比这个要复杂,因为对象会被用于事务提交,还需要控制对象在事务提交后才能释放字符串类型字段,在更新时还需要判断字符串是否有变更等等。

为什么需要对字符串进行处理呢?因为如果不对字符串进行处理的话,当go的字符串被赋值给cgo创建的内存块后,go并不不清楚字符串被引用,从而导致有用的字符串被gc回收。

同样的道理也适用于嵌套的结构,例如:

type MyTable struct {
    ChildTable *MyChildTable
}

如果一个go创建的MyChildTable对象被赋值给一个cgo维护的MyTable对象的ChildTable字段,go的gc是跟踪不到这个引用关系的,这时候会出现MyTable对象还有效的时候,内部的ChildTable字段所引用的go对象已经被回收,如果程序访问ChildTable对象,就会出现段错误。

但是子表的情况是比较好处理的,只要原来new(MyChildTable)的地方替换为自己实现的newMyChildTable(),用cgo来申请内存,自己手工释放,就不会有问题,边界情况也没有字符串那么多。代码像这样:

sizeofMyChildTable := unsafe.SizeOf(MyChildTable{})

func newMyChildTable() *MyChildTable {
    return (*MyChildTable)(C.calloc(1, C.size_t(sizeofMyChildTable)))
}

排查段错误很困难,所以我想先做排除法,首先去掉了最复杂的字符串优化逻辑,含有字符串类型字段的内存表都不进行优化。还好游戏中字符串用得不多,只有少数几个表有用到字符串,稍微降低优化效果提高程序稳定性,还是划算的。

去掉字符串优化后的新版程序,已经稳定允许了一周,算是正式验证了cgo进行GC优化的有效性。

本文来自:达达的主页

感谢作者:达达

查看原文:使用CGO进行GC优化的注意事项

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。