這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
前陣子我利用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最佳化的有效性。