转载自达达的博客
前阵子我利用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优化的有效性。
有疑问加站长微信联系(非本文作者)